Skip to main content
  1. Posts/

Update and persist user settings with hydrated_bloc

<time datetime="2022-11-08 00:00:00 &#43;0000 UTC">8 November 2022</time><span class="px-2 text-primary-500">&middot;</span><span title="Reading time">5 mins</span><span class="px-2 text-primary-500">&middot;</span> <span class="mb-[2px]"> <a href="https://github.com/cgutierr-zgz/cgutierr-zgz.github.io/edit/main/content/posts/storing-settings-with-hydrated-bloc/index.md" class="text-lg hover:text-primary-500" rel="noopener noreferrer" target="_blank" title=""><span class="relative inline-block align-text-bottom px-1 icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M490.3 40.4C512.2 62.27 512.2 97.73 490.3 119.6L460.3 149.7L362.3 51.72L392.4 21.66C414.3-.2135 449.7-.2135 471.6 21.66L490.3 40.4zM172.4 241.7L339.7 74.34L437.7 172.3L270.3 339.6C264.2 345.8 256.7 350.4 248.4 353.2L159.6 382.8C150.1 385.6 141.5 383.4 135 376.1C128.6 370.5 126.4 361 129.2 352.4L158.8 263.6C161.6 255.3 166.2 247.8 172.4 241.7V241.7zM192 63.1C209.7 63.1 224 78.33 224 95.1C224 113.7 209.7 127.1 192 127.1H96C78.33 127.1 64 142.3 64 159.1V416C64 433.7 78.33 448 96 448H352C369.7 448 384 433.7 384 416V319.1C384 302.3 398.3 287.1 416 287.1C433.7 287.1 448 302.3 448 319.1V416C448 469 405 512 352 512H96C42.98 512 0 469 0 416V159.1C0 106.1 42.98 63.1 96 63.1H192z"/></svg> </span> Suggest an edit</a> <a href="https://github.com/cgutierr-zgz/storing_settings_with_hydrated_bloc" class="text-lg hover:text-primary-500" rel="noopener noreferrer" target="_blank"> · <span class="relative inline-block align-text-bottom px-1 icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512"><path fill="currentColor" d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg> </span> View source </a> </span>

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