Search code examples
flutterdartbloc

Persisting State using Hydrated Bloc version 5 and above


I am trying to use Hydrated Bloc to persist the state in my app. The tutorials I have found are all using previous versions of Hydrated Blocs and use the BlocSupervisor, which was removed in the Dart version of the bloc package in v.5 (https://pub.dev/packages/bloc/changelog). flutter_bloc and hydrated_bloc removed it when they updated to bloc v.5 (https://pub.dev/packages/flutter_bloc/changelog and https://pub.dev/packages/hydrated_bloc/changelog). The documentation says it should be replaced with the BlocObserver. hydrated_bloc and flutter_bloc do not list a replacement. So far, I have not found a HydratedBloc tutorial that uses anything other than BlocSupervisor and BlocDelegate; only flutter_bloc tutorials.

How do I create the HydratedBloc equivalent of the BlocObserver in order to persist state?

EDIT: Okay, I think I'm on the right track now thanks to your example.

Here is the relevant portion of my code, with the classes renamed:

class ClassABLoC
    extends HydratedBloc<ClassAEvent, ClassAState> {
//This does initialize it with some values, but not the ones from storage. Trying to call fromJson() there gives an error. 
 ClassABLoC() : super(ClassAState.dataNotReceived());
@override
  Stream<classAState> mapEventToState(
      classAEvent event) async* {

    if (event is classAInitialize){
      print("Initializing");
      yield fromJson(json.decode(
        HydratedBloc.storage.read("vehicleNumber") as String,
        ) as Map<String, dynamic>,
      );
    }

    if (event is classAValidate) {
      yield classAState.validated(
        theEvent: event,
        numberValidated: validateTheInput(event.numberVal, event.vehicleNumber),
        distanceValidated:
            validateTheInput(event.distanceVal, event.vehicleDistanceTraveled),
        yearValidated: validateTheInput(event.yearVal, event.vehicleYear),
        vinValidated: validateTheInput(event.vinVal, event.vin),
        licensePlateValidated:
            validateTheInput(event.licensePlateVal, event.vehicleLicensePlate),
        revsPerDistValidated:
            validateTheInput(event.revsPerDistVal, event.vehicleRevsPerDist),
        fuelTypeValidated: validateTheInput(event.fuelTypeVal, event.fuelType),
        fuelCapacityValidated:
            validateTheInput(event.fuelCapacityVal, event.fuelCapacity),
        siteValidated: validateTheInput(event.siteVal, event.vehicleSite),
        numberError: event.numberVal!.validationFailedMsg,
        distanceError: event.distanceVal!.validationFailedMsg,
        yearError: event.yearVal!.validationFailedMsg,
        vinError: event.vinVal!.validationFailedMsg,
        licensePlateError: event.licensePlateVal!.validationFailedMsg,
        revsPerDistError: event.revsPerDistVal!.validationFailedMsg,
        fuelTypeError: event.fuelTypeVal!.validationFailedMsg,
        fuelCapacityError: event.fuelCapacityVal!.validationFailedMsg,
        siteError: event.siteVal!.validationFailedMsg,
      );
    } else if (event is StoreDataEvent) {
      toJson(StoreDataState(
        vehicleNumber: event.vehicleNumber,
        vehicleYear: event.vehicleYear,
        vehicleRevsPerDist: event.vehicleRevsPerDist,
        vehicleDistanceTraveled: event.vehicleDistanceTraveled,
        vin: event.vin,
        vehicleLicensePlate: event.vehicleLicensePlate,
        fuelCapacity: event.fuelCapacity,
        fuelType: event.fuelType,
        vehicleSite: event.vehicleSite,
      ));
    }
  }

Map<String, dynamic>? toJson(ProgramDataTracSVTState state) {
    if (state is StoreDataState) {
      print("State was StoreDataState.");
      print(state.toString());
      return {
        'vehicleNumber': state.vehicleNumber,
        'vehicleDistanceTraveled': state.vehicleDistanceTraveled,
        'vehicleLicensePlate': state.vehicleLicensePlate,
        'vin': state.vin,
        'rehicleRevsPerDist': state.vehicleRevsPerDist,
        'vehicleSite': state.vehicleSite,
        'vehicleYear': state.vehicleYear,
        'fuelCapacity': state.fuelCapacity,
        'fuelType': state.fuelType,
        'distUnit': state.distUnits,
      };
    }
  }


ProgramDataTracSVTState fromJson(Map<String, dynamic> json) {
    print(json['vehicleDistance'] as String);
    return ProgramDataTracSVTState(
      vehicleNumber: (json['vehicleNumber'] as String?) == "" ||
              json['vehicleNumber'] == null
          ? "HARDCODED"
          : json['vehicleNumber'] as String,
      vehicleDistanceTraveled:
          (json['vehicleDistanceTraveled'] as String?) == "" ||
                  (json['vehicleDistanceTraveled'] as String?) == null
              ? "HARDCODED VALUE"
              : json['vehicleDistanceTraveled'] as String,
      vehicleLicensePlate: (json['vehicleLicensePlate'] as String?) == "" ||
              (json['vehicleLicensePlate'] as String?) == null
          ? ""
          : json['vehicleLicensePlate'] as String,
      vin: (json['vin'] as String?) == "" || (json['vin'] as String?) == null
          ? "HARDCODED VALUE"
          : json['vin'] as String,
      vehicleRevsPerDist: (json['vehicleRevsPerDist'] as String?) == "" ||
              (json['vehicleRevsPerDist'] as String?) == null
          ? "500"
          : json['vehicleRevsPerDist'] as String,
      vehicleSite: (json['vehicleSite'] as String?) == "" ||
              (json['vehicleSite'] as String?) == null
          ? "Home Base Site"
          : (json['vehicleSite'] as String),
      vehicleYear: (json['vehicleYear'] as String?) == "" ||
              (json['vehicleYear'] as String?) == null
          ? "2018"
          : json['vehicleYear'] as String,
      fuelCapacity: (json['fuelCapacity'] as String?) == "" ||
              (json['fuelCapacity'] as String?) == null
          ? ""
          : (json['fuelCapacity'] as String),
      fuelType: (json['fuelType'] as String?) == "" ||
              (json['fuelType'] as String?) == null
          ? "Diesel"
          : (json['fuelType'] as String),
      distUnits: (json['distUnit'] as String?) == "" ||
              (json['distUnit'] as String?) == null
          ? "None"
          : (json['distUnit'] as String),
    );
  }

I need to get more than one string's worth of data from the storage. How would I do that? I can't combine the JSON strings as far as I know.


Solution

  • Update

    So if I understand correctly you're looking for a full on database in addition to just being able to restart in whatever previous state you were in.

    My original answer shows the latter. After looking at your code I should clarify that you don't need to manually call the toJson and fromJson methods. Those get called automatically any time the state changes and it overwrites whatever was stored previously, and on app restart will restore the most recent state.

    So you're returning a ProgramDataTracSVTState in your fromJson. Wherever in the app that you're displaying that state of ClassABLoC on app restart, should display the last active state without you setting up a dedicated classAInitialize event just to manually call fromJson. See my first example for reference on that.

    In order to store multiple instances of your VehicleModel for a typical local database (I assume you have a model class for what you're storing in your toJson, if not, you should create one), I would create separate methods that calls HydratedBloc.storage.write(key, value) and HydratedBloc.storage.read(key)to access whichever vehicle you need from storage.

    If all your vehicle numbers are unique you could use that as a key to access any vehicle in the main storage map.

    Here's a simplified example with a basic CarModel

    class CarModel {
      final int id;
      final String car;
    
      CarModel({required this.id, required this.car});
    
      CarModel.fromJson(Map<String, dynamic> map)
          : id = map['id'],
            car = map['car'];
    
      Map<String, dynamic> toJson() {
        return {
          'id': id,
          'car': car,
        };
      }
    }
    

    Events

    class PrintStorageData extends TestEvent {}
    
    class StoreDataEvent extends TestEvent {
      final CarModel car;
    
      StoreDataEvent({required this.car});
    }
    

    states

    abstract class TestState extends Equatable {
      final CarModel testModel;
    
      const TestState(this.testModel);
    
      @override
      List<Object> get props => [testModel];
    }
    
    class State1 extends TestState {
      State1({required CarModel model}) : super(model);
    }
    

    Updated TestBloc

    class TestBloc extends HydratedBloc<TestEvent, TestState> {
      TestBloc()
          : super(State1(model: CarModel(id: 0, car: 'no cars listed'))) {
        on<PrintStorageData>(
          (event, emit) => _printStorageData(),
        );
    
        on<StoreDataEvent>((event, emit) {
          emit(State1(model: event.car)); // toJson gets called here and fromJson on app restart
    
          _storeCarModel(event.car); // this is what stores in a database you can access later on
        });
      }
    
      Future<void> _printStorageData() async {
        final mapFromStorage =
            await HydratedBloc.storage.read('vehicles') as Map? ?? {};
    
        if (mapFromStorage.isNotEmpty) {
          mapFromStorage.forEach((key, value) {
            log('$key: $value');
          });
        } else {
          log('No vehicles stored');
        }
      }
    
      Future<void> _storeCarModel(CarModel model) async {
        final id = model.id; // using id for storage key
        final storageMap = await HydratedBloc.storage.read('vehicles') as Map? ??
            {}; // initializing to empty map if nothing is stored
        storageMap[id] = model.toJson();
        await HydratedBloc.storage.write('vehicles', storageMap);
      }
    
      @override
      TestState fromJson(Map<String, dynamic>? json) {
        return State1(model: CarModel.fromJson(json!));
      }
    
      @override
      Map<String, dynamic>? toJson(TestState state) {
        return {'id': state.testModel.id, 'car': state.testModel.car};
      }
    }
    

    To check this you can add a few cars in the UI and print the stored list after restart

          ElevatedButton(
                  onPressed: () => context.read<TestBloc>().add(PrintStorageData()),
                  child: Text('Read Data'),
                ),
                ElevatedButton(
                  onPressed: () {
                    final car = CarModel(id: 1, car: 'tesla');
                    context.read<TestBloc>().add(StoreDataEvent(car: car));
                  },
                  child: Text('Store new car'),
                ),
    

    Original Answer

    Since you didn't provide any of your code here's an example of saving a basic String.

    State

    abstract class TestState extends Equatable {
      final String testString;
    
      const TestState(this.testString);
    
      @override
      List<Object> get props => [testString];
    }
    
    class State1 extends TestState {
      State1({required String testString}) : super(testString);
    }
    

    Events

    abstract class TestEvent {}
    
    class ChangeStringToSaved extends TestEvent {}
    
    class UpdateSaved extends TestEvent {}
    

    HydradedBloc

    class TestBloc extends HydratedBloc<TestEvent, TestState> {
      TestBloc() : super(State1(testString: 'not saved')) {
        on<ChangeStringToSaved>(
            (event, emit) => emit(State1(testString: 'this is saved')));
        on<UpdateSaved>(
            (event, emit) => emit(State1(testString: 'updated saved value')));
      }
    // Creating State1 from stored Map
      @override
      TestState? fromJson(Map<String, dynamic> json) {
        return State1(testString: json['value'] as String);
      }
    // Saving to a basic Map
      @override
      Map<String, dynamic>? toJson(TestState state) {
        return {'value': state.testString};
      }
    }
    

    Basic UI

    class Home extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                BlocConsumer<TestBloc, TestState>(
                    listener: (context, state) {},
                    builder: (context, state) {
                      return Text(state.testString);
                    }),
                ElevatedButton(
                  onPressed: () =>
                      context.read<TestBloc>().add(ChangeStringToSaved()),
                  child: Text('Change to Saved'),
                ),
                ElevatedButton(
                  onPressed: () => context.read<TestBloc>().add(UpdateSaved()),
                  child: Text('Update Saved'),
                ),
              ],
            ),
          ),
        );
      }
    }
    

    Anytime I update the testString, its automatically persisted after a restart.

    So if I understand correctly you're looking for a full on database in addition to just being able to restart in whatever previous state you were in.

    enter image description here