Flutter and ObjectBox - High-Performance NoSQL Database

thecodexhub
Towards Dev
Published in
10 min readAug 28, 2022

--

Persisting data locally or remotely on the server is one of the fundamental requirements of any mobile application development process. Choosing a suitable database to work with, can sometimes be tricky.

ObjectBox is a super-fast NoSQL ACID-compliant object persistence for cross-platform Flutter apps. An intuitive API, rich support for queries and relations, efficient data access, and minimal usage of CPU, memory, and battery make it an ideal choice for mobile devices.

“The NoSQL SQLite alternative for Mobile, IoT, and Embedded Devices.”

Additionally, the database can be kept optionally synchronized conveniently and effectively across multiple devices without any hassle from your side.

Benchmark of ObjectBox (source: here)

What we are building

We are going to build an Expense Tracker app that shows users their list of expenses. Through this app, we are going to explore entities, relations, reactive queries, ordering, and other functionalities.

You might ask if the above-said features are already explained beautifully in the official documentation of ObjectBox, then what is the requirement of this article?

Well, this article will not only help you with learning about ObjectBox, but also it will provide you with sufficient knowledge about how to use it in your next production application, how to maintain the codebase with proper architecture and state management techniques, and how to effectively isolate the presentation layer from the business logic layer.

Check out the below screenshots from the application we are going to build. Also, you might be curious to see how the app works, and for that, there is a video link here.

Screenshots from the application we are going to build

Feel free to check out the final repository anytime you got stuck.

Table of Contents

Project Dependencies

We shall start by replacing the pubspec.yaml file at the root of the project with the following:

Well, that’s a large number of packages we are installing.

But you know, good practices are good practices. You can try replacing the versions of the packages with their latest versions. And the reason they are needed is already specified in the above gist.

Platform Specific Setup

Before we start writing our code, we need to do some initial setups to work with ObjectBox. Please follow the below link that will provide you with steps to do for the platforms you want to run the app.

https://pub.dev/packages/objectbox#getting-started

For example, if you are planning to add the sync feature of the ObjectBox in your android app, you need to set the minSdkVersion 21 in the android/app/build.gradle file. Here is how to do this.

Open the local.properties file that resides inside the android folder. And add the following line:

flutter.minSdkVersion=21

Then go to android/app/build.gradle file and check the defaultConfig section, update the minSdkVersion as following

minSdkVersion localProperties.getProperty(‘flutter.minSdkVersion’).toInteger()

Define the Entities - ExpenseType and Expense

Next, take a look at the entities. For this example, an expense type will consist of an identifier and name. Whereas, an expense entity will consist of an amount, note, and date when created.

In ObjectBox, the persistable objects are called entities. You can annotate classes with @Entity to help ObjectBox know which of them are entities. This will trigger ObjectBox to generate persistence code tailored for this class.

Note: It’s completely up to you to define what a user needs to look like in the context of your requirement.

Additionally, there are more annotations available to configure your entity class and its properties like @Transient, @Index, @NameInDb, @Unique, @Property, etc. Check out the official documentation for more details.

https://docs.objectbox.io/entity-annotations

After the annotations are placed in the right place, it is a good time to run the code generator using the build_runner. The boilerplate for the entities to be able to fetch from the database or persist in the database will be generated in the root of the lib folder. So let’s run the build_runner.

flutter pub run build_runner watch — delete-conflicting-outputs

You can see now that the objectbox.g.dart file has been generated and another file named objectbox-model.json has also been generated. This JSON file is going to be used by the ObjectBox sync to sync the data across multiple devices.

Now we are going to establish a relationship between the two entities. Luckily, ObjectBox supports all kinds of relationships out of the box.

Relations will make it easy to access all the expenses made by a user. If you can remember, we have already discussed that one expense type can be associated with multiple expenses (to-many relationship), but one expense can be associated with only one expense type (to-one relationship)

Note: The @Backlink annotation tells ObjectBox which ToOne relation to use to populate the list of expenses.

Initialize Store with StoreRepository

To access data from ObjectBox, we need to create a BoxStore object. And we can create the store using the model definition defined in the objectbox.g.dart file. Also, we can provide a directory to store the data.

You can create internal packages to better layer the application architecture and maintain clear boundaries, but for the sake of simplicity, you can follow along and create a new folder inside the lib directory and name it repository.

Then create a new file store_repository that will be responsible for abstracting the internal implementation details of how we initialize the store. Additionally, we will provide a getter to let other repository classes use the store object initialized in the StoreRepository class.

Create ExpenseTypeRepository and ExpenseRepository

Let’s create an ExpenseTypeRepository to handle the add, get and delete functionality for the expense types. Similarly, we can create an ExpenseRepository for these same functionalities. But there is only one difference when adding an expense we need to set the expense type using the target keyword.

In a reactive framework like Flutter, it’s nearly always preferable to continuously monitor the data using a Stream that will emit a new event whenever the data in the database is modified, for example, when we add a new expense type or when we delete an expense.

Using ObjectBox we can do that easily with the in-built watch() method that returns Stream<Query<T>> which we can then map to get our expected result. The watch() method takes an optional parameter triggerImmediately (default is set to false), which if set to true, a single stream event will be sent immediately after subscribing.

Follow the below code that watches over the persisted data and the desired output is returned.

