arrow-left arrow-right brightness-2 chevron-left chevron-right facebook-box facebook loader magnify menu-down rss-box star twitter-box youtube-box twitter white-balance-sunny window-close
Flutter & MobX: Dark/Light Mode Switcher
6 min read

Flutter & MobX: Dark/Light Mode Switcher

Flutter & MobX: Dark/Light Mode Switcher

In this article we're going to create a small application that uses  Flutter MobX and Provider to toggle between two ThemeData states. We'll look at the following key concepts:

  1. How to change between dark and light mode.
  2. How to create a ThemeStore which will be responsible for firing action(s) and managing observable values.
  3. How to inject the ThemeStore into our Widget tree
  4. How to show a SnackBar based on a reaction between when the application is in dark or light mode.

Project Setup

Let's go ahead and create a new Flutter project and install our required dependencies:

# New Flutter project
$ flutter create mobx_theme

# Open in VS Code
$ cd mobx_theme && code .

Once we've got the project open, we can update pubspec.yaml with the following dependencies and dev_dependencies:

dependencies:
  flutter:
    sdk: flutter

  provider: ^4.0.5
  mobx: ^1.1.1
  flutter_mobx: ^1.1.0
  shared_preferences: ^0.5.6+3

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  mobx_codegen: ^1.0.3

Switching Between Dark and Light Mode

Before implementing any state management solutions, how do we switch between dark and light mode? Flutter makes it easy with changes to brightness within a selected ThemeData.

Here's an example of a lightTheme and darkTheme respectively:

  ThemeData get lightTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.deepPurpleAccent,
        brightness: Brightness.light,
        scaffoldBackgroundColor: Color(0xFFecf0f1),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  ThemeData get darkTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.tealAccent,
        brightness: Brightness.dark,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

This only represents a small amount of the overall configurable theming options, and it's encouraged that you come up with your own theme(s) here. :)

Theme Repository

We'll start off by creating an IThemeRepository interface:

/// lib/domain/theme/interfaces/i_theme_repository.dart
import 'package:flutter/material.dart';

abstract class IThemeRepository {
  Future<String> getThemeKey();
  Future<void> setThemeKey(Brightness brightness);
}

We'll then create a ThemeKey class to hold our constant key(s) related to Theme:

class ThemeKey {
  static const String THEME = "theme";
}

In the future, you may want to abstract this functionality out into a Preferences repository as seen in my other article: https://developer.school/how-to-save-data-to-localstorage-shared-prefs-in-flutter-flutter-web/

Our implementation of this can be seen here:

/// lib/infrastructure/theme/datasources/theme_repository.dart
import 'dart:ui';

import 'package:mobx_theme/domain/theme/constants/theme_keys.dart';
import 'package:mobx_theme/domain/theme/interfaces/i_theme_repository.dart';
import 'package:shared_preferences/shared_preferences.dart';

class ThemeRepository implements IThemeRepository {
  @override
  Future<void> setThemeKey(Brightness brightness) async {
    (await SharedPreferences.getInstance()).setString(
      ThemeKey.THEME,
      brightness == Brightness.light ? "light" : "dark",
    );
  }

  @override
  Future<String> getThemeKey() async {
    return (await SharedPreferences.getInstance()).getString(ThemeKey.THEME);
  }
}

Essentially, we're either setting a SharedPreferences value of either light or dark depending on the brightness passed in. We're also able to retrieve this value to set the appropriate theme in the future.

Theme Service

We've now got the ability to save and retrieve theme keys. We can go ahead and create a ThemeService which uses this to return an appropriate theme for our user:

/// lib/application/theme/services/theme_service.dart
import 'package:flutter/material.dart';
import 'package:mobx_theme/domain/theme/interfaces/i_theme_repository.dart';

class ThemeService {
  ThemeService(IThemeRepository themeRepository)
      : _themeRepository = themeRepository;

  IThemeRepository _themeRepository;

  ThemeData get lightTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.deepPurpleAccent,
        brightness: Brightness.light,
        scaffoldBackgroundColor: Color(0xFFecf0f1),
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  ThemeData get darkTheme => ThemeData(
        primarySwatch: Colors.teal,
        accentColor: Colors.tealAccent,
        brightness: Brightness.dark,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      );

  Future<ThemeData> getTheme() async {
    final String themeKey = await _themeRepository.getThemeKey();

    if (themeKey == null) {
      await _themeRepository.setThemeKey(lightTheme.brightness);

      return lightTheme;
    } else {
      return themeKey == "light" ? lightTheme : darkTheme;
    }
  }

  Future<ThemeData> toggleTheme(ThemeData theme) async {
    if (theme == lightTheme) {
      theme = darkTheme;
    } else {
      theme = lightTheme;
    }

    await _themeRepository.setThemeKey(theme.brightness);
    return theme;
  }
}

Next up, we'll use this service inside of our ThemeStore to handle reactivity on these value(s):

Theme Store

The Store will be responsible for exposing our current theme to our MaterialApp. We've also created a computed getter for isDark which we can use at any point to determine whether we're currently in dark mode.

/// lib/application/theme/store/theme_store.dart
import 'package:flutter/material.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_theme/application/theme/services/theme_service.dart';

part 'theme_store.g.dart';

class ThemeStore extends _ThemeStore with _$ThemeStore {
  ThemeStore(ThemeService themeService) : super(themeService);
}

