Ora

How do I change the theme color on my Flutter app?

Published in Flutter Theming 6 mins read

To change the theme color in your Flutter app, you primarily work with the ThemeData object, which defines the visual properties of your MaterialApp. This involves defining color schemes, typography, and other visual attributes for both light and dark modes, then using a state management solution like Riverpod to dynamically switch between them.

Understanding Flutter Themes

Flutter's theming system allows you to define a consistent look and feel across your entire application. The core of this system is the ThemeData class, which holds properties like colorScheme, textTheme, appBarTheme, and more.

The ColorScheme is particularly important as it dictates the primary, secondary, background, surface, and error colors, among others. Widgets automatically pick up colors from the nearest ThemeData in the widget tree.

Step-by-Step Guide to Changing Theme Colors

Here's how to implement dynamic theme color changes in your Flutter app:

1. Defining Your Themes (Light and Dark)

Start by creating two distinct ThemeData objects, one for your light theme and one for your dark theme. The ColorScheme factory constructor (ColorScheme.fromSeed) is an excellent starting point for generating a coherent set of colors based on a single seed color.

import 'package:flutter/material.dart';

// Define your light theme
final ThemeData lightTheme = ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.blue, // Your primary seed color for light mode
    brightness: Brightness.light,
  ),
  useMaterial3: true,
  // You can customize other properties here, e.g.,
  appBarTheme: const AppBarTheme(
    backgroundColor: Colors.blueAccent,
    foregroundColor: Colors.white,
  ),
  // ... more customizations
);

// Define your dark theme
final ThemeData darkTheme = ThemeData(
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.indigo, // Your primary seed color for dark mode
    brightness: Brightness.dark,
  ),
  useMaterial3: true,
  // Customize for dark mode
  appBarTheme: const AppBarTheme(
    backgroundColor: Colors.grey[900],
    foregroundColor: Colors.white,
  ),
  // ... more customizations
);

2. Managing Theme State with Riverpod

To allow users to switch themes dynamically, you need a way to manage the current theme state. Riverpod is a powerful and efficient state management library perfect for this. You'll create a Notifier (or StateNotifier) to hold the current ThemeMode (light, dark, or system).

First, add flutter_riverpod to your pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.5.1 # Use the latest version

Then, create a theme provider:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// Enum to represent theme preferences
enum AppThemeMode {
  light,
  dark,
  system,
}

// Notifier to manage the theme mode
class ThemeNotifier extends StateNotifier<AppThemeMode> {
  ThemeNotifier() : super(AppThemeMode.system); // Default to system theme

  void toggleTheme() {
    state = state == AppThemeMode.light ? AppThemeMode.dark : AppThemeMode.light;
  }

  void setThemeMode(AppThemeMode mode) {
    state = mode;
  }
}

// Provider for the ThemeNotifier
final themeProvider = StateNotifierProvider<ThemeNotifier, AppThemeMode>((ref) {
  return ThemeNotifier();
});

3. Applying Themes to Your App

Now, wrap your MaterialApp with a ProviderScope (required by Riverpod) and use the theme, darkTheme, and themeMode properties of MaterialApp to apply your defined themes based on the state managed by Riverpod.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Import your theme_manager.dart and theme_definitions.dart files
// For this example, let's assume theming.dart contains both ThemeNotifier and lightTheme/darkTheme

void main() {
  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AppThemeMode currentThemeMode = ref.watch(themeProvider);

    return MaterialApp(
      title: 'Flutter Theme Demo',
      theme: lightTheme, // Your defined light theme
      darkTheme: darkTheme, // Your defined dark theme
      themeMode: switch (currentThemeMode) {
        AppThemeMode.light => ThemeMode.light,
        AppThemeMode.dark => ThemeMode.dark,
        AppThemeMode.system => ThemeMode.system,
      },
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final currentThemeMode = ref.watch(themeProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Theme Changer'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Theme: ${currentThemeMode.name}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                ref.read(themeProvider.notifier).toggleTheme();
              },
              child: const Text('Toggle Theme'),
            ),
            const SizedBox(height: 10),
            DropdownButton<AppThemeMode>(
              value: currentThemeMode,
              onChanged: (AppThemeMode? newValue) {
                if (newValue != null) {
                  ref.read(themeProvider.notifier).setThemeMode(newValue);
                }
              },
              items: AppThemeMode.values
                  .map<DropdownMenuItem<AppThemeMode>>((AppThemeMode value) {
                return DropdownMenuItem<AppThemeMode>(
                  value: value,
                  child: Text(value.name),
                );
              }).toList(),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Example of using a color from the current theme's colorScheme
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Primary color: ${Theme.of(context).colorScheme.primary}'),
              backgroundColor: Theme.of(context).colorScheme.primary,
            ),
          );
        },
        child: const Icon(Icons.color_lens),
      ),
    );
  }
}

