In today’s always online world, there are still lots of use cases for storing data offline; especially when it comes to mobile development, but which database should you use?
In Flutter, you have an array of options to pick from if you need an offline database solution. Which option you ultimately go for is up to you, but there are factors that affect this decision (aside from the unique features they might provide) that typically boil down to how simple it is to get started and their speed, both of which we will cover in this article.
In this blog post, we’ll be looking at how the setup works for Hive and other databases, and compare and contrast basic functionalities every database will be expected to feature (specifically CRUD, i.e., create, read, update, delete).
We’ll do some benchmarking to determine how long it takes each database option to perform these operations, so you can have a clear understanding of which database option is right for your Flutter app project, Hive or otherwise.
Let’s get started.
The CRUD operations referred to previously will be carried out using a User
object, i.e., writing n number of user objects to the database, etc. This is what the user object would look like:
class UserModel { final int id; final DateTime createdAt; final String username; final String email; final int age; UserModel(...); factory UserModel.fromMap(Map<String, dynamic> map) { return UserModel(...); } Map<String, dynamic> toMap() { return {...}; } }
To make use of an offline database in an application, the first thing to do is to get the location of the app directory which is where the database will be stored. The Flutter path_provider
and path
packages can be used to easily retrieve this information:
import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; final dir = await getApplicationDocumentsDirectory(); // For hive, isar and objectbox final dbPath = dir.path; // For sembast and sqflite final dbPath = path.join(dir.path, 'databaseName.db');
Hive is a key-value database written in pure Dart, with no native dependencies. This makes it truly cross-platform, as it is able to run on all platforms that support the Dart programming language.
Initializing the database is usually done immediately once the application launches. Once you have the path to where the database should be stored, initializing the Hive database is as easy as calling:
Hive.init(dbPath);
To perform CRUD operations using Hive, an instance of a Box
is needed.
final box = await Hive.openBox('users');
To add/update a user to the database (Create/Update), you have to provide a unique key and a value to the put()
method:
final user = UserModel(...); await box.put(user.id, user.toMap());
To get a user from the database (Read), you simply request the user using the key provided, like so:
final userMap = box.get(user.id); return UserModel.fromMap(userMap);
To delete a user from the database (Delete), you pass the key to the delete
method.
final user = UserModel(...); await box.delete(user.id);
Sembast is a NoSQL database that makes use of a text document where each entry is a JSON object. Sembast makes it easy to build reactive apps, as you can listen and trigger certain actions when a document changes.
With the database path ready, you can initialize the database by calling:
final db = await databaseFactoryIo.openDatabase(dbPath);
To perform CRUD operations using Sembast, an instance of a StoreRef
is needed:
final store = StoreRef('users');
To add/update a user to the database, you have to pass a key to the record
function and chain the put
method, which takes the database and value as parameters:
final user = UserModel(...); await store.record(user.id).put(db, user.toMap());
To get a user from the database, you must pass the key to record
and chain the get
method:
final userMap = await store.record(user.id).get(db); return UserModel.fromMap(userMap);
To delete a user from the database, you pass the key to record
and chain the delete
method:
final user = UserModel(...); await store.record(user.id).delete(db);
Sqflite is a SQL database that provides the ability to write raw SQL commands, which are quite powerful when you know what to do. You also have the option to make use of helper functions.
With the database path ready, initialize the database and create a table to store users by doing the following:
static const String USER_TABLE = "users"; final db = await openDatabase( dbPath, onCreate: (db, version) async { await db.execute( 'CREATE TABLE $USER_TABLE (id TEXT PRIMARY KEY, createdAt TEXT, username TEXT, email TEXT, age INTEGER)', ); }, version: 1, );
To add/update a user to the database, you have to pass the table name and value to be inserted to the insert
or update
helper functions.
final user = UserModel(...); await db.insert(USER_TABLE, user.toMap()); // to update the user defined above await db.update(USER_TABLE, user.toMap(), where: "id = ?", whereArgs: [user.id]);
To get a user from the database, you can use a query string to search it. This will return a list of matches, along with a user ID that is unique; it should return a Map containing only one item:
final users = await db.query(USER_TABLE, where: "id = ?", whereArgs: [user.id]); return UserModel.fromMap(users.first);
To delete a user from the database, you pass to the delete
method, the table name, and a query string to identify the user to be deleted:
final user = UserModel(...); await db.delete(USER_TABLE, where: "id = ?", whereArgs: [user.id]);
To continue with Isar and ObjectBox, we’ll need more dependencies for the project, as they both use type annotations and code generation, which enables you to read/write Dart objects directly to the database (Hive also provides this as an option).
Isar is a feature-rich offline database with powerful queries, filters, and sort functionalities.
You can also build very reactive apps with it, as it is able to listen to data changes. With Isar, this is what the dependencies and model classes would look like:
dependencies: ... isar: $latest isar_flutter_libs: $latest dev_dependencies: ... build_runner: $latest isar_generator: $latest
import 'package:isar/isar.dart'; part 'isar_user.g.dart'; @Collection() // Anotate the user model using Collection() class UserModel { int id; ... UserModel(...); }
To get the autogenerated code, simply run flutter pub run build_runner build
.
With the database path ready, initialize the database by calling:
final isar = await Isar.open( schemas: [UserModelSchema], // A list of anotated collections directory: dbPath, );
To add/update a user to the database, you have to pass the user model directly to the put
method:
final user = UserModel(...); isar.writeTxn((isar) async { await isar.isarUserModels.put(user); }
To get a user from the database, you use the get
method, which takes the id
of the user:
final user = await isar.isarUserModels.get(user.id);
To delete a user from the database, you use the delete
method, which takes the id
of the user:
isar.writeTxn((isar) async { await isar.isarUserModels.delete(user.id); }
ObjectBox is a NoSQL database written with pure Dart. With ObjectBox, this is what the dependencies and model classes look like:
dependencies: ... objectbox: $latest objectbox_flutter_libs: $latest dev_dependencies: ... build_runner: $latest objectbox_generator: $latest
@Entity() // Anotate the user model using Entity() class UserModel { int id; ... UserModel(...); }
To get the autogenerated code simply run flutter pub run build_runner build
, exactly the same as with Isar. With the database path ready, you can initialize the database by calling:
final store = await openStore(directory: dbPath); final box = store.box<UserModel>();
To add/update a user to the database, you have to pass the UserModel
directly to the put
method:
final user = UserModel(...); box.put(user);;
To get a user from the database, you use the get
method, which takes the id
of the user:
final user = box.get(user.id);
To delete a user from the database, you use the remove
method, which takes the id
of the user;
box.remove(user.id);
Firstly, we’ll be looking at how long it takes each database option to perform n number of writes, reads, and deletes.
Secondly, seeing as some of these database options (Hive, Isar, and ObjectBox) have more optimized ways to perform CRUD operations — like writing, reading, or deleting multiple items using a single method — we’ll also look at how long it takes to write, read, and delete n number of users from the database.
final users = [UserModel(...), UserModel(...), UserModel(...)]; // Hive box.putAll(data), .deleteAll(userIds) // Isar isar.isarUserModels.putAll(users), .getAll(userIds), .deleteAll(userIds) // Objectbox box.putMany(users), .getMany(userIds), .removeMany(userIds)
Thirdly, because of the variation on different device operating systems, the benchmark will be done on both Android and iOS physical devices, while running in release mode for optimal performance.
The following benchmark results were received while running the project in release mode on an iPhone 12 running iOS v15.6.
Below are the results (time in milliseconds) from 10 consecutive runs when 1,000 individual write, read, and delete operations were performed.
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Isar | 65 | 73 | 68 | 68 | 71 | 69 | 73 | 69 | 68 | 70 | 69.7 |
Hive | 86 | 85 | 97 | 93 | 91 | 87 | 85 | 90 | 91 | 100 | 90.5 |
Sembast | 241 | 252 | 263 | 257 | 258 | 240 | 236 | 253 | 257 | 246 | 250.3 |
Sqflite | 774 | 653 | 665 | 697 | 757 | 757 | 769 | 836 | 758 | 819 | 751.2 |
ObjectBox | 18686 | 18705 | 18806 | 18790 | 18767 | 18724 | 18763 | 18717 | 18739 | 18744 | 18744.1 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.0 |
Sembast | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2 | 2.0 |
ObjectBox | 14 | 24 | 24 | 23 | 30 | 24 | 24 | 24 | 23 | 23 | 23.3 |
Isar | 103 | 99 | 98 | 116 | 99 | 98 | 111 | 98 | 98 | 108 | 102.8 |
Sqflite | 135 | 133 | 136 | 151 | 134 | 132 | 133 | 131 | 155 | 140 | 138.0 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Isar | 41 | 32 | 33 | 34 | 36 | 32 | 34 | 33 | 36 | 36 | 34.7 |
Hive | 73 | 86 | 73 | 78 | 92 | 76 | 80 | 64 | 65 | 71 | 75.8 |
Sembast | 485 | 507 | 491 | 481 | 503 | 491 | 497 | 523 | 503 | 515 | 499.6 |
Sqflite | 733 | 750 | 743 | 741 | 748 | 743 | 749 | 754 | 842 | 830 | 763.3 |
ObjectBox | 18771 | 18784 | 18684 | 18698 | 18761 | 18680 | 18738 | 18683 | 18744 | 18739 | 18782.2 |
Below are the results (time in milliseconds) from 10 consecutive runs when optimized methods are used (for Hive, Isar, and ObjectBox) to write, read, and delete 1,000 users.
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Isar | 6 | 3 | 6 | 5 | 7 | 5 | 5 | 5 | 5 | 6 | 5.3 |
Hive | 14 | 16 | 12 | 15 | 15 | 17 | 15 | 15 | 14 | 13 | 14.6 |
ObjectBox | 20 | 19 | 23 | 18 | 19 | 23 | 20 | 20 | 19 | 20 | 20.1 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.0 |
ObjectBox | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.0 |
Isar | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 1 | 0.8 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 1 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 2 | 2 | 1.3 |
Isar | 1 | 3 | 1 | 2 | 1 | 1 | 1 | 1 | 2 | 4 | 1.7 |
ObjectBox | 19 | 19 | 21 | 19 | 19 | 20 | 18 | 19 | 19 | 18 | 19.1 |
The following benchmark results were received while running the project in release mode on a physical Samsung Galaxy A31 running Android 11.
Below are the results (time in milliseconds) from 10 consecutive runs when 1,000 individual write, read, and delete operations are performed.
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 322 | 321 | 322 | 402 | 380 | 287 | 340 | 303 | 300 | 320 | 329.7 |
Isar | 382 | 431 | 311 | 351 | 346 | 377 | 323 | 363 | 262 | 363 | 350.9 |
ObjectBox | 1614 | 1525 | 1608 | 1502 | 1473 | 1522 | 1583 | 1522 | 1619 | 1521 | 1548.9 |
Sembast | 2666 | 2352 | 2600 | 2507 | 2416 | 2297 | 2712 | 2641 | 2399 | 2508 | 2509.8 |
Sqflite | 3968 | 5281 | 4122 | 3448 | 3767 | 3641 | 4280 | 3609 | 3828 | 4026 | 3997.0 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1.0 |
Sembast | 17 | 17 | 16 | 16 | 18 | 17 | 16 | 17 | 15 | 15 | 16.4 |
ObjectBox | 18 | 22 | 19 | 17 | 21 | 18 | 20 | 20 | 17 | 10 | 19.2 |
Isar | 1142 | 1497 | 1380 | 1162 | 1305 | 1200 | 1240 | 1194 | 1206 | 1349 | 1267.5 |
Sqflite | 3148 | 3275 | 3209 | 2696 | 2691 | 2723 | 2731 | 2660 | 2680 | 2654 | 2846.7 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Isar | 358 | 380 | 322 | 347 | 354 | 375 | 341 | 321 | 318 | 353 | 346.9 |
Hive | 763 | 873 | 860 | 721 | 879 | 801 | 848 | 819 | 868 | 772 | 820.4 |
ObjectBox | 1566 | 1740 | 1580 | 1574 | 1650 | 2167 | 1575 | 1546 | 1586 | 1572 | 1655.6 |
Sqflite | 3896 | 4026 | 3946 | 3878 | 3610 | 3889 | 3558 | 4315 | 3554 | 3509 | 3818.1 |
Sembast | 6349 | 6729 | 7375 | 6575 | 6585 | 6980 | 6321 | 6770 | 6256 | 6756 | 6689.0 |
Below are the results (time in milliseconds) from 10 consecutive runs when optimized methods are used (for Hive, Isar, and ObjectBox) to write, read, and delete 1,000 users.
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
ObjectBox | 4 | 6 | 6 | 10 | 5 | 4 | 5 | 5 | 5 | 5 | 5.5 |
Isar | 9 | 10 | 9 | 9 | 8 | 7 | 7 | 9 | 7 | 8 | 8.3 |
Hive | 14 | 20 | 18 | 14 | 13 | 16 | 15 | 14 | 16 | 13 | 15.3 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Hive | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0.0 |
ObjectBox | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1.0 |
Isar | 6 | 7 | 3 | 5 | 6 | 4 | 6 | 4 | 3 | 4 | 4.8 |
Avg (ms) | |||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
ObjectBox | 3 | 2 | 2 | 2 | 3 | 3 | 2 | 2 | 3 | 3 | 2.5 |
Hive | 5 | 6 | 7 | 5 | 5 | 7 | 5 | 6 | 6 | 4 | 5.6 |
Isar | 8 | 8 | 5 | 4 | 4 | 5 | 5 | 8 | 9 | 3 | 5.9 |
For write operations on iOS, Isar was the fastest. Performing 1,000 individual write operations in an average of 69.7ms and writing 1,000 users in an average of 5.3ms.
On Android, meanwhile, Hive was the fastest with an average of 329.7ms when performing 1,000 individual write operations — though, when optimized, ObjectBox was able to write 1,000 users to the database in 5.5ms, performing better than both Hive (15.3ms) and Isar (8.3ms).
For read operations, Hive was the fastest on both iOS and Android. Performing both 1,000 individual read operations and reading 1,000 users in an average of 0ms (less than 1ms). ObjectBox also read 1,000 users in an average of 0ms on iOS and 1ms on Android, but performed 1,000 individual read operations in an average of 23.3ms on iOS and 19.2ms on Android.
For delete operations, Isar was the fastest on both iOS and Android. Performing 1,000 individual delete operations in an average of 34.7ms on iOS and 346.9 on Android. Hive performed slightly better when deleting 1,000 users (average of 1.3ms) as compared to Isar (average of 1.7ms) on iOS while on Android. ObjectBox was the fastest when deleting 1,000 users from the database, with an average of 2.5ms.
Setup was quite straightforward for all options discussed, so ease of use probably comes down to syntax preference, which in my personal opinion would be Hive. Isar and ObjectBox might require extra setup, but do allow you to read and write Dart objects directly to the database (Hive also provides an option for this, with the same extra setup of annotations and code generation).
Thank you for making it to the bottom of my experiment — I hope this article has helped you come to a decision about which Flutter app database works best for your project. If you would like to run the benchmarks yourself, here’s the link to the project on GitHub for your convenience.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
2 Replies to "Comparing Hive to other Flutter app database options"
Is Hive Apache related to Flutter Hive?
We tried to reproduce these results (especially the bad performance of ObjectBox on iOS) and arrived at completely different numbers: https://github.com/o-ifeanyi/db_benchmarks/issues/2