Flutter App Themes using HydratedBloc
We can simply change flutter app theme with BloC pattern and Shared-preferences library, however we can also achieve this goal using a BloC library extension called HydratedBloc. The hydrated_bloc package is an extension of the flutter_bloc library which automatically stores states so that they can be restored even if the app is closed and opened again later. In this tutorial we are going to create a simple app and change its theme using this library.
Default app layout
Nothing special here, just a simple app with one page:
main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Theme Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: const ChangeThemePage()
);
}
}
change_theme.dart
class ChangeThemePage extends StatefulWidget {
const ChangeThemePage();
@override
_ChangeThemePageState createState() => _ChangeThemePageState();
}
class _ChangeThemePageState extends State<ChangeThemePage> {
bool _isDarkMode=false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Change Theme'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Dark Mode:'),
Switch(
value: _isDarkMode,
onChanged: (value){
setState(() {
_isDarkMode=value;
});
},
)
],
),
),
);
}
}
As you can see there isn’t much of a code in here. The app looks like this:
Adding BloC libraries
First we are going to add 3 libraries, bloc, flutter_bloc and hydrated_bloc:
bloc: ^6.1.0
flutter_bloc: ^6.0.6
hydrated_bloc: ^6.0.3
In the next step we add Bloc Events, currently we have 2 events, one for enabling light theme and another for enabling dark theme:
theme_event.dart
abstract class ThemeEvent{
const ThemeEvent();
}
class LightThemeEvent extends ThemeEvent{
const LightThemeEvent();
}
class DarkThemeEvent extends ThemeEvent{
const DarkThemeEvent();
}
We can add Bloc States but since we are only working with ThemeData
, we can use ThemeData
as our state.
Creating ThemeBloc is not much different from creating a normal bloc, the only difference is we are extending HydratedBloc
.
class ThemeBloc extends HydratedBloc<ThemeEvent, ThemeData> {
ThemeBloc() : super(ThemeData.light());
@override
Stream<ThemeData> mapEventToState(ThemeEvent event) async* {
if (event is LightThemeEvent) yield ThemeData.light();
if (event is DarkThemeEvent) yield ThemeData.dark();
}
@override
ThemeData fromJson(Map<String, dynamic> json) {
try {
if (json['light'] as bool) return ThemeData.light();
return ThemeData.dark();
} catch (_) {
return null;
}
}
@override
Map<String, bool> toJson(ThemeData state) {
try {
return {'light': state == ThemeData.light()};
} catch (_) {
return null;
}
}
}
In the code above:
fromJson
method used to read data from a file, this override method happens automatically and If there is a state stored on the disk, the value passed tosuper()
is ignored and the stored state is picked as initial.toJson
stores apps current state, this method happens automatically after a state change.
Now that our bloc is ready, we can change our UI. First we add bloc to our main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initializing hydratedBloc, we can also change
// the default location of storage file.
HydratedBloc.storage = await HydratedStorage.build();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_)=>ThemeBloc(),
child: BlocBuilder<ThemeBloc,ThemeData>(
builder: (BuildContext context, theme)=>
MaterialApp(
title: 'Flutter Theme Demo',
theme: theme,
home: ChangeThemePage(theme==ThemeData.dark()),
)
),
);
}
}
And we also add bloc to change_theme.dart :
class ChangeThemePage extends StatefulWidget {
final bool isCurrentThemeDark;
const ChangeThemePage(this.isCurrentThemeDark);
@override
_ChangeThemePageState createState() => _ChangeThemePageState();
}
class _ChangeThemePageState extends State<ChangeThemePage> {
bool _isDarkMode;
@override
void initState() {
_isDarkMode = widget.isCurrentThemeDark;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Change Theme'),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Dark Mode:'),
BlocListener<ThemeBloc, ThemeData>(
listener: (context, state) {
_isDarkMode = state == ThemeData.dark();
},
child: Switch(
value: _isDarkMode,
onChanged: (value) {
context.bloc<ThemeBloc>().add(value ? DarkThemeEvent() : LightThemeEvent());
setState(() {
_isDarkMode = value;
});
},
),
)
],
),
),
);
}
}
In this code:
- We read
isCurrentThemeDark
from main widget to get the initial state of the Switch. - We also added
BlocListener
to theSwitch
to change its value. - With the help of
context.bloc()<ThemeBloc>
we fire new events using the switch.
Here is the result:
The code is also available from this github repository.