
When you're choosing a database you're going to use in your next project, it's good to make wise and informed choices. ObjectBox is a very fast NoSQL local database for Flutter and also native Android/iOS with an intuitive API, rich support for queries and relations, plus you can optionally keep the database synced across multiple devices without any hassle on your part.
We're going to build a "shop order app" showing orders of respective customers. You're going to learn a lot - setting up of entities, relations, ordering, and reactive queries. First, we're going to focus only on the local database functionality and then I'll show you how easy it is to sync your data across devices with ObjectBox Sync.
The finished app
The app will look and behave as on the video below. It has a DataTable
widget which displays all of the orders in rows. A single row displays the order ID, customer name (gotten through a database relation), price, and lastly a delete button.
To keep things simple, we won't have any forms to input the data about the order and the customer. Instead, we add new orders and switch the customer associated with the order by tapping buttons in the AppBar
. We're going to use a package called Faker to obtain things like fake customer names.
We can sort the displayed orders by their ID or price either ascendingly or descendingly by tapping on a column name in the DataTable
.
Finally, tapping on the customer name will open up a modal bottom sheet that displays only the respective customer's orders - again, relations between order and customer entities will come in handy for that.
Starting out
To get up and running quickly, grab the starter project from below. It contains all the widgets needed to display something on the screen, however, those widgets don't have any classes and data to work with - that will be our job to implement right now. Since we want to focus on the ObjectBox database, we're going to use a simple StatefulWidget
for state management.
The HomePage
widget that contains the AppBar
buttons has everything prepared already so that we can immediately start writing non-boilerplate code. We're also depending on the faker package in the starter project.
home_page.dart
import 'package:faker/faker.dart';
import 'package:flutter/material.dart';
import 'order_data_table.dart';
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final faker = Faker();
@override
void initState() {
super.initState();
setNewCustomer();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Orders App'),
actions: [
IconButton(
icon: Icon(Icons.person_add_alt),
onPressed: setNewCustomer,
),
IconButton(
icon: Icon(Icons.attach_money),
onPressed: addFakeOrderForCurrentCustomer,
),
],
),
body: OrderDataTable(
// TODO: Pass in the orders
onSort: (columnIndex, ascending) {
// TODO: Query the database and sort the data
},
),
);
}
void setNewCustomer() {
// TODO: Implement properly
print('Name: ${faker.person.name()}');
}
void addFakeOrderForCurrentCustomer() {
// TODO: Implement properly
print('Price: ${faker.randomGenerator.integer(500, min: 10)}');
}
}
The OrderDataTable
widget also has all the boilerplate already written for you. We're using the DataTable
widget which specifies its columns
and, most importantly, rows
- this is where data coming from the database will be displayed. Since DataTable
is a core Flutter widget, I already expect you to understand how it works. If you don't, I'd recommend you to check out the official Flutter docs.
Again, everything (including the sorting callback function) is already implemented, all we need is to add some actual data to be displayed.
order_data_table.dart
import 'package:flutter/material.dart';
class OrderDataTable extends StatefulWidget {
final void Function(int columnIndex, bool ascending) onSort;
const OrderDataTable({
Key? key,
required this.onSort,
}) : super(key: key);
@override
_OrderDataTableState createState() => _OrderDataTableState();
}
class _OrderDataTableState extends State<OrderDataTable> {
bool _sortAscending = true;
int _sortColumnIndex = 0;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
sortColumnIndex: _sortColumnIndex,
sortAscending: _sortAscending,
columns: [
DataColumn(
label: Text('Number'),
onSort: _onDataColumnSort,
),
DataColumn(
label: Text('Customer'),
),
DataColumn(
label: Text('Price'),
numeric: true,
onSort: _onDataColumnSort,
),
DataColumn(
label: Container(),
),
],
rows: [
DataRow(
cells: [
DataCell(
Text('ID'),
),
DataCell(
Text('CUSTOMER NAME'),
onTap: () {
// TODO: Show only tapped customer's orders in a modal bottom sheet
},
),
DataCell(
Text(
'\$PRICE',
),
),
DataCell(
Icon(Icons.delete),
onTap: () {
// TODO: Delete the order from the database
},
),
],
),
],
),
),
);
}
void _onDataColumnSort(int columnIndex, bool ascending) {
setState(() {
_sortColumnIndex = columnIndex;
_sortAscending = ascending;
});
widget.onSort(columnIndex, ascending);
}
}
Adding dependencies

