Themes are a sure way to add a vibrant touch to your apps. With Flutter's theming support, you can customize literally everything with only a few lines of code. What if you want to let your users change the themes on the fly though? At the very least, every app should support light and dark mode.
One option is to hack something together using StatefulWidgets. Another, and better option, is to use the flutter_bloc library to create an extensible and manageable theme switching framework.
Project Setup
Before we get started building the UI, let's add a few dependencies. We obviously need flutter_bloc and we're also going to add equatable - this is optional, but should we need value equality, this package will do the repetitive work for us.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^0.20.0
equatable: ^0.4.0
...
Let's also create the basic scaffolding of this project. The most important part is the global/theme folder - that's where the ThemeBloc and other related code will be located. After all, themes should be applied globally.
Making Custom Themes
Before doing anything else, we first have to decide on the themes our app will use. Let's keep it simple with only 4 themes - green and blue, both with light and dark variants. Create a new file app_themes.dart inside the theme folder.
Flutter uses a class ThemeData to, well, store theme data. Since we want to configure 4 distinct instances of ThemeData, we will need a simple way to access them. The best option is to use a map. We could use strings for the keys...
app_themes.dart
final appThemeData = {
"Green Light": ThemeData(
brightness: Brightness.light,
primaryColor: Colors.green,
),
...
};
As you can imagine, as soon as we add multiple themes, strings will become cumbersome to use. Instead, let's create an enum AppTheme. The final code will look like this:
app_themes.dart
import 'package:flutter/material.dart';
enum AppTheme {
GreenLight,
GreenDark,
BlueLight,
BlueDark,
}
final appThemeData = {
AppTheme.GreenLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.green,
),
AppTheme.GreenDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.green[700],
),
AppTheme.BlueLight: ThemeData(
brightness: Brightness.light,
primaryColor: Colors.blue,
),
AppTheme.BlueDark: ThemeData(
brightness: Brightness.dark,
primaryColor: Colors.blue[700],
),
};
Adding the Bloc
Having custom themes created, we can move on to create a way for the UI to initiate and also listen to theme changes. This will all happen inside a ThemeBloc which will receive ThemeChanged events, figure out which theme should be displayed, and then output ThemeState containing the proper ThemeData pulled from the Map created above.
If you're new to the Bloc library or the reactive Bloc pattern in general, check out the following tutorial.
While we could create all the files and classes manually, there's a handy extension for both VS Code and IntelliJ which will spare us some time. At least in VS Code, right click on the theme folder and select Bloc: New Bloc from the menu. The name should be "theme" and select "yes" to use the equatable package. This will generate 4 files. One of them, called "bloc", is a barrel file which just exports all the other files.
ThemeEvent
There will be only one event ThemeChanged which pass the selected AppTheme enum value to the Bloc.
theme_event.dart
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';
import '../app_themes.dart';
@immutable
abstract class ThemeEvent extends Equatable {
// Passing class fields in a list to the Equatable super class
ThemeEvent([List props = const []]) : super(props);
}
class ThemeChanged extends ThemeEvent {
final AppTheme theme;
ThemeChanged({
@required this.theme,
}) : super([theme]);
}
ThemeState
Similar to the event, there will be only a single ThemeState. We will actually make the generated class concrete (not abstract), instead of creating a new subclass. There can logically be only one theme in the app. On the other hand, there can be multiple events which cause the theme to change.
This state will hold a ThemeData object which can be used by the MaterialApp.
theme_state.dart
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
@immutable
class ThemeState extends Equatable {
final ThemeData themeData;
ThemeState({
@required this.themeData,
}) : super([themeData]);
}
ThemeBloc
Bloc is the glue which connects events and states together with logic. Because of the way we've set up the app theme data, ThemeBloc will take up only a few simple lines of code.
theme_bloc.dart
import 'dart:async';
import 'package:bloc/bloc.dart';
import '../app_themes.dart';
import './bloc.dart';
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
@override
ThemeState get initialState =>
// Everything is accessible from the appThemeData Map.
ThemeState(themeData: appThemeData[AppTheme.GreenLight]);
@override
Stream<ThemeState> mapEventToState(
ThemeEvent event,
) async* {
if (event is ThemeChanged) {
yield ThemeState(themeData: appThemeData[event.theme]);
}
}
}
Changing Themes
To apply a theme to the whole app, we have to change the theme property on the root MaterialApp. The ThemeBloc will also have to be available throughout the whole app. After all, we need to use its state in the aforementioned MaterialApp and also dispatch events from the PreferencePage, which we are yet to create.
Let's wrap the root widget of our app be a BlocProvider and while we're at it, also add a BlocBuilder which will rebuild the UI on every state change. Since we're operating with the ThemeBloc, the whole UI will be rebuilt when a new ThemeState is outputted.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'ui/global/theme/bloc/bloc.dart';
import 'ui/home/home_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
builder: (context) => ThemeBloc(),
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: _buildWithTheme,
),
);
}
Widget _buildWithTheme(BuildContext context, ThemeState state) {
return MaterialApp(
title: 'Material App',
home: HomePage(),
theme: state.themeData,
);
}
}
Of course, the UI won't ever be rebuilt just yet. For that we need to dispatch the ThemeChanged event to the ThemeBloc. Users will select their preferred theme in the PreferencePage, but first, to make the UI a bit more realistic, let's add a dummy HomePage.
home_page.dart
import 'package:flutter/material.dart';
import '../preference/preference_page.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
onPressed: () {
// Navigate to the PreferencePage
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => PreferencePage(),
));
},
)
],
),
body: Center(
child: Container(
child: Text(
'Home',
style: Theme.of(context).textTheme.display1,
),
),
),
);
}
}
PreferencePage is where the ThemeChanged events will be dispatched. Again, because of the way we can access all of the app themes, implementing the UI will be as simple as accessing the appThemeData map in a ListView.
preference_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:theme_switching_prep/ui/global/theme/app_themes.dart';
import 'package:theme_switching_prep/ui/global/theme/bloc/bloc.dart';
class PreferencePage extends StatelessWidget {
const PreferencePage({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Preferences'),
),
body: ListView.builder(
padding: EdgeInsets.all(8),
itemCount: AppTheme.values.length,
itemBuilder: (context, index) {
// Enums expose their values as a list - perfect for ListView
// Store the theme for the current ListView item
final itemAppTheme = AppTheme.values[index];
return Card(
// Style the cards with the to-be-selected theme colors
color: appThemeData[itemAppTheme].primaryColor,
child: ListTile(
title: Text(
itemAppTheme.toString(),
// To show light text with the dark variants...
style: appThemeData[itemAppTheme].textTheme.body1,
),
onTap: () {
// This will make the Bloc output a new ThemeState,
// which will rebuild the UI because of the BlocBuilder in main.dart
BlocProvider.of<ThemeBloc>(context)
.dispatch(ThemeChanged(theme: itemAppTheme));
},
),
);
},
),
);
}
}
Now launch the app and test it out! Of course, once the app is closed, the selected theme won't be remembered on the next launch. You can use a persistence library of any sort, such as simple preferences, SEMBAST or even Moor. Just persist the selected theme inside the ThemeBloc whenever the ThemeChanged event is dispatched.
Conclusion
Changing themes is a feature which every production app should have. With flutter_bloc and some clever design decisions along the way, we managed to painlessly change the themes while keeping the code maintainable, organized and clean.
pls upload the code for simple preferences for saving the state . so that next time it loads on selected theme.
can’t get it done
I’d recommend you to check out an extension to the flutter_bloc library (from the same author) called hydrated_bloc. It can automatically persist the last state of a Bloc.
https://pub.dev/packages/hydrated_bloc
Very good as usual.
Please remake your “firebase-firestore-chat-app” in flutter, implementing flutter_bloc & Equatable as well. Thank you in advance.
Thank you! I’m certainly going to make a Firebase Flutter app course.
Hello Matej, cool tutorial! I followed with success but i now want to persist the selected theme for the user. I tried using SharedPreferences to store the selected theme as a string, but in the initialState getter of the ThemeBloc i can’t use async function to wait for SharedPreferences to return the stored value. Any pointers on how to go about this? Also I use enum_to_string package to convert the stored string theme value back to enum.
Thanks!
Hey Antonis! To be honest, I didn’t try the proposed solutions at the end of this tutorial myself ?. You should take a look at the hydrated_bloc package – it persists the last bloc state automatically.
https://pub.dev/packages/hydrated_bloc
Hello all,
I have completed the theme persistence task. Check the code here https://github.com/galanis-a/rc_themes_hydrated.
Thanks.
Great use of HydratedBloc! ?
Hi, thank you for this great tut!
I believe there’s an error in the URI in preference_page.dart
import ‘package:theme_switching_prep…
Should be =>
import ‘package:theme_switching_bloc…
(according to your YouTube video tutorial)
This Navigator looks different now. Navigator.push(context, MaterialPageRoute(
builder: (context) => PreferencePage(),
));
Your article helped me a lot, is there any more related content? Thanks!
I don’t think the title of your article matches the content lol. Just kidding, mainly because I had some doubts after reading the article.