Flutter Bloc | Switch Theme

Wing CHAN
3 min readMar 22, 2022

--

flutter switch theme using Bloc demo

Features

  1. Change app theme mode using Bloc
  2. Persists and restores app theme mode setting using hydrated_bloc

Change app theme mode using Bloc

First of all, we need create the bloc call ThemeBloc which handle switch theme mode logic.

theme_state.dart

part of 'theme_bloc.dart';class ThemeState extends Equatable {
final ThemeMode themeMode;

const ThemeState({
required this.themeMode,
});

const ThemeState.initial() : this(themeMode: ThemeMode.system);

@override
List<Object> get props => [themeMode];
}

theme_event.dart

part of 'theme_bloc.dart';

abstract class ThemeEvent extends Equatable {
const ThemeEvent();
}

class ThemeModeSwitched extends ThemeEvent {
const ThemeModeSwitched({required this.themeMode});

final ThemeMode themeMode;

@override
List<Object> get props => [themeMode];
}

theme_bloc.dart

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
part 'theme_event.dart';
part 'theme_state.dart';
class ThemeBloc extends Bloc<ThemeEvent, ThemeState> {
ThemeBloc() : super(const ThemeState.initial()) {
on<ThemeModeSwitched>(_onThemeModeSwitched);
}

void _onThemeModeSwitched(ThemeModeSwitched event, Emitter<ThemeState> emit) {
ThemeMode newThemeMode = event.themeMode;
emit(
ThemeState(themeMode: newThemeMode),
);
}
}

Then we need to configure bloc environment.

void main() {
BlocOverrides.runZoned(
() async => runApp(
const App(),
),
);
}
class App extends StatelessWidget {
const App({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<ThemeBloc>(
create: (BuildContext context) => ThemeBloc(),
),
],
child: const AppView(),
);
}
}

We need update themeMode of MaterialApp when ThemeState in response to new states, so i use BlocBuilder on top of MaterialApp.

To simplify the presentation, I used the flex_color_scheme to handletheme and darkTheme .

class AppView extends StatelessWidget {
const AppView({
Key? key,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
ThemeMode themeMode = state.themeMode;
return MaterialApp(
title: 'Flutter Demo',
theme: FlexThemeData.light(scheme: FlexScheme.mandyRed),
darkTheme: FlexThemeData.dark(scheme: FlexScheme.mandyRed),
themeMode: themeMode,
home: const MyHomePage(title: 'Bloc theme switch'),
);
},
);
}
}

I showed how to use the bloc display and change the ThemeBloc themeMode settings in MyHomePage.

class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: BlocBuilder<ThemeBloc, ThemeState>(
builder: (context, state) {
return Column(
children: <Widget>[
...ThemeMode.values.map((themeMode) {
return RadioListTile<ThemeMode>(
value: themeMode,
groupValue: state.themeMode,
title: Text(themeMode.name),
onChanged: (newThemeMode) {
if (newThemeMode != null) {
context
.read<ThemeBloc>()
.add(ThemeModeSwitched(themeMode: newThemeMode));
}
},
);
}).toList()
],
);
},
),
),
);
}
}

Persists and restores app theme mode setting using hydrated_bloc

In the above code we have finished controlling the theme mode of the apps using Bloc, but when we reopen the apps, we lose the theme mode state we just set.

This problem is because the state is stored in the memory, which will be cleared when the apps are closed,.

To solve this problem, we need to save the state to a local file, bloc already provides a good solution for us call hydrated_bloc

hydrated_bloc:
An extension to package:bloc which automatically persists and restores bloc and cubit states. Built to work with package:bloc.

Usage

path_provider can help us find the ApplicationDocumentsDirectory location path.

hydrated_bloc: ^8.1.0
path_provider: ^2.0.9

Setup HydratedStorage

setup ApplicationDocumentsDirectory as storage Directory

void main() async {
WidgetsFlutterBinding.ensureInitialized();
final storage = await HydratedStorage.build(
storageDirectory: await getApplicationDocumentsDirectory(),
);


HydratedBlocOverrides.runZoned(
() async => runApp(
const App(),
),
storage: storage,
);
}

Implement formJson and toJson in ThemeState

The best way is use freezed to generate, I have another article about freezed

class ThemeState extends Equatable {
final ThemeMode themeMode;

const ThemeState({
required this.themeMode,
});

const ThemeState.initial() : this(themeMode: ThemeMode.system);

ThemeState.fromJson(Map<String, dynamic> json)
: themeMode = ThemeMode.values.byName(json['themeMode']);

Map<String, dynamic>? toJson() {
return {'themeMode': themeMode.name};
}


@override
List<Object> get props => [themeMode];
}

Use HydeatedBloc replace Bloc

...
import 'package:hydrated_bloc/hydrated_bloc.dart';
class ThemeBloc extends HydratedBloc<ThemeEvent, ThemeState> {
ThemeBloc() : super(const ThemeState.initial()) {
on<ThemeModeSwitched>(_onThemeModeSwitched);
}

void _onThemeModeSwitched(ThemeModeSwitched event, Emitter<ThemeState> emit) {
ThemeMode newThemeMode = event.themeMode;
emit(
ThemeState(themeMode: newThemeMode),
);
}

@override
ThemeState? fromJson(Map<String, dynamic> json) {
return ThemeState.fromJson(json);
}

@override
Map<String, dynamic>? toJson(ThemeState state) {
return state.toJson();
}

}

Now all the parts needed have been completed 🎉

Completed Code:

I hope you enjoy reading this article and hitting clap 👏 .

--

--