Theming #

Theming adalah fondasi visual sebuah aplikasi — ia mendefinisikan warna, tipografi, bentuk, dan gaya komponen secara terpusat sehingga konsisten di seluruh aplikasi. Flutter menggunakan ThemeData sebagai kontainer untuk semua keputusan visual ini, dan sejak Flutter 3.16, Material 3 (M3) menjadi default. Memahami sistem theming Flutter berarti kamu bisa mengubah tampilan seluruh aplikasi dengan beberapa baris kode.

ThemeData — Kontainer Visual Aplikasi #

ThemeData memegang semua properti visual yang mempengaruhi seluruh aplikasi: warna, tipografi, shape, dan tema per-komponen. Penampilan komponen Material 3 ditentukan terutama oleh nilai ThemeData.colorScheme dan ThemeData.textTheme.

MaterialApp(
  theme: ThemeData(
    // Material 3 adalah default sejak Flutter 3.16
    useMaterial3: true,

    // Skema warna dari seed color tunggal
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1A73E8),
    ),

    // Tipografi
    textTheme: const TextTheme(
      displayLarge: TextStyle(fontSize: 57, fontWeight: FontWeight.bold),
      titleLarge: TextStyle(fontSize: 22, fontWeight: FontWeight.w600),
      bodyMedium: TextStyle(fontSize: 14),
    ),
  ),
  home: const HomeScreen(),
)

Material 3 dan ColorScheme #

Penampilan komponen Material 3 ditentukan terutama oleh nilai ThemeData.colorScheme dan ThemeData.textTheme. ColorScheme memudahkan pembuatan skema gelap dan terang sehingga aplikasimu estetis sekaligus memenuhi persyaratan aksesibilitas.

ColorScheme.fromSeed — Skema Otomatis #

Material 3 memungkinkan pengguna mengatur seluruh tema warna aplikasi dari satu seed color. Dengan mengatur parameter seed color di konstruktor tema, Flutter akan menghasilkan color scheme yang harmonis untuk setiap widget di aplikasimu dari entri ini.

ColorScheme.fromSeed(
  seedColor: const Color(0xFF1A73E8),  // warna utama brand
  // Flutter secara otomatis menghasilkan:
  // primary, onPrimary, primaryContainer, onPrimaryContainer
  // secondary, onSecondary, secondaryContainer, onSecondaryContainer
  // tertiary, onTertiary, tertiaryContainer, onTertiaryContainer
  // error, onError, errorContainer, onErrorContainer
  // surface, onSurface, surfaceVariant, onSurfaceVariant
  // outline, shadow, inverseSurface, dll.
)

ColorScheme Manual — Kontrol Penuh #

const ColorScheme lightScheme = ColorScheme(
  brightness: Brightness.light,

  // Warna utama -- untuk FAB, tombol utama, elemen aktif
  primary: Color(0xFF006E4A),
  onPrimary: Colors.white,
  primaryContainer: Color(0xFF8FF8C8),
  onPrimaryContainer: Color(0xFF002115),

  // Warna sekunder -- untuk filter chip, elemen pendukung
  secondary: Color(0xFF4D6357),
  onSecondary: Colors.white,
  secondaryContainer: Color(0xFFCFE9DA),
  onSecondaryContainer: Color(0xFF0A1F16),

  // Warna tersier -- aksen kontras
  tertiary: Color(0xFF3D6473),
  onTertiary: Colors.white,
  tertiaryContainer: Color(0xFFC1E9FB),
  onTertiaryContainer: Color(0xFF001F2A),

  // Surface -- latar belakang komponen
  surface: Color(0xFFFBFDF9),
  onSurface: Color(0xFF191C1A),

  // Error
  error: Color(0xFFBA1A1A),
  onError: Colors.white,
  errorContainer: Color(0xFFFFDAD6),
  onErrorContainer: Color(0xFF410002),

  // Outline dan shadow
  outline: Color(0xFF717970),
);

Peran Warna di Material 3 #

Setiap warna di ColorScheme memiliki peran:

primary         -- warna utama (tombol, FAB, AppBar aktif)
onPrimary       -- teks/ikon DI ATAS primary (harus kontras)
primaryContainer -- versi terang dari primary (chip, card header)
onPrimaryContainer -- teks di atas primaryContainer

