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:
- How to change between dark and light mode.
- How to create a
ThemeStore
which will be responsible for firing action(s) and managing observable values. - How to inject the
ThemeStore
into our Widget tree - How to show a
SnackBar
based on areaction
between when the application is indark
orlight
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
Top comments (1)
Hello, i'm trying to implement this example in my source code, but i having the follow error. Maybe i having a problem in my main.
dev-to-uploads.s3.amazonaws.com/i/...
My source code: github.com/ViniOkamoto/PomodoroNeu