Implementing Dark and Light Theme Modes in Flutter Applications: A Complete Solution Based on Provider and Persistent Storage

Nov 26, 2025 · Programming · 10 views · 7.8

Keywords: Flutter | Theme Switching | Provider | Persistent Storage | Dark Mode

Abstract: This article provides a comprehensive technical solution for implementing dark and light theme modes in Flutter applications. By combining Provider state management with SharedPreferences persistent storage, it achieves real-time theme switching and long-term user preference preservation. The article covers theme detection, state management, and interface updates with complete code examples and implementation logic, helping developers build modern Flutter applications with theme switching capabilities.

Core Architecture of Theme Mode Implementation

Implementing theme switching functionality in Flutter applications requires comprehensive consideration of state management, persistent storage, and interface updates. The Provider-based state management solution effectively addresses the propagation of theme data throughout the widget tree, while SharedPreferences ensures long-term preservation of user theme preferences.

Implementation of Storage Management Layer

Persistent storage forms the foundation of theme mode implementation, achieved through the SharedPreferences package for local data storage. The storage manager needs to handle different types of data storage requirements:

import 'package:shared_preferences/shared_preferences.dart';

class StorageManager {
  static void saveData(String key, dynamic value) async {
    final prefs = await SharedPreferences.getInstance();
    if (value is int) {
      prefs.setInt(key, value);
    } else if (value is String) {
      prefs.setString(key, value);
    } else if (value is bool) {
      prefs.setBool(key, value);
    } else {
      print("Invalid Type");
    }
  }

  static Future<dynamic> readData(String key) async {
    final prefs = await SharedPreferences.getInstance();
    dynamic obj = prefs.get(key);
    return obj;
  }

  static Future<bool> deleteData(String key) async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.remove(key);
  }
}

Design and Implementation of Theme Manager

The theme manager, acting as a ChangeNotifier, is responsible for managing the application's theme state and providing theme switching functionality. Its core lies in defining specific configurations for dark and light themes, and driving interface updates through notification mechanisms:

import 'package:flutter/material.dart';
import '../services/storage_manager.dart';

class ThemeNotifier with ChangeNotifier {
  final darkTheme = ThemeData(
    primarySwatch: Colors.grey,
    primaryColor: Colors.black,
    brightness: Brightness.dark,
    backgroundColor: const Color(0xFF212121),
    accentColor: Colors.white,
    accentIconTheme: IconThemeData(color: Colors.black),
    dividerColor: Colors.black12,
  );

  final lightTheme = ThemeData(
    primarySwatch: Colors.grey,
    primaryColor: Colors.white,
    brightness: Brightness.light,
    backgroundColor: const Color(0xFFE5E5E5),
    accentColor: Colors.black,
    accentIconTheme: IconThemeData(color: Colors.white),
    dividerColor: Colors.white54,
  );

  ThemeData _themeData;
  ThemeData getTheme() => _themeData;

  ThemeNotifier() {
    StorageManager.readData('themeMode').then((value) {
      print('value read from storage: ' + value.toString());
      var themeMode = value ?? 'light';
      if (themeMode == 'light') {
        _themeData = lightTheme;
      } else {
        print('setting dark theme');
        _themeData = darkTheme;
      }
      notifyListeners();
    });
  }

  void setDarkMode() async {
    _themeData = darkTheme;
    StorageManager.saveData('themeMode', 'dark');
    notifyListeners();
  }

  void setLightMode() async {
    _themeData = lightTheme;
    StorageManager.saveData('themeMode', 'light');
    notifyListeners();
  }
}

Application Entry and Provider Integration

At the application entry point, the theme manager is injected into the widget tree through ChangeNotifierProvider, ensuring that the entire application can access the theme state:

void main() {
  return runApp(ChangeNotifierProvider<ThemeNotifier>(
    create: (_) => new ThemeNotifier(),
    child: MyApp(),
  ));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<ThemeNotifier>(
      builder: (context, theme, _) => MaterialApp(
        theme: theme.getTheme(),
        home: Scaffold(
          appBar: AppBar(
            title: Text('Hybrid Theme'),
          ),
          body: Row(
            children: [
              Container(
                child: FlatButton(
                  onPressed: () => {
                    print('Set Light Theme');
                    theme.setLightMode();
                  },
                  child: Text('Set Light Theme'),
                ),
              ),
              Container(
                child: FlatButton(
                  onPressed: () => {
                    print('Set Dark theme');
                    theme.setDarkMode();
                  },
                  child: Text('Set Dark theme'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

User Interaction Design for Theme Switching

Theme switching operations are triggered through button components. When users click the corresponding buttons, the theme manager's corresponding methods are called, triggering state updates and interface reconstruction:

FlatButton(
  onPressed: () => {
    print('Set Light Theme');
    theme.setLightMode();
  },
  child: Text('Set Light Theme'),
)

Supplementary Solution for System Theme Detection

In addition to in-app theme switching, system theme detection functionality can be integrated. By observing system brightness changes through WidgetsBindingObserver, synchronization with the system theme can be achieved:

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  Brightness? _brightness;

  @override
  void initState() {
    WidgetsBinding.instance?.addObserver(this);
    _brightness = WidgetsBinding.instance?.window.platformBrightness;
    super.initState();
  }

  @override
  void didChangePlatformBrightness() {
    if (mounted) {
      setState(() {
        _brightness = WidgetsBinding.instance?.window.platformBrightness;
      });
    }
    super.didChangePlatformBrightness();
  }
}

Extensibility of Custom Theme Configuration

In practical development, theme configurations can be extended according to specific requirements. Beyond basic color settings, visual elements such as fonts, shapes, and animations can be defined to create unique brand visual experiences:

final customDarkTheme = ThemeData(
  brightness: Brightness.dark,
  primaryColor: Colors.deepPurple,
  accentColor: Colors.amber,
  fontFamily: 'Roboto',
  textTheme: TextTheme(
    headline1: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
    bodyText1: TextStyle(fontSize: 16, color: Colors.white70),
  ),
);

Performance Optimization and Best Practices

When implementing theme switching functionality, attention must be paid to performance optimization. Avoid rebuilding the entire widget tree during theme switching by employing proper component design and state management to ensure smooth application operation. Additionally, provide appropriate loading states and error handling to enhance user experience.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.