使用 Hydrated Bloc 版本 5 及更高版本的持久状态

Persisting State using Hydrated Bloc version 5 and above

我正在尝试使用 Hydrated Bloc 在我的应用程序中保持状态。我找到的教程都是使用以前版本的 Hydrated Blocs 并使用 BlocSupervisor,它在 v.5 的 bloc 包的 Dart 版本中被删除(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)。 文档说它应该替换为 BlocObserverhydrated_blocflutter_bloc 没有列出替代品。到目前为止,我还没有找到使用 BlocSupervisorBlocDelegate 以外的任何东西的 HydratedBloc 教程;只有 flutter_bloc 个教程。

如何创建与 BlocObserver 等效的 HydratedBloc 以保持状态?

编辑: 好吧,多亏了你的榜样,我想我现在走上了正确的道路。

这是我的代码的相关部分,类 重命名为:

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

我需要从存储中获取不止一个字符串的数据。我该怎么做?据我所知,我无法组合 JSON 字符串。 非常感谢您的帮助!

更新

因此,如果我理解正确的话,除了能够以您之前所处的任何状态重新启动之外,您正在寻找一个完整的数据库。

我原来的回答是后者。查看您的代码后,我应该澄清您不需要手动调用 toJsonfromJson 方法。每当状态发生变化并覆盖之前存储的任何内容时,这些都会自动调用,并且在应用程序重新启动时将恢复最新状态。

所以您在 fromJson 中返回了 ProgramDataTracSVTState。无论您在应用程序中的什么地方显示 ClassABLoC 应用程序重启时的状态,都应该显示最后的活动状态,而无需设置专门的 classAInitialize 事件来手动调用 fromJson。请参阅我的第一个示例以供参考。

为了为典型的本地数据库存储 VehicleModel 的多个实例(我假设您有一个模型 class 用于存储在 toJson 中的内容,如果不,你应该创建一个),我会创建单独的方法来调用 HydratedBloc.storage.write(key, value)HydratedBloc.storage.read(key) 以从存储中访问你需要的任何车辆。

如果您的所有车辆编号都是唯一的,您可以将其用作访问主存储地图中任何车辆的密钥。

这是一个带有基本 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);
}

已更新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};
  }
}

要检查这一点,您可以在 UI 中添加几辆车并在重启后打印存储的列表

      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'),
            ),

原答案

由于您没有提供任何代码,这里是保存基本 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};
  }
}

基本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'),
            ),
          ],
        ),
      ),
    );
  }
}

每当我更新 testString 时,它会在重启后自动保留。

因此,如果我理解正确的话,除了能够以您之前所处的任何状态重新启动之外,您正在寻找一个完整的数据库。