Flutter State Restoration with Bloc: A Practical Guide

This article addresses the common issue of restoring state in Flutter applications that utilize the Bloc pattern, specifically when the app is killed by the system. It provides a practical solution and discusses potential pitfalls.

Understanding the Problem

Flutter’s state restoration mechanism is designed for StatefulWidgets. However, when integrating with the Bloc pattern, particularly when using HydratedBloc for persistence, issues arise due to the lack of a direct, standardized way to restore the Bloc’s state.

The Solution: A Custom RestorableBloc

A tailored solution is to create a RestorableBloc interface that allows for state restoration while minimizing the need for full persistence.

Step 1: Define the RestorableBloc


abstract class RestorableBloc extends Bloc {
  RestorableBloc({
    required this.restorationId,
    required this.restorationBucket,
  }) : super(initialState);

  final String restorationId;
  final RestorationBucket restorationBucket;

  @override
  Stream mapEventToState(Event event) async* {
    // ... (your event handling logic)
    yield state; // Important: yield the current state.
  }

  Map _serializeState(State state) {
    // Implement serialization logic to convert state to Map
    // Example:
    return {
      'property1': state.property1,
      'property2': state.property2,
    };
  }

  State _deserializeState(Map serializedState) {
    // Implement deserialization logic
    // Example:
    return State(
      property1: serializedState['property1'] as String,
      property2: serializedState['property2'] as int,
    );
  }
}

Step 2: Implement Serialization and Deserialization

Crucially, you need to define methods that convert your Bloc state to a Map for serialization and back for deserialization.

Step 3: Using the RestorableBloc in your Application


// Example Usage
class MyBloc extends RestorableBloc {
  MyBloc({
    required String restorationId,
    required RestorationBucket restorationBucket,
  }) : super(
    restorationId: restorationId,
    restorationBucket: restorationBucket,
    initialState: MyState(property1: 'Initial Value', property2: 0)
  );

  @override
  Map _serializeState(MyState state) => {
      'property1': state.property1,
      'property2': state.property2,
    };


  @override
  MyState _deserializeState(Map serializedState) => MyState(
        property1: serializedState['property1'] as String,
        property2: serializedState['property2'] as int,
      );
}


Handling the RestorationBucket

You need to obtain the RestorationBucket. The most common method is by using RestorationScope.maybeOf(context) within the BlocProvider‘s create method.


BlocProvider.value(
    value: MyBloc(
    restorationId: 'myBloc',
    restorationBucket: RestorationScope.maybeOf(context)!,
    ),
    child: ...
);

Potential Errors and Solutions

  • Error: _serializeState or _deserializeState throws an exception.

    Solution: Ensure your serialization and deserialization logic correctly handles data types in your MyState class. Add proper type casting and validation.
  • Error: Error while accessing the RestorationBucket.

    Solution: Double-check that RestorationScope.maybeOf(context) is called inside the BlocProvider‘s create method. Also, verify the context is correct and available at that point in your widget tree.
  • Error: Warning about using a visibleForTesting function.

    Solution: Remove the `// ignore: invalid_use_of_visible_for_testing_member` comment. If you truly need this function in a non-testing context, consult the Bloc documentation on how to make it available.

Key Considerations

  • Persistence Scope: This approach prioritizes restoration for specific Blocs if your app is killed. Full state persistence is usually not the goal in a similar situation.
  • Error Handling: Implement robust error handling in _serializeState and _deserializeState to gracefully manage potential exceptions. Use try-catch blocks.

By following this approach, you can effectively restore the state of your Blocs in Flutter applications, even if the app is terminated by the operating system, providing a more robust user experience.