This article is part of the Data Storage in Flutter series.
Almost every app relies on storing data locally on the device. In the past, I already created tutorials for how to use the sembast package.
Initially, I wanted to write a tutorial on the hive package, but the last release was made over two years ago. Instead, the developers created a new package called * *isar**, which is now the recommended alternative.
The Cake App revisited
As the basis of this tutorial, I will use the same structure as I already used in my tutorial “Using sembast with riverpod”. Therefore, I will here mainly talk about the differences, since the UI and the state management are completely identical.
As always, the source code of this example is available on GitHub: https://github.com/bettercoding-dev/isar-reactive-database
What is a reactive database?
First, let’s quickly have a look at what I mean with reactive databases. In a conventional database, when you make changes to the data (create, update, delete something), the changes are applied and that’s it. This means, to display the changes in the UI, you will have to explicitly read the database to get the most recent data.
Some modern databases, like isar, allow listening to changes in the data. You can create a stream to the database,
which automatically updates every time the data changes.
This way, instead of manually pulling the data from the database, every time something changes, the database
actively
pushes changes to the UI.
In our case, isar provides a watch function that can be used on queries, so everytime the query result changes,
the
UI updates automatically.
Using isar as a reactive local storage
Prerequisites
First, let’s have a look at the dependencies we need for this tutorial.
The dependencies section in our pubspec.yaml should look like this
| |
To quickly install the dependencies, you can run:
flutter pub add flutter_riverpod freezed_annotation isar isar_flutter_libs json_annotation path_provider riverpod_annotation dev:isar_generator dev:build_runner dev:riverpod_generator dev:freezed dev:json_serializable
Initializing Isar
Before we can use isar we need to initialize the database.
In the global_providers.dart file, let’s create a new provider for our database.
| |
We’re using riverpod overrides to initialize the database before running the app.
We put the database in the ApplicationsDocumentsDirectory that we retrieved with the help of the path_provider
package.
Note, that we also need to reference the CakeShema here. We will shortly see where this comes from, when looking
at
the data model.
In case you’re wondering how the
riverpodoverrides work, hat a look at this blog post.
The Cake Model
The isar package required to annotate models that should be stored in the database with the @Collection()
annotation. Using code generation with the build_runner package, this will create code to store the model in the
database.
That way also the CakeSchema is created, that we used when initializing the database above.
Although it is not required, I would still recommend using freezed with your models.
This way, you’ll get handy functions as copyWith and hashCode.
The only thing that you need to do, to make freezed and isar work together, is to use ignore field on the
annotation and create a getter for the model’s id.
| |
The CakeRepository with isar
As with the tutorial on sembast, the CakeRepository interface looks the same:
| |
Notice the getAllCakesStream() function. Using the returned Stream, we can make our UI update without manually
querying the database.
Finally, let’s have a look at the isar-specific implementation of the repository:
| |
Actually, the implementation is dead simple. In comparison to other databases, isar takes care of serialization and
deserialization for us. This way, there’s almost no code left for us to write.
For all writing operations on the isar database, the package requires us to wrap the code in writeTxn functions.
This means that we’re creating transactions for our operations.
Transactions are a way to group database operations. If one of the operations fails, all operations in the same transactions are reversed as well. Or in other words: the operations are only persisted if all the operations are successful.
To create a query, we can use the where() function on an isar collection.
If we don’t specify any parameters, all objects are returned.
Using the watch function, we can listen to any changes of this query.
With fireImmediately: true we tell the stream, to immediately emmit the latest data when a listener connects.
Otherwise, the stream would only return data once the data changes the first time.
Summary
With isar it is very straightforward to create a local database.
The watch function on queries is very helpful to listen for database changes.
In comparison to sembast, isar uses code generation, to handle serialization and deserialization for us.
The isar database also comes with a lot of other features, like indexes, links and a query builder.
To learn more about these features, be sure to check out their documentation.