Flutter Hive backwards compatibility

75 Views Asked by At

I'm using hive for locally caching.

In the application I have an I hive object named user.

class UserModel extends HiveObject implements Copyable<UserModel> {

UserModel({
required this.id,
required this.name,
});

  @HiveField(0)
  String id;
  @HiveField(1)
  String name;
  
factory UserModel.fromJson(Map<String,dynamic> json) => UserModel(
id: json['id'],
name: json['name']
);
}

class UserModelAdapter extends TypeAdapter<UserModel> {
  @override
  final typeId = 1;

  @override
  UserModel read(BinaryReader reader) {
    var numOfFields = reader.readByte();
    var fields = <int, dynamic>{
      for (var i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };

return UserModel(
id: fields[0] as String,
name: fields[1] as String,
);
}
@override
void write (BinaryWriter writer, UserModel obj){

writer
 ..writeByte(2)
 ..writeByte(0)
 ..write(obj.id)
 ..writeByte(1)
 ..write(obj.name)
  }
 }
}

The above user model is a part of app that's now live and its working as expected. Now I wanted to add a new field named favorite and i have modified the userobject as follows

class UserModel extends HiveObject implements Copyable<UserModel> {

UserModel({
required this.id,
required this.name,
required this.favourite
});

  @HiveField(0)
  String id;
  @HiveField(1)
  String name; 
  @HiveField(2)
  List<String> favourite
  
factory UserModel.fromJson(Map<String,dynamic> json) => UserModel(
id: json['id'],
name: json['name'],
favourite :  List<String>.from(
          json["favourite"] ?? [].map((x) => x))
 );
}

class UserModelAdapter extends TypeAdapter<UserModel> {
  @override
  final typeId = 1;

  @override
  UserModel read(BinaryReader reader) {
    var numOfFields = reader.readByte();
    var fields = <int, dynamic>{
      for (var i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };

return UserModel(
id: fields[0] as String,
name: fields[1] as String,
favourite: fields[2] as List<String>
);
}
@override
void write (BinaryWriter writer, UserModel obj){

writer
 ..writeByte(3)
 ..writeByte(0)
 ..write(obj.id)
 ..writeByte(1)
 ..write(obj.name)
 ..writeByte(2)
 ..write(obj.favourite)
  }
 }
}

However After updating the userobject as shown as above by adding new field the new version of the app breaks by throwing error from type adapter. How to ensure adding new fields wont break the app ?

1

There are 1 best solutions below

0
Han Parlak On

I am not sure if this is useful to you after all this time, but for the new-comers to this question, hive_generator version 1.1.0 and above lets you pass a named parameter defaultValue to your @HiveField annotation.

So, on newly created fields one can give a default value like the below:

@HiveField(123, defaultValue: 20)
int someNewField;

This gives someNewField the value 20 if it is null from the cache as a first value.

Custom Adapter

Or, as a second option, you can always create custom adapters for your hive models. This is a bit complex and cumbersome to do but if you came here for an answer, you probably need this kind of solution. This approach is also useful if the type of a field is changed (for some reason).

Assume this is your hive model:

// some_model.dart

part 'some_model.g.dart';

@HiveType(typeId: 1)
class SomeModel {
  SomeModel({
    required this.productionField1,
    required this.productionField2,
    required this.productionField3,
  });
  @HiveField(1)
  int productionField1;
  @HiveField(2)
  double productionField2;
  @HiveField(3)
  bool productionField3;
}

and this is your generated some_model.g.dart file:

// GENERATED CODE - DO NOT MODIFY BY HAND
// `some_model.g.dart`

part of 'some_model.dart';

// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************

class SomeModelAdapter extends TypeAdapter<SomeModel> {
  @override
  final int typeId = 1;

  @override
  SomeModel read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return SomeModel(
      productionField1: fields[1] as int,
      productionField2: fields[2] as double,
      productionField3: fields[3] as bool,
    );
  }

  @override
  void write(BinaryWriter writer, SomeModel obj) {
    writer
      ..writeByte(3)
      ..writeByte(1)
      ..write(obj.productionField1)
      ..writeByte(2)
      ..write(obj.productionField2)
      ..writeByte(3)
      ..write(obj.productionField3);
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is SomeModelAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}

If you want to add someNewField to this class, you can customize the class and adapter like below:

// some_model.dart

part 'custom_some_model_adapter.dart'; // this is now pointing your custom adapter file

@HiveType(typeId: 1)
class SomeModel {
  SomeModel({
    required this.productionField1,
    required this.productionField2,
    required this.productionField3,
    required this.someNewField,
  });
  @HiveField(1)
  int productionField1;
  @HiveField(2)
  double productionField2;
  @HiveField(3)
  bool productionField3;
  @HiveField(4)
  int someNewField;
}
// custom_some_model_adapter.dart

part of 'some_model.dart';

class CustomSomeModelAdapter extends TypeAdapter<SomeModel> {
  @override
  final int typeId = 1;

  @override
  SomeModel read(BinaryReader reader) {
    final numOfFields = reader.readByte();
    final fields = <int, dynamic>{
      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
    };
    return SomeModel(
      productionField1: fields[1] as int,
      productionField2: fields[2] as double,
      productionField3: fields[3] as bool,
      someNewField: fields[4] as int? ?? 100, // this, basically does what the defaultValue parameter provides
    );
  }

  @override
  void write(BinaryWriter writer, SomeModel obj) {
    writer
      ..writeByte(4)
      ..writeByte(1)
      ..write(obj.productionField1)
      ..writeByte(2)
      ..write(obj.productionField2)
      ..writeByte(3)
      ..write(obj.productionField3)
      ..writeByte(4)            // these two lines will ensure new values 
      ..write(obj.someNewField) // that are given to someNewField will persist on cache 
  }

  @override
  int get hashCode => typeId.hashCode;

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is CustomSomeModelAdapter &&
          runtimeType == other.runtimeType &&
          typeId == other.typeId;
}

All there is left to do is to register CustomSomeModelAdapter to hive:

  Hive.registerAdapter<SomeModel>(CustomSomeModelAdapter());