surface         -- latar belakang card, dialog, sheet
onSurface       -- teks utama di atas surface
surfaceVariant  -- variant surface untuk kontainer
onSurfaceVariant -- teks sekunder di atas surfaceVariant

error           -- warna error
onError         -- teks di atas error

Dark Mode — ThemeData untuk Tema Gelap #

Flutter mendukung tema terang dan gelap secara native melalui dua parameter di MaterialApp:

MaterialApp(
  // Tema untuk light mode
  theme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1A73E8),
      brightness: Brightness.light,
    ),
  ),

  // Tema untuk dark mode
  darkTheme: ThemeData(
    useMaterial3: true,
    colorScheme: ColorScheme.fromSeed(
      seedColor: const Color(0xFF1A73E8),
      brightness: Brightness.dark,   // Flutter otomatis generate warna yang sesuai
    ),
  ),

  // Kapan menggunakan theme vs darkTheme
  themeMode: ThemeMode.system,  // ikuti pengaturan sistem (default)
  // ThemeMode.light   -- selalu light
  // ThemeMode.dark    -- selalu dark
  home: const HomeScreen(),
)

Dynamic Dark Mode — Toggle oleh Pengguna #

class MyApp extends StatefulWidget {
  const MyApp({super.key});
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  ThemeMode _themeMode = ThemeMode.system;

  void _toggleTheme() {
    setState(() {
      _themeMode = _themeMode == ThemeMode.light
          ? ThemeMode.dark
          : ThemeMode.light;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: AppTheme.light,
      darkTheme: AppTheme.dark,
      themeMode: _themeMode,
      home: HomeScreen(onToggleTheme: _toggleTheme),
    );
  }
}

// Akses brightness di widget
@override
Widget build(BuildContext context) {
  final isDark = Theme.of(context).brightness == Brightness.dark;
  return Icon(
    isDark ? Icons.light_mode : Icons.dark_mode,
  );
}

TextTheme — Tipografi Konsisten #

TextTheme mendefinisikan hierarki tipografi berdasarkan peran semantik — bukan ukuran hardcoded:

ThemeData(
  textTheme: const TextTheme(
    // Display -- heading sangat besar (hero section, splash)
    displayLarge:  TextStyle(fontSize: 57, fontWeight: FontWeight.w400),
    displayMedium: TextStyle(fontSize: 45, fontWeight: FontWeight.w400),
    displaySmall:  TextStyle(fontSize: 36, fontWeight: FontWeight.w400),

    // Headline -- judul halaman dan section
    headlineLarge:  TextStyle(fontSize: 32, fontWeight: FontWeight.w400),
    headlineMedium: TextStyle(fontSize: 28, fontWeight: FontWeight.w400),
    headlineSmall:  TextStyle(fontSize: 24, fontWeight: FontWeight.w400),

    // Title -- judul card, dialog, app bar
    titleLarge:  TextStyle(fontSize: 22, fontWeight: FontWeight.w500),
    titleMedium: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
    titleSmall:  TextStyle(fontSize: 14, fontWeight: FontWeight.w500),

    // Body -- teks konten utama
    bodyLarge:  TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
    bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
    bodySmall:  TextStyle(fontSize: 12, fontWeight: FontWeight.w400),

    // Label -- tombol, tab, chip
    labelLarge:  TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
    labelMedium: TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
    labelSmall:  TextStyle(fontSize: 11, fontWeight: FontWeight.w500),
  ),
)

// Gunakan di widget -- bukan hardcode ukuran!
Text(
  'Judul Halaman',
  style: Theme.of(context).textTheme.titleLarge,   // semantik, bukan px
)
Text(
  'Konten artikel...',
  style: Theme.of(context).textTheme.bodyMedium,
)

Google Fonts di TextTheme #

import 'package:google_fonts/google_fonts.dart';

ThemeData(
  textTheme: GoogleFonts.interTextTheme(
    // Opsional: override beberapa style
    Theme.of(context).textTheme.copyWith(
      displayLarge: GoogleFonts.playfairDisplay(
        fontSize: 57,
        fontWeight: FontWeight.bold,
      ),
    ),
  ),
)

Component Themes — Override Per Komponen #

Selain colorScheme dan textTheme, ThemeData memiliki banyak component theme untuk mengkustomisasi tampilan komponen spesifik:

ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),

  // AppBar theme
  appBarTheme: const AppBarTheme(
    centerTitle: false,
    elevation: 0,
    scrolledUnderElevation: 2,
    titleTextStyle: TextStyle(fontSize: 20, fontWeight: FontWeight.w600),
  ),

  // ElevatedButton theme
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      minimumSize: const Size(double.infinity, 48),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    ),
  ),

  // OutlinedButton theme
  outlinedButtonTheme: OutlinedButtonThemeData(
    style: OutlinedButton.styleFrom(
      minimumSize: const Size(double.infinity, 48),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    ),
  ),

  // Card theme
  cardTheme: CardThemeData(
    elevation: 0,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
      side: BorderSide(color: Colors.grey.shade200),
    ),
  ),

  // InputDecoration theme (TextField global)
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
    filled: true,
    contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
  ),

  // NavigationBar theme
  navigationBarTheme: NavigationBarThemeData(
    labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
    elevation: 2,
  ),

  // BottomSheet theme
  bottomSheetTheme: const BottomSheetThemeData(
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
    ),
  ),

  // Chip theme
  chipTheme: const ChipThemeData(
    shape: StadiumBorder(),
  ),
)

