Update and persist user settings with hydrated_bloc
Table of Contents
hydrated_bloc by Felix Angelov
What is hydrated_bloc? 🧐 #
hydrated_bloc is a package that extends the bloc package to persist state changes to disk. This allows you to persist user settings, such as theme, language, and other preferences.
hydrated_bloc uses hive as the underlying storage mechanism, which is a fast, NoSQL database that runs on mobile, desktop, and the web.
Setup hydrated_bloc 🛠️ #
To use hydrated_bloc, you need to add it to your pubspec.yaml
file:
dependencies:
hydrated_bloc: ^9.0.0
Then, you need to initialize the storage. You can use the HydratedStorage.build()
method to create a storage instance. You can then pass this instance to the HydratedBloc.storage
property.
In this example, we will make use of the package path_provider
to get the path to the app’s document directory and the HydratedStorage.webStorageDirectory
for web. We will then use this paths to initialize the storage:
void main() {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorage.webStorageDirectory // for web
: await getApplicationDocumentsDirectory(), // everything else
);
// ... runApp
}
Using hydrated_bloc 🎯 #
Now that we have our storage initialized, we can start using hydrated_bloc. We will start by creating a Settings
class, that will be used to store the user settings:
@immutable
class Settings extends Equatable {
const Settings({
required this.themeMode,
});
final ThemeMode themeMode;
// ... place other settings here
Settings copyWith({ThemeMode? themeMode}) =>
Settings(themeMode: themeMode ?? this.themeMode);
Map<String, dynamic> toJson() => {'themeMode': themeMode.index};
factory Settings.fromJson(Map<String, dynamic> map) =>
Settings(themeMode: ThemeMode.values[map['themeMode'] as int]);
@override
bool get stringify => true;
@override
List<Object> get props => [themeMode];
}
Now that our user settings are defined, we can create a SettingsCubit
that extends HydratedCubit
, this cubit will be responsible for updating and persisting the user settings.
We will add a toggleThemeMode
method to the cubit, that will be used to update the theme:
class SettingsCubit extends HydratedCubit<Settings> {
SettingsCubit() : super(const Settings(themeMode: ThemeMode.system));
void toggleThemeMode(ThemeMode themeMode) =>
emit(state.copyWith(themeMode: themeMode));
@override
Settings fromJson(Map<String, dynamic> json) =>
Settings.fromJson(json);
@override
Map<String, dynamic> toJson(Settings state) => state.toJson();
}
hydrated_bloc has a really cool feature which allows us to override a given id
, this is useful when we want to have multiple instances of the same cubit. For example, if we want to have a SettingsCubit
for each user, we can override the id
property to use the user’s id:
// ...
final carlosCubit = SettingsCubit('carlos');
final dimaCubit = SettingsCubit('dima');
// ...
class SettingsCubit extends HydratedCubit<Settings> {
SettingsCubit(this._id) : super(const Settings(themeMode: ThemeMode.system));
final String _id;
@override
String get id => _id;
//... other methods
}
Since the data we want to persist is the user settings is safe to say that providing this at root level is a good idea.
We can do so by using a BlocProvider
, thanks to the flutter_bloc
package (dont forget to add it to your pubspec.yaml
):
void main() {
// ... Setup storage
runApp(
// Provides the settings cubit to the root
BlocProvider(
create: (_) => SettingsCubit(),
child: const MyApp(),
),
);
}
Having the SettingsCubit
available at root level, we can now use context.select
to get the current theme mode and use it to set the theme mode of our app:
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Hydated Storage Demo',
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
// It only listen to the themeMode of the cubit
themeMode: context.select((SettingsCubit c) => c.state.themeMode),
home: const SettingsPage(),
);
}
}
Finally, let’s create a SettingsPage
that will allow the user to change the theme mode:
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context) {
final themeMode = context.select(
(SettingsCubit c) => c.state.themeMode,
);
return Scaffold(
appBar: AppBar(
title: const Text('Settings Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Current theme mode: $themeMode'),
// Using the ThemeMode enum to get the available options
...List.generate(
ThemeMode.values.length,
(index) {
final themeMode = ThemeMode.values[index];
return ElevatedButton(
onPressed: () =>
context.read<SettingsCubit>()
.toggleThemeMode(themeMode),
child: Text(themeMode.name),
);
},
),
],
),
),
);
}
}
Conclusion 📝 #
In this post, I showed you how to use hydrated_bloc to update and persist user settings in a Flutter app. I hope you found this post useful.
I hope you enjoyed it and that you found it useful.
If you have any questions or suggestions, feel free to leave a comment below. 😄
Thanks for reading! 🤓
The full source code with 100% test coverage 🧪 for this post is available here 🔍
The pubspec.yaml file for this project uses the following dependencies 📦
dependencies:
bloc: ^8.1.0
equatable: ^2.0.5 # Used to compare objects
flutter:
sdk: flutter
flutter_bloc: ^8.1.1 # Used to provide the cubit to the root
hydrated_bloc: ^9.0.0 # Used to persist the cubit state
path_provider: ^2.0.11 # Used to get the storage directory path
dev_dependencies:
bloc_test: ^9.1.0 # Used to test the cubit
flutter_test:
sdk: flutter
mocktail: ^0.3.0 # Used to mock the storage
very_good_analysis: ^3.1.0 # Used to enforce very good practices 🦄