Creating Dynamic Themes in Flutter with Provider

In Flutter, creating a visually appealing and user-friendly application often involves providing options for users to customize the app’s theme. Dynamic theming allows users to switch between light and dark modes or choose from a range of color schemes, enhancing their overall experience. This blog post will guide you through implementing dynamic themes in Flutter using the Provider package.

What is Dynamic Theming?

Dynamic theming refers to the ability of an application to change its visual appearance (themes, colors, styles) in response to user preferences or system settings, such as dark mode.

Why Use Dynamic Theming?

  • Improved User Experience: Allows users to personalize the app to their preferences.
  • Accessibility: Supports users with visual impairments by offering high-contrast themes.
  • Aesthetic Appeal: Keeps the app modern and engaging.

Implementation Overview

We will use the Provider package to manage and provide the app’s theme data. Here’s a step-by-step guide to setting up dynamic themes:

Step 1: Add Dependencies

First, add the provider package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  provider: ^6.0.0

Run flutter pub get to install the dependencies.

Step 2: Create a ThemeProvider Class

Create a ThemeProvider class that extends ChangeNotifier to manage and notify theme changes.

import 'package:flutter/material.dart';

class ThemeProvider extends ChangeNotifier {
  ThemeMode themeMode;

  ThemeProvider({this.themeMode = ThemeMode.system});

  bool get isDarkMode => themeMode == ThemeMode.dark;

  void toggleTheme(bool isOn) {
    themeMode = isOn ? ThemeMode.dark : ThemeMode.light;
    notifyListeners();
  }
}

Step 3: Define ThemeData

Create your ThemeData for both light and dark modes.

import 'package:flutter/material.dart';

class AppThemes {
  static final light = ThemeData(
    primarySwatch: Colors.blue,
    brightness: Brightness.light,
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.white,
      foregroundColor: Colors.black,
    ),
  );

  static final dark = ThemeData(
    primarySwatch: Colors.blue,
    brightness: Brightness.dark,
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.grey,
      foregroundColor: Colors.white,
    ),
  );
}

Step 4: Wrap Your App with ChangeNotifierProvider

Wrap your app’s MaterialApp widget with ChangeNotifierProvider to provide the ThemeProvider.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart'; // Import the ThemeProvider
import 'app_themes.dart'; // Import your AppThemes

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of(context);
    return MaterialApp(
      title: 'Dynamic Theme Demo',
      themeMode: themeProvider.themeMode,
      theme: AppThemes.light,
      darkTheme: AppThemes.dark,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Theme Demo'),
      ),
      body: Center(
        child: ThemeSwitcher(),
      ),
    );
  }
}

Step 5: Create a Theme Switcher Widget

Create a widget to toggle between light and dark themes using a Switch widget.

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart'; // Import your ThemeProvider

class ThemeSwitcher extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of(context);
    return Switch(
      value: themeProvider.isDarkMode,
      onChanged: (value) {
        final provider = Provider.of(context, listen: false);
        provider.toggleTheme(value);
      },
    );
  }
}

Complete Example

Here’s the complete example combining all the steps above:

main.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart';
import 'app_themes.dart';
import 'theme_switcher.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => ThemeProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of(context);
    return MaterialApp(
      title: 'Dynamic Theme Demo',
      themeMode: themeProvider.themeMode,
      theme: AppThemes.light,
      darkTheme: AppThemes.dark,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Dynamic Theme Demo'),
      ),
      body: Center(
        child: ThemeSwitcher(),
      ),
    );
  }
}

theme_provider.dart:

import 'package:flutter/material.dart';

class ThemeProvider extends ChangeNotifier {
  ThemeMode themeMode;

  ThemeProvider({this.themeMode = ThemeMode.system});

  bool get isDarkMode => themeMode == ThemeMode.dark;

  void toggleTheme(bool isOn) {
    themeMode = isOn ? ThemeMode.dark : ThemeMode.light;
    notifyListeners();
  }
}

app_themes.dart:

import 'package:flutter/material.dart';

class AppThemes {
  static final light = ThemeData(
    primarySwatch: Colors.blue,
    brightness: Brightness.light,
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.white,
      foregroundColor: Colors.black,
    ),
  );

  static final dark = ThemeData(
    primarySwatch: Colors.blue,
    brightness: Brightness.dark,
    appBarTheme: const AppBarTheme(
      backgroundColor: Colors.grey,
      foregroundColor: Colors.white,
    ),
  );
}

theme_switcher.dart:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'theme_provider.dart';

class ThemeSwitcher extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final themeProvider = Provider.of(context);
    return Switch(
      value: themeProvider.isDarkMode,
      onChanged: (value) {
        final provider = Provider.of(context, listen: false);
        provider.toggleTheme(value);
      },
    );
  }
}

Customization and Advanced Usage

  • Theme Color Palette: Define and switch between different color palettes beyond light and dark modes.
  • Persistence: Store the user’s theme preference locally using shared_preferences or a similar package so that the app remembers the preference across sessions.
  • System Theme: Automatically switch to dark mode when the system-wide setting changes (using ThemeMode.system).

Conclusion

Implementing dynamic themes in Flutter using the Provider package provides a clean and efficient way to allow users to customize the appearance of your app. By managing the theme data in a ChangeNotifier and providing it to your app’s widgets, you can create a flexible and user-friendly theming system that enhances the overall user experience.