Implement Dark Mode in Flutter using Riverpod

Dynamically change your flutter app theme.

Implement Dark Mode in Flutter using Riverpod

Hey Gang,

In this article we will see how to add custom theme data for your flutter app and switch between light and dark themes using Riverpod.

We have seen quite a lot of apps supporting dark mode feature.

The reason being

  • Dark mode can reduce eye strain in low-light conditions.
  • Or Just going with app trends (Hey!!! everyone is doing it, why not us ? Let's have a dark mode feature too.)

Just Kidding...

Whatever may be the reason... Let's get this implemented.

Flutter by default supports dark-mode. This works by toggling dark mode button available on your phone.

To achieve this just add the following code in your main.dart file.

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(
    MyApp(),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    return MaterialApp(
      darkTheme: ThemeData.dark(),
      theme: ThemeData.light(),
      home: HomePage(),
    );
  }
}

Now let's have the dark mode toggling feature implemented within the app.

Following is preview of our final app.

theme_ninja_12.gif

To get started create a new flutter project and name it as theme_ninja. (I use Visual Studio Code editor for coding. Feel free to use any editor of your choice.)

Now go to the pubspec.yaml file and add the following dependencies

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^0.13.1
  shared_preferences: ^2.0.4

The versions may differ with time. Please do add the latest ones.

For installing the package : flutter pub get Or pub get via terminal.

shared_preferences may seem unnecessary since we just want to switch between dark and light theme modes.

But as a user one would want the mode of the app to be retained even after the app is closed (terminate the app, kill from background...). Only way to achieve this is by storing the state using Shared Preferences.

We will be creating three new files here.

  1. app_theme_provider.dart (for our app themes (color, font etc) and theme notifiers.)
  2. shared_utility.dart (for saving our app state across Platforms (iOS & Android).)
  3. home_page_widget.dart (Simple UI comprising of a Button to toggle state between dark and light mode.)

Once you have created the above three files.

Open app_theme_provider.dart file and replace the file content with following code.

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

/* AppTheme */

final appThemeProvider = Provider<AppTheme>((ref) {
  return AppTheme();
});

class AppTheme {
  //Modify to add more colors here
  static ThemeData _lightThemeData = ThemeData(
    primaryColor: Colors.blueGrey[600],
    accentColor: Colors.blueGrey[100],
    scaffoldBackgroundColor: Colors.blueGrey[100],
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        textStyle: TextStyle(
          color: Colors.white,
        ),
        primary: Colors.red,
      ),
    ),
  );

  static ThemeData _darkThemeData = ThemeData(
    primaryColor: Colors.blue,
    accentColor: Colors.black12,
    scaffoldBackgroundColor: Colors.black12,
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        textStyle: TextStyle(
          color: Colors.white,
        ),
        primary: Colors.blue,
      ),
    ),
  );

  ThemeData getAppThemedata(BuildContext context, bool isDarkModeEnabled) {
    return isDarkModeEnabled ? _darkThemeData : _lightThemeData;
  }
}

/* AppTheme Notifier */

final appThemeStateProvider = StateNotifierProvider<AppThemeNotifier>((ref) {
  final _isDarkModeEnabled =
      ref.read(sharedUtilityProvider).isDarkModeEnabled();
  return AppThemeNotifier(_isDarkModeEnabled);
});

class AppThemeNotifier extends StateNotifier<bool> {
  AppThemeNotifier(this.defaultDarkModeValue) : super(defaultDarkModeValue);

  final bool defaultDarkModeValue;

  toggleAppTheme(BuildContext context) {
    final _isDarkModeEnabled =
        context.read(sharedUtilityProvider).isDarkModeEnabled();
    final _toggleValue = !_isDarkModeEnabled;

    context
        .read(
          sharedUtilityProvider,
        )
        .setDarkModeEnabled(_toggleValue)
        .whenComplete(
          () => {
            state = _toggleValue,
          },
        );
  }
}

As we see above, We have defined two classed one AppTheme and other AppThemeNotifier.