We need to add a bunch of dependencies into the pubspec to use ObjectBox in our Flutter app - there's objectbox, then a plugin objectbox_flutter_libs, and lastly a dev dependency objectbox_generator. Apart from these three, we're also going to depend on path_provider to get the directory where we can store the local database file on Android and iOS.
pubspec.yaml
environment:
sdk: '>=2.12.0 <3.0.0'
dependencies:
flutter:
sdk: flutter
# Version 1.0.0 is coming soon
# It will be fully compatible with 0.15.0
objectbox: ^0.15.0
objectbox_flutter_libs: any
path_provider: ^2.0.1
faker: ^2.0.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: any
objectbox_generator: any
Platform-specific setup

ObjectBox supports only the 64-bit architecture on iOS, which all the devices from the last few years already run on. Still, you need to indicate this in XCode. If you're on a Mac, right click on the ios folder in the Flutter project and choose "Open in XCode". Once you're there, follow the steps from the picture below to set architecture to $ARCHS_STANDARD_64_BIT
.

Additionally, only iOS 11 and upwards is supported, so let's make sure to set it as the minimum iOS version.

Android doesn't need any such updates to the native Android project if you're using just the local ObjectBox database. However, I'll showcase you how easy it is to add the cross-device ObjectBox Sync at the end of this tutorial, and for that I need to also bump the minimum android version to SDK 21. If you're not using Sync, you don't need to do this.
android/app/build.gradle
...
android {
...
defaultConfig {
applicationId "com.resocoder.objectbox_prep"
minSdkVersion 21 // 👈 bump this
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
...
}
...
Defining the entities
ObjectBox works with entities. In Dart, these are simple classes annotated with the @Entity()
annotation. Each entity needs to have an int id;
field that will be populated by the ObjectBox package automatically, so it shouldn't be a required
constructor parameter. Other than that, entities can have any field of a supported type. Let's create ShopOrder
and Customer
classes, for now, without any relation between them.
entities.dart
import 'package:objectbox/objectbox.dart';
@Entity()
class ShopOrder {
int id;
int price;
ShopOrder({
this.id = 0,
required this.price,
});
}
@Entity()
class Customer {
int id;
String name;
Customer({
this.id = 0,
required this.name,
});
}
As you can see, the entity classes are simple - they really contain just the data we're interested in without any boilerplate. Instead, all of the boilerplate will be generated into the objectbox.g.dart file which is always going to be at the root of the lib folder. Let's run the build_runner!
🧑💻 terminal
flutter pub run build_runner watch --delete-conflicting-outputs
Still, we'd like to define a relation between the ShopOrder
and Customer
entities to, for example, easily get all of the orders that a particular customer has made. Luckily, ObjectBox supports all kinds of relations out of the box! 🙃
When you think about it, an order will always belong only to one customer. Customers, on the other hand, can make multiple orders. The relation we're looking for here is therefore One-to-Many. One customer to many orders, and reversely, many orders to one customer.
Relations are signified by creating a field and setting it immediately equal to either ToOne<NameOfOtherEntity>()
or ToMany<NameOfOtherEntity>()
. These helper classes allow us to hold a Customer
instance inside a ShopOrder
object in our Dart code, although in the actual database, this data will be stored completely separately.
entities.dart
@Entity()
class ShopOrder {
int id;
int price;
final customer = ToOne<Customer>();
ShopOrder({
this.id = 0,
required this.price,
});
}
@Entity()
class Customer {
int id;
String name;
@Backlink()
final orders = ToMany<ShopOrder>();
Customer({
this.id = 0,
required this.name,
});
}
Notice the @Backlink()
annotation on the orders
field? This tells the entity that the orders made by the customer will be gotten (backlinked) by looking at the ToOne
relation inside the ShopOrder
class.
Creating a Store

You access all ObjectBox data using a Store
. First though, you need to initialize it and specify where the database file is going to be stored. We'll do so from initState
of our HomePage
state.
To get the directory where it's safe to store the database file on the user's mobile device, we call getApplicationDocumentsDirectory()
and then continue with the value from this asynchronous call inside then
since initState
cannot be marked as asynchronous.
We'll put the Store
into a late
field and then indicate if it's already initialized with a separate boolean, so that we don't have to deal with nulls throughout our codebase.
home_page.dart
class _HomePageState extends State<HomePage> {
final faker = Faker();
late Store _store;
bool hasBeenInitialized = false;
@override
void initState() {
super.initState();
getApplicationDocumentsDirectory().then((dir) {
_store = Store(
// This method is from the generated file
getObjectBoxModel(),
directory: join(dir.path, 'objectbox'),
);
setState(() {
hasBeenInitialized = true;
});
});
}
@override
void dispose() {
_store.close();
super.dispose();
}
...
}
Setting customers & adding orders
The user of the app can set the "current customer" to a different Customer
object with a new ID and a new name by pressing "person icon" on the AppBar
.

Subsequently, this Customer
object will be the one who "makes the order" when the dollar sign button is pressed. Let's now implement the setNewCustomer
and addFakeOrderForCurrentCustomer
methods inside home_page.dart.
To make the current customer accessible throughout the class, we'll create a late
field for it.
home_page.dart
class _HomePageState extends State<HomePage> {
...
late Customer _customer;
@override
void initState() {
super.initState();
// We want to have a customer populated right when the app starts
setNewCustomer();
getApplicationDocumentsDirectory().then((dir) {
...
});
}
...
void setNewCustomer() {
_customer = Customer(name: faker.person.name());
}
void addFakeOrderForCurrentCustomer() {
final order = ShopOrder(
price: faker.randomGenerator.integer(500, min: 10),
);
order.customer.target = _customer;
_store.box<ShopOrder>().put(order);
}
}
Let's now dissect the addFakeOrderForCurrentCustomer
a bit. Creating the ShopOrder
object is straightforward. Then, we populate the ToOne<Customer>
relation with the current Customer
entity inside the _customer
field. Lastly, we persistently put the order into the database.
A single ObjectBox Store
can have many Box
es. In this case, there will be one for the ShopOrder
entities and another one for Customer
s.
ShopOrder
box, the database is smart enough to automatically put the Customer
entity specified as the target
of the relation into its own Box<Customer>
. This is not always the case though, read more in the official docs.Watching the data

We now have putting the data into the database handled. This is of no use though if we cannot see it in the UI. For that, we first need to read the data from the database.
It's possible to perform a one-off read with ObjectBox, but in a reactive framework such as Flutter, it's almost always better to continuously watch the data using a Stream
that will produce a new event whenever the data in the database is updated - for example, we add a new order.
We want to start watching the data inside Box<ShopOrder>
immediately when the app starts so we're going to create a simple query and watch it inside initState
. We'll also put the returned Stream
into a state field - we'll want to use it from the build
method.
home_page.dart
class _HomePageState extends State<HomePage> {
...
// 👇 ADD THIS
late Stream<List<ShopOrder>> _stream;
@override
void initState() {
super.initState();
setNewCustomer();
getApplicationDocumentsDirectory().then((dir) {
_store = Store(
getObjectBoxModel(),
directory: join(dir.path, 'objectbox'),
);
setState(() {
// 👇 ADD THIS
_stream = _store
.box<ShopOrder>()
// The simplest possible query that just gets ALL the data out of the Box
.query()
.watch(triggerImmediately: true)
// Watching the query produces a Stream<Query<ShopOrder>>
// To get the actual data inside a List<ShopOrder>, we need to call find() on the query
.map((query) => query.find());
hasBeenInitialized = true;
});
});
}
...
}
Displaying the data
We have the Stream
of ShopOrder
s and now we need to show it inside the OrderDataTable
. The starter project already contains all of the preparation code, so let's just come in and make it possible to pass in a List<ShopOrder>
object. First, we of course need to create a field for the orders.
order_data_table.dart
class OrderDataTable extends StatefulWidget {
final List<ShopOrder> orders;
final void Function(int columnIndex, bool ascending) onSort;
const OrderDataTable({
Key? key,
required this.orders,
required this.onSort,
}) : super(key: key);
@override
_OrderDataTableState createState() => _OrderDataTableState();
}
Once we have the List, we can display its data inside the DataTable
. For that, we're only interested in the rows
parameter of the DataTable
widget's constructor. For each ShopOrder
object inside the orders
List, we want to map it into a DataRow
that's going to nicely display the data to the user.
order_data_table.dart
class _OrderDataTableState extends State<OrderDataTable> {
...
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
...
// 👇 This is important
rows: widget.orders.map((order) {
return DataRow(
cells: [
DataCell(
Text(order.id.toString()),
),
DataCell(
Text(order.customer.target?.name ?? 'NONE'),
onTap: () {
// TODO: Show only tapped customer's orders in a modal bottom sheet
},
),
DataCell(
Text(
'\$${order.price}',
),
),
DataCell(
Icon(Icons.delete),
onTap: () {
// TODO: Delete the order from the database
},
),
],
);
}).toList(),
),
),
);
}
...
}
Let's now pass the actual List<ShopOrder>
into the OrderDataTable
! Since we have a Stream
in the HomePage
state, we're going to use a StreamBuilder
widget to rebuild the OrderDataTable
whenever new a ShopOrder
is added to the database and the stream emits a new event.
home_page.dart
class _HomePageState extends State<HomePage> {
...
@override
Widget build(BuildContext context) {
return Scaffold(
...
body: !hasBeenInitialized
? Center(
child: CircularProgressIndicator(),
)
: StreamBuilder<List<ShopOrder>>(
stream: _stream,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else {
return OrderDataTable(
orders: snapshot.data!,
onSort: (columnIndex, ascending) {
// TODO: Query the database and sort the data
},
);
}
},
),
);
}
...
}
And the app now works! Kind of... While we can display and add new orders, we'd also like to be able to sort the orders based on their ID or price, delete an order, and lastly, display only the orders of a specific customer in a modal bottom sheet. Let's go one-by-one.
Sorting the orders

