Change flutter Application Theme dynamically using “Hydrated-bloc”

Michael Adeyemo
8 min readApr 1, 2020

This tutorial is an update to one of my previous post- Change flutter Application Theme dynamically using “bloc” which i wrote somethings ago, but since then, the flutter_bloc and the way we approach dynamic theming has changed. Therefore this tutorial is set to address the modern theming method and how to use hydrated_bloc to change and persist theme settings across app restart.

What we are creating

In this tutorial we are going to recreate the home page of my “ Dart tutorial app” with little adjustment. By the end of this tutorial we should have an app that looks like this

Let’s get started

create a new flutter project and name it hydrated_bloc_example.

Open the pubspec.yaml and add the following dependencies

hydrated_bloc: ^3.0.0
flutter_bloc: ^3.2.0
equatable: ^1.1.1

Notice that i did not add shared_preferences because the hydrated_bloc will persist the state and we do not have to do it manually using shared_preferences. Run the “flutter pub get” and make sure there is no error.

In the lib folder, create a new theme.dart file, create a folder called “bloc”, then create another folder called “home_page”. The theme.dart file will contains all the codes relating to the light and dark themeData. The bloc folder will contain all the codes relating to the hydrated_bloc, while the home_page will contain UI codes relating to the home page.

At this point your lib folder should look like this.

theme.dart

Inside the theme.dart file, add this code

const Color primaryColor = Color(0xFF6990AF);
const Color secondaryColor = Color(0xFF73AFB0);

final ThemeData lightTheme = _buildLightTheme();
final ThemeData darkTheme = _buildDarkTheme();
ThemeData _buildLightTheme() {
final ColorScheme colorScheme = const ColorScheme.light().copyWith(
primary: primaryColor,
secondary: secondaryColor,
);
final ThemeData base = ThemeData(
brightness: Brightness.light,
accentColorBrightness: Brightness.dark,
colorScheme: colorScheme,
primaryColor: primaryColor,
buttonColor: primaryColor,
indicatorColor: Colors.white,
toggleableActiveColor: secondaryColor,
splashColor: Colors.white24,
splashFactory: InkRipple.splashFactory,
accentColor: secondaryColor,
canvasColor: Colors.white,
scaffoldBackgroundColor: Colors.white,
backgroundColor: Colors.white,
errorColor: const Color(0xFFB00020),
buttonTheme: ButtonThemeData(
colorScheme: colorScheme,
textTheme: ButtonTextTheme.primary,
),
);
return base;
}

ThemeData _buildDarkTheme() {
final ColorScheme colorScheme = const ColorScheme.dark().copyWith(
primary: primaryColor,
secondary: secondaryColor,
);
final ThemeData base = ThemeData(
brightness: Brightness.dark,
accentColorBrightness: Brightness.dark,
primaryColor: primaryColor,
cardColor: Color(0xFF121A26),
primaryColorDark: const Color(0xFF0050a0),
primaryColorLight: secondaryColor,
buttonColor: primaryColor,
indicatorColor: Colors.white,
toggleableActiveColor: secondaryColor,
accentColor: secondaryColor,
canvasColor: const Color(0xFF2A4058),
scaffoldBackgroundColor: const Color(0xFF121A26),
backgroundColor: const Color(0xFF0D1520),
errorColor: const Color(0xFFB00020),
buttonTheme: ButtonThemeData(
colorScheme: colorScheme,
textTheme: ButtonTextTheme.primary,
),
);
return base;
}

Hydrated bloc

When we design bloc, we need to think of the event and state of the bloc.

Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.

States are the output of a Bloc and represent a part of your application’s state. UI components can be notified of states and redraw portions of themselves based on the current state.

some of the guidelines of using bloc is that:

  1. Components should send inputs “as is”.
  2. Components should show outputs as close as possible to “as is”.

We would try as much as possible to follow these guidelines when creating our event and state.

Inside the bloc folder, create three files as follows

theme_change_event.dart

In our case we would only define a single event to change the theme. Inside the theme_change_event.dart add the following code.

abstract class ThemeChangeEvent extends Equatable {}

class OnThemeChangedEvent extends ThemeChangeEvent {
final bool lightMode;
OnThemeChangedEvent(this.lightMode);
@override
List<Object> get props => [lightMode];
}

