Skip to main content
  1. Posts/

Update and persist user settings with hydrated_bloc

hydrated_bloc by Felix Angelov

In this post, I’ll show you how to use hydrated_bloc to update and persist user settings like theme, language, etc. in a Flutter app.
This post was made using hydrated_bloc version 9.0.0

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();
}
Important: Persisting multiple instances of the same cubit

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),
                );
              },
            ),
          ],
        ),
      ),
    );
  }
}
hydated_bloc demo

And... that's it! 🎉


Our user settings are now persisted to disk and we could use them to update the app's theme, language, etc.

Easy, right? 😄

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 🦄

References 📚 #

Author
Carlos Gutiérrez