Sorting is very easy to accomplish. We already have an onSort
callback on the OrderDataTable
widget. Let's simply create a new database query in this callback function that's going to be appropriately sorted, and then reset the Stream
that's used by the StreamBuilder
to contain the sorted data.
Every entity like our ShopOrder
has a generated "companion class" thats has an underscore appended to its name, in our case a ShopOrder_
. This class is used to specify the fields based on which we'd like the query to be sorted (or ordered - but ordering ShopOrders
sounds silly...)
The onSort
callback receives the columnIndex
that has been tapped by the user. We should sort the data based on this index - index 0 is for the ID column, while index 2 is for the price column.
Whether we sort ascendingly or descendingly is handled by the flags
parameter. Default (0) means ascending sorting, otherwise we use a value from the Order
(not our ShopOrder
!!) class to specify a descending sorting.
home_page.dart
return OrderDataTable(
orders: snapshot.data!,
onSort: (columnIndex, ascending) {
final newQueryBuilder = _store.box<ShopOrder>().query();
final sortField =
columnIndex == 0 ? ShopOrder_.id : ShopOrder_.price;
newQueryBuilder.order(sortField,
flags: ascending ? 0 : Order.descending);
setState(() {
_stream = newQueryBuilder
.watch(triggerImmediately: true)
.map((query) => query.find());
});
},
);
Deleting orders

