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 handle
theme
anddarkTheme
.
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 👏 .