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
riverpod
overrides 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.