This tutorial will help you get started with the ObjectBox Flutter Database. We will create a simple task-list app using all ObjectBox CRUD operations (Create, Read, Update, Delete). Additionally, we will support adding a tag to each task by setting up a to-one relation between tasks and tags. The pure Dart ObjectBox API is very easy to use, as you will see by going through the steps outlined below.
A couple of useful links:
- See how ObjectBox compares to other Flutter databases
- Get started with ObjectBox
- Example app repository
About the app
Users can enter a new task, choose which tag to apply and add the task. All added tasks are displayed as a checklist with the associated tag. Users can mark a task as finished by ticking its checkbox and delete it by swiping it away.
Each task entry also shows the date when it was created or finished, depending on the state of the task.
How to start using the ObjectBox Database in your Flutter app
Add the library
Please refer to the Getting Started page for up-to-date information about adding the ObjectBox dependencies to your project.
Create a model file
ObjectBox is an object-oriented non-relational (NoSQL) database. First, we need to tell the database which entities to store. We can do this by defining a model and then using the build_runner to generate the binding code. The model is defined by writing Dart classes and annotating them.
Create a model file (e.g. model.dart), where we want to define two entities: one for task tags and one for tasks. These are just Dart classes with the @Entity annotation. Each entity must have an ID property of type int, which serves as the unique identifier. When this is called “id”, the property is recognized automatically but if you want to use a different name, annotate it with “@Id()”. In the Tag class, we also create a String property for the tag name. Here is how the model will look. Don’t worry about the objectbox.g.dart import for now – this file will be generated later.
1 2 3 4 5 6 7 8 9 10 11 12 | import 'package:intl/intl.dart'; import 'package:objectbox/objectbox.dart'; import 'objectbox.g.dart'; @Entity() class Tag { int id; String name; Tag (this.name, {this.id=0}); } |
Additional properties of our Task entity include a String for the task’s text and two DateTime properties for the date when a task was created and finished. Then we also define a to-one relation between Tasks and Tags. This is needed so that one can assign a tag to each task created within the app. We’ll come back to how relations work at the end of this tutorial.
1 2 3 4 5 6 7 8 9 10 11 12 | @Entity() class Task { int id; String text; DateTime dateCreated; DateTime? dateFinished; Task(this.text, {this.id=0, DateTime? dateCreated}) : dateCreated = dateCreated ?? DateTime.now(); final tag = ToOne<Tag>(); } |
Generate binding code
Once our model is done, we generate the ObjectBox binding code by running flutter pub run build_runner build. This will create the objectbox.g.dart file from the import above. You will need to do this every time you update the model (e.g. by adding or removing an entity or a property), and ObjectBox will take care of the change. However, in cases like entity renaming, you will need to provide ObjectBox with more information. Read more about data model updates in the ObjectBox docs.
Create a Store
Store represents an ObjectBox database and works together with Boxes to allow getting and putting. A Box instance gives you access to objects of a particular type. One would typically create one Store for their app and a number of Boxes that depends on the number of entities they want to store in the database.
So that our app can always display the current list of tasks without the need to actively refresh it, we create a data stream. Within ObjectBox, we can stream data using queries. We query all tasks by using taskBox.query()
and order them by date created in a descending order: order(Task_.dateCreated, flags: Order.descending)
. Then we use the watch() method to create a stream.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class ObjectBox { /// The Store of this app. late final Store store; /// Two Boxes: one for Tasks, one for Tags. late final Box<task> taskBox; late final Box<tag> tagBox; /// A stream of all tasks ordered by date. late final Stream<Query<task>> tasksStream; ObjectBox._create(this.store) { taskBox = Box<task>(store); tagBox = Box<tag>(store); final qBuilder = taskBox.query() ..order(Task_.dateCreated, flags: Order.descending); tasksStream = qBuilder.watch(triggerImmediately: true); } </tag></task></task></tag></task> |
Finally, we define the create() method that will create an instance of ObjectBox to use in our app.
1 2 3 4 5 6 | static Future<objectbox> create() async { // Future<store> openStore() {...} is defined in the generated objectbox.g.dart final store = await openStore(); return ObjectBox._create(store); } </store></objectbox> |
Open the store
Now we can initialise the store in our app’s main() function. Do this by calling the create() method of the ObjectBox class.
1 2 3 4 5 6 7 8 9 10 11 12 13 | /// Provides access to the ObjectBox Store throughout the app. late ObjectBox objectbox; Future<void> main() async { // This is required so ObjectBox can get the application directory // to store the database in. WidgetsFlutterBinding.ensureInitialized(); objectbox = await ObjectBox.create(); … } </void> |
CRUD operations
CREATE – Put a new Task or Tag into the Store
In the homepage state subclass of main.dart, we define the methods for adding new tasks and tags. Start by creating a task using the input controller. Then, set a tag we want to relate to this task by calling tag.target(). Now we only need to put the new Task object in its Box.
1 2 3 | Task task = Task(_taskInputController.text); task.tag.target = tagToAdd; objectbox.taskBox.put(task); |
READ – Get all tag names
To get all tag names, we call getAll() on our tagBox. This will return a list with all tags. If you want to read just a single object, call the get(id) method to get only the desired single object back. For a range of objects, use getMany(), passing a list of ids to it.
1 | List<Tag> tags = objectbox.tagBox.getAll(); |
Another way of reading data is by using queries. In the “Create a Store” section above, we created a task stream with the help of a query builder. There we just needed all tasks, so no criteria was specified. But generally, one can specify custom criteria to obtain a list of objects matching the needs. Learn more about how to use queries using the ObjectBox Query Docs.
DELETE – remove tasks by swiping
To remove objects from the database, we add a dismissible Flutter widget. Inside the setState method of the onDismissed property, we simply use the ObjectBox remove() operation with the corresponding task id.
1 2 3 4 5 6 7 8 9 | Dismissible( … onDismissed: (direction) { // Remove the task from the store. objectbox.taskBox.remove(tasks[index].id); setState(() {}); … }, ), |
UPDATE – Updating the date when Task was finished
Our app prints all tasks as a list with checkboxes. Each task’s finished date is initially set to null. We want the finished date of a task to update when the user marks the task as done. The non-null finished date therefore will act as an indicator that the task is finished. Let’s do this inside the setState() method of the onChanged property of our Checkbox class. Set the dateFinished to DateTime.now() if it was null when the checkbox value was changed, and set back to null otherwise.
1 2 3 4 5 6 7 8 9 10 11 12 | Checkbox( value: notNull(tasks[index].dateFinished), onChanged: (bool? value) { setState(() { if (!notNull(tasks[index].dateFinished)) { tasks[index].dateFinished = DateTime.now(); } else { tasks[index].dateFinished = null; } }); } ) |
Relations
Remember we initialised a ToOne relation in the very first section and planned to come back to this at the end? Now that we have covered all CRUD operations, we can come back to discussing those.
Relations allow us to build references between objects. They have a direction: a source object references a target object. There are three types of relations:
- to-one relations have one target object, as used above;
- one-to-many relations can have multiple target objects, but each target only has one source, e.g. we could add a backlink (one-to-many) to Tag in our example to find out all tasks with a specific tag;
- many-to-many relations involve targets that can have multiple sources, e.g. to improve our example app by supporting multiple tags per task, we could replace the to-one with a many-to-many relation.
Read about relations in more detail and learn how to use them with the help of the ObjectBox Relations Docs or our video tutorial.
How to use relations
When listing all tasks, we might want to include each task’s tag next to the task name. The relations are already initialised (see the “Create a model file” section). Now we need to read the tag of each task we want to list. So, inside the Text widget that displays the task name, we use tasks[index].tag.target?.name
to print the name of the corresponding tag.
1 2 3 | Text( '${tasks[index].text} (tag: ${tasks[index].tag.target?.name})', ) |
Build your own Flutter app with the ObjectBox Database
Now you have all the tools needed to build your own version of the task-list app with tags. The setup we described is rather minimal, so that anyone can get started easily. However, this gives you lots of room for improvement. For example, you could replace the existing to-one relation with a to-many, allowing users to add more than one tag per task. Or you can add task-list filtering and/or sorting functionality using different queries. The possibilities are truly endless.
We’d like to learn more about the creative ways to use the ObjectBox Database in Flutter you came up with! Let us know via Twitter or email.