Pressing the trash can button inside the DataCell
in our OrderDataTable
should delete the order from the database. For that, we'll need to access the Store
even from the OrderDataTable
so let's make sure we can pass it in.
order_data_table.dart
class OrderDataTable extends StatefulWidget {
...
final Store store;
const OrderDataTable({
Key? key,
...
required this.store,
}) : super(key: key);
@override
_OrderDataTableState createState() => _OrderDataTableState();
}
Now inside the last DataCell
in the mapped DataRow
:
orders_data_table.dart
class _OrderDataTableState extends State<OrderDataTable> {
...
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SingleChildScrollView(
child: DataTable(
...
rows: widget.orders.map((order) {
return DataRow(
cells: [
...
// 👇 Edit this last DataCell's code
DataCell(
Icon(Icons.delete),
onTap: () {
widget.store.box<ShopOrder>().remove(order.id);
},
),
],
);
}).toList(),
),
),
);
}
...
}
Removing entities from a database is just as simple as putting them in. Now we, of course, need to pass the Store
into the widget from the HomePage
state.
home_page.dart
return OrderDataTable(
orders: snapshot.data!,
onSort: (columnIndex, ascending) {
...
},
store: _store,
);
Show orders of a specific customer
Do you remember how a Customer
is related to many ShopOrders
? I mean, it literally has a field initialized to ToMany<ShopOrder>
! That's precisely what we're going to use now to display all of the orders of a specific customer without having to worry about somehow finding these orders associated with a customer by ourselves. The One-to-Many relation that ObjectBox supports is going to handle all of the complexity for us and we can just use the nice Dart objects as we're used to.
Inside the DataCell
displaying the customer's name, we're going to show a modal bottom sheet that's simply going to contain a ListView
displaying all of the tapped customer's orders. Or, should I say, the DataCell
's order's customer's orders.
Yes, the previous sentence has surely confused you, but that's precisely what we're displaying in the ListView
. Every order has a To-One relation to a customer, and then every customer has a To-Many relation to orders made by that customer. So, we're actually utilizing two relations to obtain the data we need.
order_data_table.dart
DataCell(
Text(order.customer.target?.name ?? 'NONE'),
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) {
return Material(
child: ListView(
children: order.customer.target!.orders
.map(
(_) => ListTile(
title: Text(
'${_.id} ${_.customer.target?.name} \$${_.price}',
),
),
)
.toList(),
),
);
},
);
},
),
Cross-device Sync
It's very easy to synchronize data across multiple devices if you're already using ObjectBox as your local database. ObjectBox Sync is a paid service that is a complete out-of-the-box data synchronization solution to always keep your data up-to-date.
If you're interested in using Sync, just contact the people at ObjectBox and they're going to set you up with the best possible solution for your project. Once you have your ObjectBox Sync executable, you can run it on your server and begin syncing data in no time.
Sync has a very good documentation that can get you running in a matter of minutes once you have your very own executable or a Docker image readily at hand.
If you'd like to see a demonstration of Sync, then make sure to check out the video tutorial accompanying this article, starting from this timestamp.
Conclusion
ObjectBox is a fast local database with a pleasant API, rich queries, relation support and you can also easily sync your data across multiple devices by using ObjectBox Sync.
How does it compare to alternatives? When it comes to performance, it blows most of the other Flutter databases out of the water. Compared to other NoSQL databases such as Sembast or even Shared Preferences, it shines with its support for relations and advanced ordering capabilities. When you add the option of easily synchronizing your data, ObjectBox should be definitely an option you consider when choosing a database for your next Flutter app.
The method ‘box’ was called on null.
Receiver: null
Tried calling: box()
turned out that i put some code outside the async method so the object is being referenced before it’s fully initialized.
Thanks for the detailed explanation.
Hi and thanks for this perfect tutorial. That looks very similar to Isar (or Isar look similar to ObjectBox). I will try ASAP.
Did you know if, for exemple, in a desktop application, I open the app twice, if I update a customer in one screen, I will automaticly see the change on the other screen ? I need an app where I have an editable screen and a viewer screen. I tried with Isar, but it doesn’t seem to work.
Thanks again !
getApplicationDocumentsDirectory() throws _CastError: Null check operator on a null value. I am stuck since yesterday! HELP!!!!!!!!!!!!!! FYI, I am using null-safe version on a stable channel. Flutter doctor says everything is okay.
Hola, muy interesante el tutorial. Pero tengo una consulta como podria usar objectbox con arquitectura limpia?
Hi Matt, how would you initialize ObjectBox in an app similar to your clean architecture example? I am wondering how to access the objectBox object in LocalDataSource, when dependencies are injected with get_it / injectable
I finally came up with this implementation: https://stackoverflow.com/questions/68679357/how-to-access-inject-objectbox-database-in-repository-in-flutter-reso-coder-dd
This article is very well written. Thanks