Our OnThemeChangeEvent has a lightMode property to inform the bloc the type of event been pass in.

theme_change_state.dart

Since the MaterialApp component already have a theme, darkTheme and a themeMode property, we can leverage on these to create the state.

the ThemeMode Describes which theme will be used by MaterialApp.

So what we would try to do is to supply the MaterialAPP with the theme and darkTheme property and the use our bloc to toggle the themeMode depending on the user preference. In the Bonus section of this article, i will tell you the downside of this approach and then make suggestion for an alternative.

Inside the theme_change_state.dart file, create a class as follows.

class ThemeState extends Equatable{
final bool isLightMode;
final ThemeMode themeMode;
const ThemeState(this.isLightMode, this.themeMode);
factory ThemeState.light() {
return ThemeState(true, ThemeMode.light);
}
factory ThemeState.dark() {
return ThemeState(false, ThemeMode.dark);
}
@override
List<Object> get props => [isLightMode,themeMode];
}

We created this wrapper class to avoid doing any logic in the UI, therefore following the guideline-Components should show outputs as close as possible to “as is”. Add the following code under the ThemeState class.

abstract class ThemeChangeState extends Equatable {
final ThemeState themeState;
ThemeChangeState(this.themeState);
@override
List<Object> get props => [themeState];
}

class LightThemeState extends ThemeChangeState {
static final state = ThemeState.light();
LightThemeState() : super(state);
}

class DarkThemeState extends ThemeChangeState {
static final state = ThemeState.dark();
DarkThemeState() : super(state);
}

theme_change_bloc.dart

A Bloc (Business Logic Component) is a component which converts a Stream of incoming Events into a Stream of outgoing States.

Inside the theme_change_bloc, create a class ThemeChangeBloc that extends the HydratedBloc inside of the normal Bloc class.

Extending the HydratedBloc requires you to override the toJson and fromJson method because the HydratedBloc needs to know how to persist and retrieve your state.

You are required to implement the toJson and fromJson method, and then the mapEventToState method. Do that as follows.

class ThemeChangeBloc extends HydratedBloc<ThemeChangeEvent, ThemeChangeState> {

@override
ThemeChangeState get initialState => super.initialState ?? LightThemeState();

@override
ThemeChangeState fromJson(Map<String, dynamic> json) {
bool isLightMode = json["isLightMode"] as bool;
if (isLightMode) {
return LightThemeState();
} else {
return DarkThemeState();
}
}

@override
Map<String, dynamic> toJson(ThemeChangeState state) {
return {"isLightMode": state.themeState.isLightMode};
}

@override
Stream<ThemeChangeState> mapEventToState(ThemeChangeEvent event) async* {
if (event is OnThemeChangedEvent) {
yield* _onChangedTheme(event.lightMode);
}
}

Stream<ThemeChangeState> _onChangedTheme(bool lightMode) async* {
if (lightMode) {
yield LightThemeState();
} else {
yield DarkThemeState();
}
}
}

The toJson method simply persist the isLightMode property from the themeState as a bool value with the key “isLightMode”. Then in the fromMap method we simply retrieve the bool value from the key “isLightMode” and check if it is true or false. If it is true, return LightThemeState() otherwise return DarkThemeState(). the mapEventToState also yield the corresponding state according to the lightMode property.

Home page

In the home_page folder, create the following dart files.

inside the home_page.dart add the following code.

class DartTutorialHomePage extends StatefulWidget {
DartTutorialHomePage();
@override
_DartTutorialHomePageState createState() => _DartTutorialHomePageState();
}

class _DartTutorialHomePageState extends State<DartTutorialHomePage> {
@override
Widget build(BuildContext context) {
return Container(
child: Scaffold(
appBar: myAppBar(),
body: ListView.separated(
itemCount: contents.length,
itemBuilder: (BuildContext context, int index) {
return Card(
child: ListTile(
title: Text("${contents[index]}"),
),
);
},
separatorBuilder: (BuildContext context, int index) {
return Divider();
},
),
),
);
}
}

