Chosen Local Database Library
Hive is a No-SQL lightweight and blazing fast key-value database written in pure Dart.
Refer Local storage comparison for reason behind choosing Hive.
Refer Hive Library for more details.
Introduction to Hive
Questions | Definition |
---|---|
What’s a Box ? | All data stored in Hive is organised in boxes. |
Where is data stored ? | File is generated for each box. Eg: |
How to initiate Hive ? | Hive.init(path); All box files will be created in this location. |
How to delete all data ? | We have to delete all the box files and close the Hive await Hive.deleteFromDisk(); await Hive.close(); |
Hive Operations |
|
Steps to insert non-primitive data in Hive |
|
What’s HiveType ? & How are each parameter’s stored ? | A non primitive data type is created using Hive annotations. We have to register @HiveType(typeId:0) class SampleTable { @HiveField(0) final String name; @HiveField(1) final int age; SampleTable(this.name, this.age); } |
What's HiveField ? | As Hive stores everything in File, it needs each parameter to be registered for writing and reading. We have to set |
Why & How to generate Adapter for a HiveType ? | Hive curd operations work on top of Hive adapters. Command to generate adapters If
Eg: It automatically generated read and write functionality. class SampleTableAdapter extends TypeAdapter<SampleTable> { @override int get typeId => 0; @override void write(BinaryWriter writer, SampleTable obj) {...} @override SampleTable read(BinaryReader reader) {...} } |
How to register an Adapter ? | Hive.registerAdapter(SampleTableAdapter); |
How to open a box ? | Hive expects box name to open a box. Box box = await Hive.openBox( boxName, compactionStrategy: (int total, int deleted) { return deleted > 20; }, ); |
What’s Compaction ? | When you updated and deleted data is written at the end of the box file which leads to a growing box file. Compaction is a strategy to compact a box. |
What’s compactionStrategy in openBox ? | we can invoke compaction automatically after every |
Functionalities
We are storing list of items in Local database.
Eg: List of contacts, list of language reference, list of biller, etc.
Required in our application
Functions | Code | Use-cases |
Get item by key |
| Get one biller by biller code. biller = box.get(billerCode); |
Get list of items |
| Get list of billers billers = box.toMap().values.toList(); |
Insert/Update all |
|
|
Delete by key |
| Delete a biller await box.delete(billerCode); |
Wrapper for CRUD operations - BaseLocalDataSource
Abstract class takes 2 types.
First datatype is used for storing value. [The key is stricted to be String]
Second datatype is the model from which the table is extended from.
This is used for a overriden function that takes inout as list of model datatype and converts to list of table datatype to insert into database. This class contains basic functions such as get, getAll, insertOrUpdateAll, delete, deleteAll.
Initiate the class with box name. Every extended class should register the adapters in the constructor.
ContactsLocalDataSource() : super(boxName: 'contacts') { DatabaseUtil.registerAdapter<ContactTable>(ContactTableAdapter()); DatabaseUtil.registerAdapter<AccountContactTable>( AccountContactTableAdapter()); }
It has 5 functions implemented
Future<TableType> get(String key) async { final Box<TableType> box = await boxInstance; return box.get(key); } Future<List<TableType>> getAll() async { final Box<TableType> box = await boxInstance; return box.toMap().values.toList(); } Future<void> putAll(Map<String, TableType> items) async { final Box<TableType> box = await boxInstance; await box.putAll(items); } Future<void> delete(String key) async { final Box<TableType> box = await boxInstance; await box.delete(key); } Future<void> deleteAll() async { final Box<TableType> box = await boxInstance; await box.deleteAll(box.toMap().keys.toList()); }
It has 3 Overridden members
Future<ModelType> getFormattedItem(String key);
Usecase for converting table item to a model
Future<ContactsItemModel> getFormattedItem(String key) async { final ContactTable contact = await get(key); if (contact == null) { return null; } return ContactsItemModel.fromContactTable(contact); }
Future<List<ModelType>> getformattedData();
Usecase for converting list of table item to a list of models
Future<List<ContactsItemModel>> getformattedData() async { final List<ContactTable> contacts = await getAll(); return contacts .map((contact) => ContactsItemModel.fromContactTable(contact)) .toList(); }
Future<void> insertOrUpdateItems(List<ModelType> contacts);
Usecase for converting list of models to a map which contains key which contains the primary key and value which contains the data belonging to table class.
Future<void> insertOrUpdateItems(List<ContactsItemModel> contacts) async { final Map<String, ContactTable> contactMap = { for (var contact in contacts) contact.contactId: ContactTable.fromModel(contact) }; await putAll(contactMap); }
Example of Implementation of Contacts
Steps:
Create HiveType for Contact and AccountInContact.
Include a part `contact_account_table.g.dart` if yourlib/data/datasources/local/databases/tables/contact_table.dart
lib/data/datasources/local/databases/tables/contact_account_table.dartpart 'contact.g.dart'; // is for generating a graph @HiveType(typeId: 0) class ContactTable extends ContactsItemModel { @HiveField(10) List<AccountContactTable> accountTableList; }
part 'contact_account.g.dart'; // is for generating a graph @HiveType(typeId: 1) class AccountContactTable extends AccountInContactsModel { }
Instead of creating parameter inContactTable
for all fields (contactId, contactName, …).
Since its extended fromContactsItemModel
which is extended fromContactsItemEntity
.Therefore, we can define the fields in
ContactsItemEntity
class.class ContactsItemEntity { @HiveField(1) bool header; @HiveField(2) bool headerResult; @HiveField(3) String contactId; ... } class AccountInContactsEntity { @HiveField(0) String accountId; @HiveField(1) String identifier; @HiveField(2) String name; @HiveField(3) String username; ... }
Auto generate the dependency graph code for the application
flutter packages pub run build_runner build --delete-conflicting-outputs
Note: `--delete-conflicting-outputs` is optional to override the conflict graph.
You will get a dependency graph generated for the tables with adapter class called
ContactTableAdapter
&AccountContactTableAdapter
with the below file names.lib/data/datasources/local/databases/tables/contact_table.g.dart
lib/data/datasources/local/databases/tables/contact_account_table.g.dart
(You can change that name with the optional
adapterName
parameter of@HiveType
)Create a local datasource repository with
Box name - contacts
register adapters in constructor
overridden functionsclass ContactsLocalDataSource extends BaseLocalDataSource<ContactTable, ContactsItemModel> { ContactsLocalDataSource() : super(boxName: 'contacts') { DatabaseUtil.registerAdapter<ContactTable>(ContactTableAdapter()); DatabaseUtil.registerAdapter<AccountContactTable>( AccountContactTableAdapter()); } @override Future<ContactsItemModel> getFormattedItem(String key) async { final ContactTable contact = await get(key); if (contact == null) { return null; } return ContactsItemModel.fromContactTable(contact); } @override Future<List<ContactsItemModel>> getformattedData() async { final List<ContactTable> contacts = await getAll(); return contacts .map((contact) => ContactsItemModel.fromContactTable(contact)) .toList(); } @override Future<void> insertOrUpdateItems(List<ContactsItemModel> contacts) async { final Map<String, ContactTable> contactMap = { for (var contact in contacts) contact.contactId: ContactTable.fromModel(contact) }; await putAll(contactMap); } }
Use these function in repository implementation level.
Check if data exist in local storage,
if empty, then request from network and save data into local databaseFuture<ContactsModel> getContactsItems(String cif) async { // Check local database List<ContactsItemModel> contactsItems = await contactsLocalDataSource.getformattedData(); if (contactsItems.isEmpty) { // If empty, then call the network and store in local database contactsItems = await contactsRemoteDataSource.getContactsItems(cif); await contactsLocalDataSource.insertOrUpdateItems(contactsItems); } return ContactsModel(contactsItems: contactsItems); }