In addition to specifying conditions, you can order the returned results using the order() method:

Create RepositoryProvider for the repositories

So now, as we have completed creating our repositories, we can inject them into our app using the RepositoryProvider so that we can work with only single instances of the repositories throughout the app.

The main.dart file can be replaced by the following code. We are simply setting up some global configuration along with initializing the ObjectBox store and calling runApp with an instance of the App.

Notice how we are injecting a single instance of the StoreRepository into the App and it is explicit constructor dependency.

Our app.dart will provide an instance of the StoreRepository to the application via the RepositoryProvider and also creates and provides the instances for the other repositories. After we are done, the app.dart file will look like the following.

Create the Forms for inputs

From the screenshots of the application, we are going to build, you might have already figured out that we are going to have two separate forms - one for taking inputs to create expense types and another for creating expenses. Let’s finish setting up them quickly.

Models for Form Inputs

An Identifier and Name input model for ExpenseType and an Amount and Note input model for Expense are useful for encapsulating the validation logic and will be used in creating the user interactive forms.

All these input models are going to be created using the formz package that allows us to work with a validated value object rather than a primitive object type like String.

Blocs for Form inputs

We will be creating two separate blocs - one for handling the ExpenseTypeForm and another for ExpenseForm.

Let’s start with ExpenseTypeFormBloc. This will be responsible for managing the ExpenseTypeFormState of the form. It exposes API for adding an expense type to the database, as well as gets notified when the identifier or the name is updated.

ExpenseTypeForm - State

ExpenseTypeForm - Event

ExpenseTypeForm - Bloc

Next is the ExpenseFormBloc. This will be responsible for managing the ExpenseFormState of the form. Like the previous form bloc, this will also expose API for adding expenses to the database, as well as get notified when the amount or note or expense type gets changed.

The only difference is that we need an additional event to load up all the available expense types when the form is shown to the user.

ExpenseForm - State

ExpenseForm - Event

ExpenseForm - Bloc

Let’s make a plan for the next steps before going further. The blocs that we have built just now are solely responsible only for managing the form data and submitting the forms. We need a separate set of blocs still to handle the fetch streams and other actions like delete.

Create ExpenseTypeBloc - Fetch Stream, Delete

Let’s start by creating the events. We are going to have an ExpenseTypeSubscriptionRequested event which is a startup event, in response to which the bloc will subscribe to the stream of expense types from the ExpenseTypeRepository. Another event ExpenseTypeDeleted will delete an expense type.

The ExpenseTypeState will keep track of the list of expense types, and the status.

Now let’s create the Bloc. When ExpenseTypeSubscriptionRequested is added, the bloc starts by emitting a loading state. In response, the UI can render a loading widget. Next, we use

emit.forEach<List<ExpenseType>>( … )

which creates a subscription on the expense type stream from the ExpenseType repository.

And when the ExpenseTypeDeleted is added, it simply just calls the method from the repository. The emit is never called from within _onExpenseTypeDeleted. Instead, that notifies the repository and emits an updated list from the expense type streams.

Create ExpenseType Page view and form

The ExpenseTypePage is responsible for creating and providing instances for ExpenseTypeFormBloc and ExpenseTypeBloc to the ExpenseTypeView.

It’s very important to keep the creation of blocs/cubits separate from where they are consumed. This will allow you to easily inject mock instances and test your view in isolation.

The ExpenseTypeView is responsible for rendering the form in response to the ExpenseTypeFormState and invokes events in response to the user interactions. Also, it renders the list of the ExpenseTypes in response to the ExpenseFormState.

Create ExpenseBloc - Fetch Stream, Delete

Similarly, we are going to have an ExpenseListRequested event which is a startup event, in response to which the bloc will subscribe to the stream of expenses from the ExpenseRepository.

Other events like ExpenseListSortByTimeRequested, ExpenseListSortByAmountRequested, ExpenseInLast7DaysRequested will subscribe to corresponding methods from the ExpenseRepository.

The ToggleExpenseSort will take care of changing the order type and adding the expected events to it. And the ExpenseDeleted will delete a particular expense from the database.

The ExpenseState will keep track of the list of expenses, the expense sort method, total expenses in the last 7 days, and the status.

Now let’s create the Bloc. logically, it remains the same as ExpenseTypeBloc.

Build the ExpensePage and ExpenseFormPage

The ExpensePage is responsible for creating and providing an instance for ExpenseBloc to the ExpenseView, which will then render the UI for different data available. Also, it renders the list of the Expenses in response to the ExpenseState.

On the other hand, The ExpenseFormPage is responsible for creating and providing an instance for ExpenseFormBloc to the ExpenseFormView. The ExpenseFormView is responsible for rendering the form in response to the ExpenseFormState and invokes events in response to the user interactions.

Well, we are done now. If you face any challenges, you can always check out the source code.

Conclusion

If you have made it this far, congratulations!

If you have any queries or suggestions or questions, dive into the comments sections. Alternatively, you can reach out to me using Instagram, Twitter, LinkedIn, or Email.

Please remember to applaud if this article was helpful and taught you something new. You know you can clap 50 times 🚀using that clap button. Give it a shot.😅

That’s it. Hope it helps!

--

--

Passionate Flutter Developer from INDIA || Technical Content Creator || Instagram: @thecodexhub