Mengakses Tema di Widget #

@override
Widget build(BuildContext context) {
  // Akses seluruh ThemeData
  final theme = Theme.of(context);

  // Akses ColorScheme
  final colors = Theme.of(context).colorScheme;

  // Akses TextTheme
  final texts = Theme.of(context).textTheme;

  return Container(
    color: colors.primaryContainer,
    child: Text(
      'Halo',
      style: texts.titleLarge!.copyWith(
        color: colors.onPrimaryContainer,
      ),
    ),
  );
}

Local Theme Override dengan Theme Widget #

Untuk mengubah tema hanya di bagian tertentu dari widget tree tanpa mempengaruhi seluruh aplikasi:

// Override seluruh ThemeData (tidak mewarisi dari parent)
Theme(
  data: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.pink)),
  child: const FloatingActionButton(
    onPressed: null,
    child: Icon(Icons.add),
  ),
)

// Extend (mewarisi dari parent dan override sebagian) -- lebih direkomendasikan
Theme(
  data: Theme.of(context).copyWith(
    colorScheme: Theme.of(context).colorScheme.copyWith(
      primary: Colors.red,
    ),
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
    ),
  ),
  child: const DangerZoneSection(),
)

ThemeExtension — Tambahkan Warna Kustom ke Tema #

ThemeExtension memungkinkan kamu menambahkan nilai kustom ke sistem tema — misalnya warna semantic yang tidak ada di ColorScheme standar:

// Definisikan extension
@immutable
class AppColors extends ThemeExtension<AppColors> {
  final Color? sukses;
  final Color? peringatan;
  final Color? infoBackground;
  final Color? kartePremium;

  const AppColors({
    required this.sukses,
    required this.peringatan,
    required this.infoBackground,
    required this.kartePremium,
  });

  // copyWith untuk immutability
  @override
  AppColors copyWith({
    Color? sukses,
    Color? peringatan,
    Color? infoBackground,
    Color? kartePremium,
  }) {
    return AppColors(
      sukses: sukses ?? this.sukses,
      peringatan: peringatan ?? this.peringatan,
      infoBackground: infoBackground ?? this.infoBackground,
      kartePremium: kartePremium ?? this.kartePremium,
    );
  }

  // lerp untuk interpolasi antar theme (dark/light transition)
  @override
  AppColors lerp(AppColors? other, double t) {
    if (other is! AppColors) return this;
    return AppColors(
      sukses: Color.lerp(sukses, other.sukses, t),
      peringatan: Color.lerp(peringatan, other.peringatan, t),
      infoBackground: Color.lerp(infoBackground, other.infoBackground, t),
      kartePremium: Color.lerp(kartePremium, other.kartePremium, t),
    );
  }
}

// Daftarkan di ThemeData
ThemeData(
  extensions: [
    AppColors(
      sukses: const Color(0xFF2E7D32),
      peringatan: const Color(0xFFF57C00),
      infoBackground: const Color(0xFFE3F2FD),
      kartePremium: const Color(0xFFFFD700),
    ),
  ],
)

