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 #
ThemeDataadalah kontainer visual aplikasi — definisikan diMaterialApp.themedanMaterialApp.darkTheme. Material 3 adalah default sejak Flutter 3.16.ColorScheme.fromSeedmenghasilkan color scheme harmonis dari satu seed color secara otomatis — tersedia versi light dan dark.ThemeModemengontrol kapan dark theme digunakan:system(ikuti OS),light, ataudark. Toggle programatik menggunakansetState.TextThememendefinisikan 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.Themewidget memungkinkan override tema lokal — gunakanTheme.of(context).copyWith()agar mewarisi dari parent tema.ThemeExtensionmenambahkan warna atau nilai kustom ke sistem tema — sangat berguna untuk warna semantic (sukses, peringatan) yang tidak ada diColorSchemestandar.- Organisasikan tema ke file terpisah (
app_theme.dart) dengan static getterlightdandarkuntuk keterbacaan dan maintainability yang lebih baik.