Implementing Dark Mode in Flutter Apps
•6 min read
Dark mode is a popular feature in modern apps. This article will teach you how to implement dark mode in Flutter, including theme switching and best practices.
Understanding ThemeData
Flutter's ThemeData
class is the foundation for implementing dark mode:
MaterialApp( theme: ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, // Light theme specific styles ), darkTheme: ThemeData( brightness: Brightness.dark, primarySwatch: Colors.blue, // Dark theme specific styles ), )
Theme Provider Implementation
Create a theme provider to manage theme state:
class ThemeProvider extends ChangeNotifier { bool _isDarkMode = false; bool get isDarkMode => _isDarkMode; void toggleTheme() { _isDarkMode = !_isDarkMode; notifyListeners(); } }
Theme Configuration
Define your light and dark themes:
class AppTheme { static ThemeData get lightTheme { return ThemeData( brightness: Brightness.light, primarySwatch: Colors.blue, scaffoldBackgroundColor: Colors.white, appBarTheme: AppBarTheme( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), textTheme: TextTheme( bodyText1: TextStyle(color: Colors.black87), bodyText2: TextStyle(color: Colors.black87), ), ); } static ThemeData get darkTheme { return ThemeData( brightness: Brightness.dark, primarySwatch: Colors.blue, scaffoldBackgroundColor: Colors.grey[900], appBarTheme: AppBarTheme( backgroundColor: Colors.grey[800], foregroundColor: Colors.white, ), textTheme: TextTheme( bodyText1: TextStyle(color: Colors.white70), bodyText2: TextStyle(color: Colors.white70), ), ); } }
Theme Switching Implementation
Implement theme switching in your app:
void main() { runApp( ChangeNotifierProvider( create: (_) => ThemeProvider(), child: const MyApp(), ), ); } class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Consumer<ThemeProvider>( builder: (context, themeProvider, child) { return MaterialApp( title: 'Flutter Dark Mode Demo', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: themeProvider.isDarkMode ? ThemeMode.dark : ThemeMode.light, home: const HomePage(), ); }, ); } }
Theme Switching UI
Add a theme toggle button:
class ThemeToggleButton extends StatelessWidget { const ThemeToggleButton({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Consumer<ThemeProvider>( builder: (context, themeProvider, child) { return IconButton( icon: Icon( themeProvider.isDarkMode ? Icons.light_mode : Icons.dark_mode, ), onPressed: themeProvider.toggleTheme, ); }, ); } }
Persisting Theme Preference
Save theme preference using shared_preferences
:
class ThemeProvider extends ChangeNotifier { static const String _themeKey = 'isDarkMode'; bool _isDarkMode = false; bool get isDarkMode => _isDarkMode; ThemeProvider() { _loadTheme(); } Future<void> _loadTheme() async { final prefs = await SharedPreferences.getInstance(); _isDarkMode = prefs.getBool(_themeKey) ?? false; notifyListeners(); } Future<void> toggleTheme() async { _isDarkMode = !_isDarkMode; final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_themeKey, _isDarkMode); notifyListeners(); } }
Custom Theme Extensions
Create custom theme extensions for app-specific colors:
class AppColors extends ThemeExtension<AppColors> { final Color success; final Color warning; final Color error; const AppColors({ required this.success, required this.warning, required this.error, }); @override ThemeExtension<AppColors> copyWith({ Color? success, Color? warning, Color? error, }) { return AppColors( success: success ?? this.success, warning: warning ?? this.warning, error: error ?? this.error, ); } @override ThemeExtension<AppColors> lerp(ThemeExtension<AppColors>? other, double t) { if (other is! AppColors) return this; return AppColors( success: Color.lerp(success, other.success, t)!, warning: Color.lerp(warning, other.warning, t)!, error: Color.lerp(error, other.error, t)!, ); } }
Using Custom Theme Extensions
// In your theme configuration static ThemeData get lightTheme { return ThemeData( // ... other theme properties extensions: [ AppColors( success: Colors.green, warning: Colors.orange, error: Colors.red, ), ], ); } // Usage in widgets final colors = Theme.of(context).extension<AppColors>()!; Container( color: colors.success, child: Text('Success'), )
Best Practices
- Use Semantic Colors: Define colors based on their purpose
- Maintain Contrast: Ensure text remains readable in both themes
- Test Thoroughly: Verify appearance on different devices
- Consider Accessibility: Support system theme preferences
- Optimize Performance: Minimize theme-related rebuilds
Testing Theme Implementation
void main() { testWidgets('Theme Toggle Test', (WidgetTester tester) async { await tester.pumpWidget( ChangeNotifierProvider( create: (_) => ThemeProvider(), child: const MyApp(), ), ); // Verify initial theme expect(Theme.of(tester.element(find.byType(MyApp))).brightness, equals(Brightness.light)); // Toggle theme await tester.tap(find.byIcon(Icons.dark_mode)); await tester.pump(); // Verify theme change expect(Theme.of(tester.element(find.byType(MyApp))).brightness, equals(Brightness.dark)); }); }
Conclusion
Implementing dark mode in Flutter apps involves:
- Understanding ThemeData and its properties
- Creating a theme provider for state management
- Defining light and dark theme configurations
- Implementing theme switching functionality
- Persisting theme preferences
- Using custom theme extensions
- Following best practices for accessibility and performance
By following these guidelines, you can create a polished dark mode implementation that enhances your app's user experience.
Stay tuned for step-by-step tutorials and examples!