// Akses di widget
final appColors = Theme.of(context).extension<AppColors>()!;
Container(
  color: appColors.sukses,
  child: const Icon(Icons.check, color: Colors.white),
)

Organisasi Kode Tema #

Pisahkan definisi tema ke file terpisah untuk keterbacaan:

// lib/core/theme/app_theme.dart
class AppTheme {
  static ThemeData get light => ThemeData(
    useMaterial3: true,
    colorScheme: _lightColorScheme,
    textTheme: _textTheme,
    appBarTheme: _appBarTheme,
    elevatedButtonTheme: _elevatedButtonTheme,
    cardTheme: _cardTheme,
    inputDecorationTheme: _inputDecorationTheme,
    extensions: [_lightAppColors],
  );

  static ThemeData get dark => ThemeData(
    useMaterial3: true,
    colorScheme: _darkColorScheme,
    textTheme: _textTheme,
    appBarTheme: _appBarTheme,
    elevatedButtonTheme: _elevatedButtonTheme,
    cardTheme: _cardTheme,
    inputDecorationTheme: _inputDecorationTheme,
    extensions: [_darkAppColors],
  );

  static const _lightColorScheme = ColorScheme(
    brightness: Brightness.light,
    primary: Color(0xFF1A73E8),
    onPrimary: Colors.white,
    // ... semua warna
  );

  static const _darkColorScheme = ColorScheme(
    brightness: Brightness.dark,
    primary: Color(0xFF8AB4F8),
    onPrimary: Color(0xFF003580),
    // ... semua warna
  );

  static final _textTheme = TextTheme(
    titleLarge: GoogleFonts.inter(fontSize: 22, fontWeight: FontWeight.w600),
    bodyMedium: GoogleFonts.inter(fontSize: 14),
    labelLarge: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w500),
  );

  static const _lightAppColors = AppColors(
    sukses: Color(0xFF2E7D32),
    peringatan: Color(0xFFF57C00),
    infoBackground: Color(0xFFE3F2FD),
    kartePremium: Color(0xFFFFD700),
  );

  static const _darkAppColors = AppColors(
    sukses: Color(0xFF4CAF50),
    peringatan: Color(0xFFFF9800),
    infoBackground: Color(0xFF0D2137),
    kartePremium: Color(0xFFFFD700),
  );

  // Komponen theme sebagai static getter
  static const _appBarTheme = AppBarTheme(
    centerTitle: false,
    elevation: 0,
  );

  static final _elevatedButtonTheme = ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      minimumSize: const Size(double.infinity, 48),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    ),
  );

  static final _cardTheme = CardThemeData(
    elevation: 0,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  );

  static final _inputDecorationTheme = InputDecorationTheme(
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
    filled: true,
  );
}

Ringkasan #

  • ThemeData adalah kontainer visual aplikasi — definisikan di MaterialApp.theme dan MaterialApp.darkTheme. Material 3 adalah default sejak Flutter 3.16.
  • ColorScheme.fromSeed menghasilkan color scheme harmonis dari satu seed color secara otomatis — tersedia versi light dan dark.
  • ThemeMode mengontrol kapan dark theme digunakan: system (ikuti OS), light, atau dark. Toggle programatik menggunakan setState.
  • TextTheme mendefinisikan hierarki tipografi berdasarkan peran semantik (displayLarge, titleLarge, bodyMedium, dll.) — gunakan peran ini alih-alih hardcode ukuran font.
  • Component themes (appBarTheme, elevatedButtonTheme, cardTheme, inputDecorationTheme, dll.) mengkustomisasi tampilan komponen spesifik secara global.
  • Theme widget memungkinkan override tema lokal — gunakan Theme.of(context).copyWith() agar mewarisi dari parent tema.
  • ThemeExtension menambahkan warna atau nilai kustom ke sistem tema — sangat berguna untuk warna semantic (sukses, peringatan) yang tidak ada di ColorScheme standar.
  • Organisasikan tema ke file terpisah (app_theme.dart) dengan static getter light dan dark untuk keterbacaan dan maintainability yang lebih baik.

← Sebelumnya: Navigation   Berikutnya: Anti-Pattern →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact