Offline data persistence in Flutter with Hive

Offline data persistence in Flutter with Hive

Introduction

Offline mode is a crucial requirement for almost every app, from e-commerce apps to social media apps. This involves storing data locally on a user's device for later use.

For example:

  • Current logged-in user information like username, email, profile avatar URL etc.

  • REST API returned data like a list of products, feeds, or articles.

  • Cart items.

There are several options available to persist data locally like SharedPreference, SQLite, Room etc.

In this article, we will explore what Hive is and how to persist data in Flutter apps using Hive.

Hive

Hive is a lightweight, fast key-value, NoSQL database written in pure Dart.

Hive has a high benchmark, it has no native dependencies as it is written in Dart. It also supports all platforms supported by Flutter.

A NoSQL database (aka "not only SQL") is a non-relational database that stores data in an unstructured form without following a fixed schema.

If you need to model your data with many relationships, in that case, it is recommended to use SQLite.

Let's get started by adding the hive_flutter dependency to our pubspec.yaml

dependencies:
  hive_flutter: ^1.1.0

Create a DatabaseService class and add initializeDatabase() method. Inside this initializeDatabase() method, we are going to initialize Hive.

class DatabaseService {
  Future<void> initializeDatabase() async {
    await Hive.initFlutter();
  }
}

Boxes

Hive stores its data in boxes containing key-value sets. Hence, we can open a box to store user details and another box to store a list of cart items.

Open box

Before accessing a box, we need to first open it.

await Hive.openBox('testBox');
await Hive.openBox<DataModel>('dataModelBox');

Get the opened box

var testBox = Hive.box('testBox');
var dataModelBox = Hive.box<DataModel>('dataModelBox');

Write to the opened box

testBox.put("email", "johndoe@gmail.com");
testBox.put("username", "johndoe");

Read from the opened box

If the key is not present in the box, null will be returned. Hence, we can use Dart Null safety feature to make our email and username variables nullable. By default, Dart variables are non-nullable, and passing null value to a non-nullable variable will crash our app. Optionally, the box get method has a named parameter called defaultValue, where we can pass a default value.

Null safety prevents errors that result from unintentional access of variables set to null.

String? email = testBox.get("email");
String? username = testBox.get("username");

String email = testBox.get("email", defaultValue: "");
String username = testBox.get("username", defaultValue: "");

Type Adapters

Hive supports all primitive types, String, int, List, Map, DateTime and Uint8List. To store objects, we need to register a TypeAdapter which converts the object from and to binary form.

Create a user model class

import 'package:hive_flutter/hive_flutter.dart';

part 'user.g.dart';

@HiveType(typeId: 0)
class User {
  @HiveField(0)
  String userId;
  @HiveField(1)
  String username;
  @HiveField(2)
  String email;
  @HiveField(3)
  String photoUrl;
  @HiveField(4)
  String bio;

  User(this.userId, this.username, this.email, this.photoUrl, this.bio);

  @override
  String toString() {
    return 'User(userId: $userId, username: $username, email: $email, photoUrl: $photoUrl, bio: $bio)';
  }
}

In the User class above, we imported the hive_flutter package; also, we annotated our User data class with the @HiveType and provided a typeId.

The part "user.g.dart"; will be generated automatically for us.

Add the following dependencies below to your pubspec.yaml We are going to use these dependencies to auto-generate some classes later on.

dev_dependencies:
  hive_generator: ^1.1.0
  build_runner: ^2.1.11

Run the build runner command below to auto-generate a UserAdapter class inside a file called user.g.dart.

flutter pub run build_runner build --delete-conflicting-outputs

Register Adapter

String boxName = "UserBox";
class DatabaseService {
  Future<void> initializeDatabase() async {
    // ...
    await Hive.openBox<User>(boxName);
    Hive.registerAdapter<User>(UserAdapter());
  }
}

Get data

Here we just check if the box contains data, we return the first item else we just return null.

class DatabaseService {
  late Box<User> userBox;
  Future<void> initializeDatabase() async {
    //...
    userBox = Hive.box(boxName);
  }

  User? read() {
    return userBox.values.isNotEmpty
        ? userBox.values.first
        : null;
  }
}

Insert or update data

Here, we first clear the box by calling userBox.clear() before inserting the updated user object. This is to ensure that we only have one user object in the box. I.e., we can only have one logged-in user at a time.

class DatabaseService {
  //...
  Future<void> insertOrUpdate(User user) async {
    await userBox.clear();
    await userBox.add(user);
  }
}

Delete data

class DatabaseService {
  //...
  Future<void> delete() async {
    await userBox.clear();
  }
}

We are going to use the get_it package to register and retrieve an object of our DatabaseService class.

The get_it package is a service locator for Dart and Flutter applications. This package allows you to register and retrieve objects (Services) from anywhere within your app and also makes it easier to manage dependencies and decouple different parts of your code.

dependencies:
  get_it: ^7.6.0

Create a file called locator.dart. Inside this file, add a function called setupLocator() and register the DatabaseService class as a Singleton

import 'package:get_it/get_it.dart';
GetIt locator = GetIt.instance;

setupLocator() {
  locator.registerSingleton<DatabaseService>(
    DatabaseService(),
  );
}

Inside the main() method, call setupLocator(); to register the services with get_it.

We are going to use locator<DatabaseService>() to get the instance of DatabaseService class and call the initializeDatabase() to initialize our Hive Database and open our UserBox

Future<void> main() async {
  setupLocator();
  await locator<DatabaseService>().initializeDatabase();
  runApp(const MyApp());
}

We can play around with the Hive Database by inserting, updating, reading, and deleting data.

// Retrieve DatabaseService object
DatabaseService databaseService = locator<DatabaseService>();
// Create a user object
User user =
    User("1", "johndoe", "johndoe@jd.com", "https://cdn.pixabay.com/photo/2021/10/24/21/34/profile-pic-6739366_1280.png", "Software Engineer");
// Insert user object into the database
await databaseService.insertOrUpdate(user);
// Read stored user object
User? dbUser = databaseService.read();

// Update current user username
user.username = "johndoe.eth";
await databaseService.insertOrUpdate(user);

// Delete current user data (E.g When a user logs out)
await databaseService.delete();

Conclusion

This article provided a comprehensive overview of Hive, a popular NoSQL database solution for Flutter applications. We discussed the process of persisting data in Flutter apps using Hive and also explored various CRUD operations, including creating, reading, updating and deleting data in Hive.

You can consider using Hive in your next Flutter project due to its lightweight nature and support for key-value pairs and complex data structures. Additionally, Hive's compatibility with both Android and iOS platforms ensures cross-platform support.

Thank you for reading.