4. Adding Custom Colors Beyond ColorScheme

Sometimes, you need colors or other theme properties that don't fit neatly into ColorScheme. Flutter's ThemeExtension is the recommended way to add custom properties to your theme.

  1. Define a Custom Theme Extension: Create a class that extends ThemeExtension<YourExtensionClass>.

    import 'package:flutter/material.dart';
    
    @immutable
    class CustomAppColors extends ThemeExtension<CustomAppColors> {
      const CustomAppColors({
        required this.warningColor,
        required this.successColor,
      });
    
      final Color? warningColor;
      final Color? successColor;
    
      @override
      CustomAppColors copyWith({Color? warningColor, Color? successColor}) {
        return CustomAppColors(
          warningColor: warningColor ?? this.warningColor,
          successColor: successColor ?? this.successColor,
        );
      }
    
      @override
      CustomAppColors lerp(ThemeExtension<CustomAppColors>? other, double t) {
        if (other is! CustomAppColors) {
          return this;
        }
        return CustomAppColors(
          warningColor: Color.lerp(warningColor, other.warningColor, t),
          successColor: Color.lerp(successColor, other.successColor, t),
        );
      }
    
      // Helper to easily access custom colors
      static CustomAppColors of(BuildContext context) =>
          Theme.of(context).extension<CustomAppColors>()!;
    }
  2. Add the Extension to Your ThemeData: Include an instance of your custom extension in the extensions list of your ThemeData objects.

    // In your theme_definitions.dart
    // ...
    final ThemeData lightTheme = ThemeData(
      // ... existing properties
      extensions: const <ThemeExtension<dynamic>>[
        CustomAppColors(
          warningColor: Colors.amber,
          successColor: Colors.green,
        ),
      ],
    );
    
    final ThemeData darkTheme = ThemeData(
      // ... existing properties
      extensions: const <ThemeExtension<dynamic>>[
        CustomAppColors(
          warningColor: Colors.orange,
          successColor: Colors.lightGreen,
        ),
      ],
    );
  3. Access Custom Colors: Retrieve your custom colors from the ThemeData using Theme.of(context).extension<CustomAppColors>().

    // In any widget
    // ...
    Text(
      'Warning Message',
      style: TextStyle(color: CustomAppColors.of(context).warningColor),
    ),
    // Or
    // Text(
    //   'Success Message',
    //   style: TextStyle(color: Theme.of(context).extension<CustomAppColors>()?.successColor),
    // ),

Key ThemeData Properties for Color Control

Property Description Example Usage
colorScheme Defines a set of harmonized colors for the UI. ColorScheme.fromSeed(seedColor: Colors.purple)
primaryColor The background color of the primary parts of the app (obsoleted by ColorScheme.primary). Colors.blue (Prefer colorScheme.primary)
accentColor The color for accent widgets (obsoleted by ColorScheme.secondary). Colors.pink (Prefer colorScheme.secondary)
scaffoldBackgroundColor The background color of the Scaffold. Colors.grey[200]
appBarTheme Defines the default appearance of AppBars. AppBarTheme(backgroundColor: Colors.deepPurple)
buttonTheme Defines the default appearance of buttons (obsoleted by elevatedButtonTheme, etc.). ButtonThemeData(buttonColor: Colors.green)
cardColor The color of Card widgets. Colors.white
textTheme Defines the default text styles for different text types. textTheme: TextTheme(bodyMedium: TextStyle(color: Colors.black))
brightness The overall brightness of the theme (light or dark). Brightness.light
extensions A list of custom ThemeExtensions to add custom properties. extensions: <ThemeExtension<dynamic>>[CustomAppColors(...)

Best Practices for Theme Management

  • Centralize Theme Definitions: Keep your ThemeData definitions in a separate file (e.g., theme_definitions.dart) for better organization.
  • Use ColorScheme.fromSeed: This constructor helps create a harmonious color palette, reducing the need to manually define every color.
  • Accessibility: Ensure sufficient contrast between text and background colors for all theme modes. Tools like WebAIM Contrast Checker can help.
  • Responsive Theming: Consider how your theme looks on various screen sizes and orientations.
  • Testing: Test your app thoroughly in both light and dark modes to ensure all widgets adapt correctly.

By following these steps, you can effectively manage and change theme colors in your Flutter application, offering a dynamic and personalized user experience.