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 setup works for Hive and other databases, and compare and contrast basic functionalities every database will be expected to feature (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.
CRUD
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 package 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
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
Sembast is a NoSQL database which 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 db 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
Sqflite is a Structured Query Language (SQL) database which 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
/update
helper function.
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 to the project, as they both use type annotations and code generation, which enables you to write/read Dart objects directly to the database (Hive also provides this as an option).
Isar
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 auto-generated 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
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 auto-generated 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 user model 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.
boxremoveuserid">box.remove(user.id);
Benchmark results
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.
iOS (iPhone 12 V15.6)
The following benchmark results were received while running the project in release mode on an iPhone 12, versus an 15.6.
Below are the results (time in milliseconds) from 10 consecutive runs when 1000 individual write, read, and delete operations were performed.
Write
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 |
Read
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 |
Delete
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 1000 users.
Write
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 |
Read
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 |
Delete
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 |
Android (Galaxy A31, Android 11)
The following benchmark results were received while running the project in release mode on a physical Samsung Galaxy A31, running on Android 11.
Below are the results (time in milliseconds) from 10 consecutive runs when 1000 individual write, read, and delete operations are performed.
Write
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 |
Read
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 |
Delete
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 1000 users.
Write
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 |
Read
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 |
Delete
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 |
Final results
For write operations on iOS, Isar was the fastest. Performing 1000 individual write operations in an average of 69.7ms and writing 1000 users in an average of 5.3ms.
On Android, meanwhile, Hive was the fastest with an average of 329.7ms when performing 1000 individual write operations — though, when optimized, ObjectBox was able to write 1000 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 1000 individual read operations and reading 1000 users in an average of 0ms (less than 1ms). ObjectBox also read 1000 users in an average of 0ms on iOS and 1ms on Android, but performed 1000 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 1000 individual delete operations in an average of 34.7ms on iOS and 346.9 on Android. Hive performed slightly better when deleting 1000 users (average of 1.3ms) as compared to Isar (average of 1.7ms) on iOS while on Android. ObjectBox was the fastest when deleting 1000 users from the database, with an average of 2.5ms.
Conclusion
Setup was quite straight forward 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 with 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.
The post Comparing Hive to other Flutter app database options appeared first on LogRocket Blog.
from LogRocket Blog https://ift.tt/18Fq2VB
Gain $200 in a week
via Read more