final contents = [
"Dart Overview",
"Dart Syntax and keywords",
"Dart Variable and Data types",
"Dart Operators",
"Dart Constant and Data types",
"Dart Control flow Statement",
"Dart Functions",
"Dart Classes and Inheritance",
"Dart Abstract Class and Interface",
"Dart Libraries and Packages",
"Dart Concurrency, Async and Future",
"Asynchronous Programming Stream",
"Dart Exception, Typedef and Debugging",
"Dart Unit Testing"
];

you should get an error message concerning the myAppBar() because it is not yet defined, we would come back to that in a moment.

main.dart

In the main.dart file, we need to provide the bloc above the MaterialApp, so that it will be available down the widget tree to any level. We would use the BlocProvider and the BlocBuilder class from the flutter_bloc package.

BlocProvider is a Flutter widget which provides a bloc to its children via BlocProvider.of<T>(context). It is used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.

In most cases, BlocProvider should be used to create new blocs which will be made available to the rest of the subtree. In this case, since BlocProvider is responsible for creating the bloc, it will automatically handle closing the bloc.

BlocBuilder is a Flutter widget which requires a Bloc and a builder function. BlocBuilder handles building the widget in response to new states. BlocBuilder is very similar to StreamBuilder but has a more simple API to reduce the amount of boilerplate code needed. The builder function will potentially be called many times and should be a pure function that returns a widget in response to the state.

Inside the main.dart file add the following code.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
BlocSupervisor.delegate = await HydratedBlocDelegate.build();
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<ThemeChangeBloc>(
create: (_) => ThemeChangeBloc(),
child: BlocBuilder<ThemeChangeBloc, ThemeChangeState>(
builder: (context, state) {
return MaterialApp(
title: 'Flutter Demo',
themeMode: state.themeState.themeMode,
darkTheme: darkTheme,
theme: lightTheme,
home: DartTutorialHomePage(),
);
}),
);
}
}

Notice how the BlocProvider and BlocBuilder is used in the main.dart . Also notice that no logic is performed in the MaterialApp other than supplying the themeMode from the state.themeState.

Back to myAppBar()

With the ThemeChangeBloc already provider form the main page, we can use the BlocBuilder to listen to the bloc from anywhere in the widget tree.

Inside the my_app_bar.dart add the following code.

AppBar myAppBar() => AppBar(
elevation: 0.0,
title: Text("Dart Tutorial"),
leading: IconButton(
icon: Icon(Icons.code),
onPressed: () {},
),
actions: <Widget>[
Row(
children: <Widget>[
Text(
"Light Mode",
style: TextStyle(fontSize: 12),
),
BlocBuilder<ThemeChangeBloc, ThemeChangeState>(
builder: (context, state) {
return Padding(
padding: EdgeInsets.only(top: 0),
child: Switch(
value: state.themeState.isLightMode,
onChanged: (value) =>
BlocProvider.of<ThemeChangeBloc>(context)
.add(OnThemeChangedEvent(value))),
);
},
)
],
),
],
);

Notice how the BlocBuilder is been used, and also notice that there is no logic written to manipulate the Switch other than supplying the isLightMode to the value property and adding the value to the OnThemeChangedEvent.

Bonus

The problem with our present implementation of the theme is that, our app theme does not respond to the system theme changes for example , if your user is using Android 10 and switch the phone to dark mode, our app will not automatically change to the provided dark theme since we have overrides it behavior. The user will have to manually toggle the theme Switch provided in the app.

One way to avoid this is to change the implementation of our bloc state to the following.

Change the ThemeState to use ThemeData instead of ThemeMode

class ThemeState extends Equatable {
final bool isLightMode;
final ThemeData themeData;
const ThemeState(this.isLightMode, this.themeData);
factory ThemeState.light() {
return ThemeState(true, lightTheme);
}
factory ThemeState.dark() {
return ThemeState(false, darkTheme);
}
@override
List<Object> get props => [isLightMode, themeData];
}

Then change the MaterialApp in the main.dart to

MaterialApp(
darkTheme: darkTheme,
theme: state.themeState.themeData,
home: DartTutorialHomePage(),
)

No other change is required. With this, the app can respond to system theme change and also the inbuilt theme change.

Do you need a senior flutter developer? message me.

--

--