Flutter'da Material 3 Tema Sistemi: Koyu/Açık Tema ve Dynamic Color
Material 3 (Material You), Google'ın en yeni tasarım dilidir. Flutter 3.x ile birlikte tam destek kazanmıştır. Renk sistemi, tipografi ve bileşen stili tutarlı bir şekilde tanımlanır. Bu yazıda kapsamlı ThemeData yapılandırmasını, tema geçişini ve Android 12+ Dynamic Color özelliğini ele alacağız.
ColorScheme ile Renk Paleti
// Tek bir seed renkten tüm palet otomatik üretilir
class AppTheme {
static const _seedColor = Color(0xFF6750A4); // ana renk
static ThemeData get light => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.light,
),
// Tipografi
textTheme: _buildTextTheme(Brightness.light),
// Bileşen temaları
appBarTheme: const AppBarTheme(centerTitle: false, elevation: 0),
cardTheme: CardTheme(
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
static ThemeData get dark => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.dark,
),
textTheme: _buildTextTheme(Brightness.dark),
appBarTheme: const AppBarTheme(centerTitle: false, elevation: 0),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 52),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
),
),
);
static TextTheme _buildTextTheme(Brightness brightness) {
final base = brightness == Brightness.light
? ThemeData.light().textTheme
: ThemeData.dark().textTheme;
return base.apply(fontFamily: 'Inter');
}
}
Tema Geçişi: ThemeCubit
@freezed
class ThemeState with _$ThemeState {
const factory ThemeState({
@Default(ThemeMode.system) ThemeMode mode,
Color? dynamicSeedColor,
}) = _ThemeState;
}
@injectable
class ThemeCubit extends Cubit<ThemeState> {
static const _key = 'theme_mode';
final SharedPreferences _prefs;
ThemeCubit(this._prefs) : super(ThemeState(
mode: _loadMode(_prefs),
));
static ThemeMode _loadMode(SharedPreferences prefs) {
return switch (prefs.getString(_key)) {
'light' => ThemeMode.light,
'dark' => ThemeMode.dark,
_ => ThemeMode.system,
};
}
Future<void> setMode(ThemeMode mode) async {
await _prefs.setString(_key, mode.name);
emit(state.copyWith(mode: mode));
}
void setDynamicColor(Color? color) =>
emit(state.copyWith(dynamicSeedColor: color));
ThemeData get lightTheme => state.dynamicSeedColor != null
? AppTheme.fromSeed(state.dynamicSeedColor!, Brightness.light)
: AppTheme.light;
ThemeData get darkTheme => state.dynamicSeedColor != null
? AppTheme.fromSeed(state.dynamicSeedColor!, Brightness.dark)
: AppTheme.dark;
}
// MaterialApp'ta kullanım
BlocBuilder<ThemeCubit, ThemeState>(
builder: (context, state) {
final cubit = context.read<ThemeCubit>();
return MaterialApp.router(
theme: cubit.lightTheme,
darkTheme: cubit.darkTheme,
themeMode: state.mode,
routerConfig: appRouter,
);
},
)
Dynamic Color: Android 12+ Material You
// pubspec: dynamic_color: ^1.7.0
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
// Android 12+ wallpaper renkleri
ColorScheme lightScheme;
ColorScheme darkScheme;
if (lightDynamic != null && darkDynamic != null) {
// Android 12+ cihazlarda duvar kağıdı renkleri kullan
lightScheme = lightDynamic.harmonized();
darkScheme = darkDynamic.harmonized();
} else {
// Fallback: seed renk
lightScheme = ColorScheme.fromSeed(
seedColor: AppTheme.seedColor, brightness: Brightness.light);
darkScheme = ColorScheme.fromSeed(
seedColor: AppTheme.seedColor, brightness: Brightness.dark);
}
return BlocBuilder<ThemeCubit, ThemeState>(
builder: (_, state) => MaterialApp.router(
theme: AppTheme.fromScheme(lightScheme),
darkTheme: AppTheme.fromScheme(darkScheme),
themeMode: state.mode,
routerConfig: appRouter,
),
);
},
);
}
}
ThemeExtension: Özel Tema Özellikleri
// Material 3'e dahil olmayan özel renk/stil değerleri için
@immutable
class AppColors extends ThemeExtension<AppColors> {
final Color success;
final Color warning;
final Color info;
final Color cardGradientStart;
final Color cardGradientEnd;
const AppColors({
required this.success,
required this.warning,
required this.info,
required this.cardGradientStart,
required this.cardGradientEnd,
});
static const light = AppColors(
success: Color(0xFF2E7D32),
warning: Color(0xFFE65100),
info: Color(0xFF01579B),
cardGradientStart: Color(0xFF6750A4),
cardGradientEnd: Color(0xFF9C27B0),
);
static const dark = AppColors(
success: Color(0xFF81C784),
warning: Color(0xFFFFB74D),
info: Color(0xFF4FC3F7),
cardGradientStart: Color(0xFF9C27B0),
cardGradientEnd: Color(0xFF6750A4),
);
@override
AppColors copyWith({Color? success, Color? warning, Color? info,
Color? cardGradientStart, Color? cardGradientEnd}) {
return AppColors(
success: success ?? this.success,
warning: warning ?? this.warning,
info: info ?? this.info,
cardGradientStart: cardGradientStart ?? this.cardGradientStart,
cardGradientEnd: cardGradientEnd ?? this.cardGradientEnd,
);
}
@override
AppColors lerp(AppColors? other, double t) {
if (other == null) return this;
return AppColors(
success: Color.lerp(success, other.success, t)!,
warning: Color.lerp(warning, other.warning, t)!,
info: Color.lerp(info, other.info, t)!,
cardGradientStart: Color.lerp(cardGradientStart, other.cardGradientStart, t)!,
cardGradientEnd: Color.lerp(cardGradientEnd, other.cardGradientEnd, t)!,
);
}
}
// ThemeData'ya ekleme
ThemeData get light => ThemeData(
useMaterial3: true,
extensions: const [AppColors.light],
// ...
);
// Widget'ta kullanım
final appColors = Theme.of(context).extension<AppColors>()!;
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [appColors.cardGradientStart, appColors.cardGradientEnd],
),
),
)
// Tema değişiminde smooth geçiş
AnimatedTheme(
data: ThemeData(/* ... */),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
child: child,
)
Material 3 ile ColorScheme.fromSeed, tek bir renk tanımından eksiksiz bir palet üretir. ThemeExtension, standart Material temasına uymayan özel renk ve stilleri tip güvenli şekilde yönetmek için mükemmel bir mekanizmadır. Dynamic Color ile Android kullanıcılarına kişiselleştirilmiş bir deneyim sunabilirsiniz.