The AppTheme class is pretty straightforward , We have defined two themes one for Dark Mode and other for Light Mode. Feel free to add more colors like for Text, Icons , NavigationBars etc. (You may refer for more attributes of ThemeData here)

We have defined appThemeProvider a basic Provider to access members of AppTheme class.

AppThemeNotifier is a StateNotifier. We retain a particular state within app here. (You may refer here)

AppThemeNotifier class notifies the state change (like is dark-mode enabled or disabled) with help of StateNotifierProvider.

We have defined a StateNotifierProvider appThemeStateProvider, Any widget observing or watching appThemeStateProvider will be notified as soon as the state is changed. Here since AppThemeNotifier is of type bool, Any toggling between true or false will cause StateNotifier to be triggered.

Now open shared_utility.dart file and replace the file content with following code.

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';


final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
  //The return type Future<SharedPreferences> isn't a 'SharedPreferences', as required by the closure's context
  //Code Reference: https://codewithandrea.com/videos/flutter-state-management-riverpod
  throw UnimplementedError();
});

final sharedUtilityProvider = Provider<SharedUtility>((ref) {
  final _sharedPrefs = ref.watch(sharedPreferencesProvider);
  return SharedUtility(sharedPreferences: _sharedPrefs);
});

class SharedUtility {
  SharedUtility({
    this.sharedPreferences,
  });

  final SharedPreferences sharedPreferences;

  bool isDarkModeEnabled() {
    return sharedPreferences.getBool('isDarkModeEnabled') ?? false;
  }

  Future<bool> setDarkModeEnabled(bool value) async {
    return await sharedPreferences.setBool('isDarkModeEnabled', value);
  }
}

We have two methods defined in SharedUtility, These are just helper methods to save and retrieve the values from Shared Preferences.

We have also defined sharedUtilityProvider and sharedPreferencesProvider a basic Provider for access.

Now open home_page_widget.dart file and replace the file content with following code.

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

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final _appThemeStateProvider = context.read(appThemeStateProvider);
    return Scaffold(
      appBar: AppBar(
        title: Text(
          'Theme Ninja',
        ),
      ),
      body: Container(
        child: Center(
          child: ElevatedButton(
            child: Text('Toggle App Themme'),
            onPressed: () => _appThemeStateProvider.toggleAppTheme(context),
          ),
        ),
      ),
    );
  }
}

In the HomePage we have ElevatedButton which calls toggleAppTheme method from appThemeStateProvider .

Here we initialise _appThemeStateProvider instance via read method of Provider.

Now open your main.dart file and replace the file content with following code.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:theme_ninja/app_theme_provider.dart';
import 'package:theme_ninja/home_page_widget.dart';
import 'package:theme_ninja/shared_utility.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final sharedPreferences = await SharedPreferences.getInstance();
  runApp(ProviderScope(
    overrides: [
      // override the previous value with the new object
      sharedPreferencesProvider.overrideWithValue(sharedPreferences),
    ],
    child: MyApp(),
  ));
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final _appThemeState = watch(appThemeStateProvider.state);
    return MaterialApp(
      theme: context
          .read(appThemeProvider)
          .getAppThemedata(context, _appThemeState),
      home: HomePage(),
    );
  }
}

Since the SharedPreferences init() is an asynchronous operation, We may endup using the sharedPreferences instance before it is initialised, So we have to override the previous instance with the lately initialised value .

sharedPreferencesProvider.overrideWithValue(sharedPreferences),

Using Riverpod, We extend our MyApp widget from ConsumerWidget. We have a _appThemeState instance which gets notified every-time the AppThemeNotifier state changes.
Based on the _appThemeState we pull the theme data value with getAppThemedata helper method.

Here ScopedReader helps to read data from the provider.

Now when we toggle the state values between true and false using ElevatedButton, The app theme changes accordingly.

Yey !!! We successfully learnt and built a flutter app for changing the app theme using Riverpod and saving the theme state in Shared Preferences too.

Complete source code is available here .

I may make mistakes too and I would love to learn from you , Beat me up in the comments section below.

Thanks for reading...