Hydrated Bloc vs Shared Preferences in Flutter: Pros & Cons Explained

image

Introduction

In Flutter, two popular state management and data persistence solutions are Hydrated Bloc and Shared Preferences. Both offer ways to save data across app sessions, but they work in fundamentally different ways and suit different use cases. Let’s explore their unique characteristics, advantages, disadvantages, and examples to understand when to use each effectively.

What is Hydrated Bloc?

Hydrated Bloc is a library that builds on top of the Bloc state management solution, making it easy to persist and retrieve data across app sessions. With Hydrated Bloc, the state of each Bloc is automatically saved to disk whenever the state changes, and rehydrated (restored) when the app is restarted. It uses hydrated_bloc as an extension of Bloc to handle data persistence automatically.

What is Shared Preferences?

Shared Preferences is a Flutter library that provides a way to store small amounts of data in key-value pairs on the device’s local storage. It’s commonly used to save simple preferences like user settings, theme choices, or login credentials, which need to persist across app launches but do not require complex data structures.

Advantages of Hydrated Bloc and Shared Preferences

Hydrated Bloc
  1. Automatic State Persistence: Hydrated Bloc saves and retrieves the state automatically, simplifying complex state management by eliminating the need for manual persistence code.
  2. Supports Complex Data Types: Hydrated Bloc supports custom models, making it easy to store entire objects or complex data structures.
  3. Integrated with Bloc Architecture: For apps already using Bloc for state management, Hydrated Bloc fits seamlessly, eliminating the need for additional libraries.
  4. Efficient and Fast: Uses an in-memory cache and optimizations to provide efficient read and write operations without noticeable lag for users.
Shared Preferences
  1. Simple and Lightweight: Shared Preferences is easy to set up, ideal for storing small, simple data items like booleans, integers, strings, and basic preferences.
  2. No Dependencies on State Management: Shared Preferences is standalone and can be used in any Flutter app without requiring a specific state management solution.
  3. Cross-Platform: Works across Android, iOS, and the web, allowing for broad compatibility.
  4. Great for Small Data Needs: Efficient for small amounts of data without the need for complex object storage.

Disadvantages of Hydrated Bloc and Shared Preferences

Hydrated Bloc
  1. Additional Overhead: Introduces dependencies on the Bloc package, which might be unnecessary for simple applications.
  2. Limited to Bloc Architecture: Hydrated Bloc is designed specifically for applications that use the Bloc pattern, so it might not suit apps using other state management methods.
  3. Memory Usage: Stores data in memory, which could affect performance with large states or numerous Blocs.
Shared Preferences
  1. Limited Data Types: Supports only primitive data types (int, double, bool, String), making it harder to store complex objects without custom encoding.
  2. Manual Data Management: Requires developers to manually manage data storage and retrieval, adding extra code complexity.
  3. Slow for Large Data: Shared Preferences is not suitable for storing large data sets, as it could lead to slower read/write operations.

Memory Usage Comparison

Hydrated Bloc:
  • In-Memory Storage: Hydrated Bloc stores the state in memory throughout the app’s runtime, which means the entire state of each Bloc is kept in RAM. This can increase memory usage, especially if the stored data is large or if there are multiple Blocs managing substantial state data.
  • Persistence on State Change: Only the most recent state is saved to disk when the state changes, which reduces disk writes but requires more RAM to keep state data readily available. This memory usage can be significant if your app has many Blocs or if the Blocs handle large or complex data structures.
  • Garbage Collection and Clearing: Hydrated Bloc clears memory usage for Blocs when they’re disposed, but long-running Blocs can contribute to higher memory usage if not managed carefully.
Shared Preferences:
  • Disk-Based Storage: Shared Preferences keeps all data on the disk and only loads data into memory temporarily when it’s accessed, so its memory footprint is minimal during runtime.
  • Small Data Focus: Designed for simple data storage, Shared Preferences does not consume much memory and is efficient for low-memory devices. However, it has to re-read data from disk each time it’s needed, which can be slower but helps keep memory usage low.
  • No Complex Data: Shared Preferences doesn’t natively support storing complex objects (without custom serialization), which also limits how much memory it will consume.

When to Use Hydrated Bloc vs Shared Preferences

  • Use Hydrated Bloc when your app has complex state management needs, involves multiple layers of state, or needs to save complex data objects persistently without significant manual coding.
  • Use Shared Preferences when you only need to store simple key-value data persistently, such as user preferences or small settings, and when complex state management is not required.

Example: Hydrated Bloc vs Shared Preferences in Action

Example 1: Using Hydrated Bloc

To start using Hydrated Bloc, add the hydrated_bloc and bloc packages to your pubspec.yaml file:

dependencies:
  flutter_bloc: ^8.0.0
  hydrated_bloc: ^8.0.0

Next, configure Hydrated Bloc in the main entry point of your app, usually in main.dart.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'dart:async';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final storage = await HydratedStorage.build();
  HydratedBlocOverrides.runZoned(
    () => runApp(MyApp()),
    storage: storage,
  );
}

class CounterCubit extends HydratedCubit<int> {
  CounterCubit() : super(0);

  void increment() => emit(state + 1);

  @override
  int? fromJson(Map<String, dynamic> json) => json['value'] as int;

  @override
  Map<String, dynamic>? toJson(int state) => {'value': state};
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        create: (context) => CounterCubit(),
        child: CounterScreen(),
      ),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counterCubit = context.read<CounterCubit>();
    return Scaffold(
      appBar: AppBar(title: Text('Hydrated Bloc Example')),
      body: Center(
        child: BlocBuilder<CounterCubit, int>(
          builder: (context, count) => Text('$count', style: TextStyle(fontSize: 24)),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counterCubit.increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

Example 2: Using Shared Preferences

For Shared Preferences, add the shared_preferences package to your pubspec.yaml file:

dependencies:
  shared_preferences: ^2.0.6

To use Shared Preferences, we’ll create a simple counter app similar to the Hydrated Bloc example:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: CounterScreen(),
    );
  }
}

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _loadCounter();
  }

  _loadCounter() async {
    final prefs = await SharedPreferences.getInstance();
    setState(() {
      _counter = prefs.getInt('counter') ?? 0;
    });
  }

  _incrementCounter() async {
    setState(() {
      _counter++;
    });
    final prefs = await SharedPreferences.getInstance();
    prefs.setInt('counter', _counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Shared Preferences Example')),
      body: Center(
        child: Text('$_counter', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}

Conclusion

In summary, Hydrated Bloc is best suited for apps with complex data structures and heavy state management needs, especially those built using the Bloc pattern. Shared Preferences, on the other hand, is great for storing lightweight data like user preferences and small settings across app sessions. By understanding each option’s strengths, limitations, and ideal use cases, developers can make informed choices that improve the app’s performance and user experience.

Leave a Comment

Your email address will not be published. Required fields are marked *