abstract class _ThemeStore with Store {
  _ThemeStore(this._themeService);

  final ThemeService _themeService;

  @computed
  bool get isDark => theme.brightness == Brightness.dark;

  @observable
  ThemeData theme;

  @action
  Future<void> getTheme() async {
    theme = _themeService.lightTheme;
    theme = await _themeService.getTheme();
  }

  @action
  Future<void> toggleTheme() async {
    theme = await _themeService.toggleTheme(theme);
  }
}

As MobX requires some code generation, we'll need to run the build_runner with mobx_codegen. Run the following in your terminal:

$ flutter pub run build_runner build 

Providing our Store

We're now able to switch brightness, however, we need to provide our MaterialApp with the current value of our theme. We can do that by using Provider:

///lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx_theme/application/theme/services/theme_service.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:mobx_theme/infrastructure/theme/datasources/theme_repository.dart';
import 'package:mobx_theme/presentation/pages/splash_screen.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        Provider<ThemeStore>(
            create: (_) =>
                ThemeStore(ThemeService(ThemeRepository()))..getTheme())
      ],
      child: Consumer<ThemeStore>(
        builder: (_, ThemeStore value, __) => Observer(
          builder: (_) => MaterialApp(
            debugShowCheckedModeBanner: false,
            title: 'MobX Theme Switcher',
            theme: value.theme,
            home: SplashPage(),
          ),
        ),
      ),
    );
  }
}

Here we're providing the ThemeStore into our Widget tree and immediately using Consumer to get the current theme.value.

As we've created theme as an @observable using MobX, any changes to theme will be reactive as we've wrapped our MaterialApp in an Observer widget.

Creating our SplashPage

As we're strictly dealing with the ability to change between dark and light mode in this article, I've created one page - SplashPage which has a simple title/subtitle. We're able to switch between dark and light mode by clicking the Floating Action Button:

Here's how we achieve this:

/// lib/presentation/pages/splash_page.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:provider/provider.dart';

class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  ThemeStore themeStore;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    themeStore ??= Provider.of<ThemeStore>(context);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: themeStore.toggleTheme,
        child: themeStore.isDark
            ? Icon(Icons.brightness_high)
            : Icon(Icons.brightness_2),
      ),
      body: buildSplash(context),
    );
  }

  Widget buildSplash(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: themeStore.isDark
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 20,
            ),
            Text(
              "Foodie",
              style: TextStyle(fontSize: 26),
            ),
            SizedBox(
              height: 4,
            ),
            Text(
              "The best way to track your nutrition.",
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }
}

There isn't too much out of the ordinary here. We're getting access to our ThemeStore in didChangeDependencies and using the themeStore.toggleTheme action when our FAB is pressed.

Using MobX Reactions

I can't think of many use cases for this, but what if you wanted to show a Snackbar (or another reaction) whenever the theme has been changed? Here's an example of what this could look like:

This is made easy with MobX. We have to register our ReactionDisposer with the Observable that we want to react against:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
import 'package:mobx_theme/application/theme/store/theme_store.dart';
import 'package:provider/provider.dart';

class SplashPage extends StatefulWidget {
  @override
  _SplashPageState createState() => _SplashPageState();
}

class _SplashPageState extends State<SplashPage> {
  ThemeStore themeStore;

  GlobalKey<ScaffoldState> _scaffoldKey;
  List<ReactionDisposer> _disposers;

  @override
  void initState() {
    super.initState();
    _scaffoldKey = GlobalKey<ScaffoldState>();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    themeStore ??= Provider.of<ThemeStore>(context);
    _disposers ??= [
      reaction((fn) => themeStore.isDark, (isDark) {
        _scaffoldKey.currentState?.removeCurrentSnackBar();

        if (isDark) {
          _scaffoldKey.currentState.showSnackBar(SnackBar(
            content: Text("Hello, Dark!"),
          ));
        } else {
          _scaffoldKey.currentState.showSnackBar(SnackBar(
            content: Text("Hello, Light!"),
          ));
        }
      })
    ];
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: _scaffoldKey,
      floatingActionButton: FloatingActionButton(
        onPressed: themeStore.toggleTheme,
        child: themeStore.isDark
            ? Icon(Icons.brightness_high)
            : Icon(Icons.brightness_2),
      ),
      body: buildSplash(context),
    );
  }

  Widget buildSplash(BuildContext context) {
    return AnnotatedRegion<SystemUiOverlayStyle>(
      value: themeStore.isDark
          ? SystemUiOverlayStyle.light
          : SystemUiOverlayStyle.dark,
      child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 20,
            ),
            Text(
              "Foodie",
              style: TextStyle(fontSize: 26),
            ),
            SizedBox(
              height: 4,
            ),
            Text(
              "The best way to track your nutrition.",
              style: TextStyle(fontSize: 16),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _disposers.forEach((disposer) => disposer());
    super.dispose();
  }
}

Whenever we register a reaction it returns a ReactionDisposer<T> which can be called to dispose of the reaction. We're only registering one reaction here, but for simplicity sake we've used a List to make it more flexible.

Our application is now able to react to isDark changes.

Summary

In this article we looked at one potential way to implement dynamic theming with MobX. I'd love to hear your thoughts as to how this could be improved and/or any future libraries you'd like me to investigate.

Code for this article: https://github.com/PaulHalliday/mobx_theme