
当你选择一个你要在下一个项目中使用的数据库时,做出明智和明智的选择是好事。ObjectBox是一个非常快速的NoSQL本地数据库,适用于Flutter和原生的Android/iOS,具有直观的API,对查询和关系有丰富的支持,另外你可以选择 在多个设备上 保持数据库同步 ,而不需要你的任何麻烦。
我们将建立一个 "商店订单应用程序",显示各个客户的订单。你将会学到很多东西--实体的设置、关系、订购和反应式查询。首先,我们将只关注本地数据库的功能,然后我将向你展示用ObjectBox Sync跨设备同步你的数据是多么简单。
完成的应用程序
该应用程序的外观和行为将与下面的视频一样。它有一个DataTable widget,以行的形式显示所有的订单。一行显示订单ID,客户名称(通过数据库关系获得),价格,最后是一个删除按钮。
为了保持简单,我们不会有任何表格来输入关于订单和客户的数据。相反,我们添加新的订单,并通过点击AppBar 中的按钮来切换与订单相关的客户。我们将使用一个叫做Faker的包来获得假的客户名称等东西。
我们可以通过点击DataTable 中的列名对显示的订单按其ID或价格进行升序或降序排序。
最后,点击客户名称将打开一个模式底单,只显示相应客户的订单--同样,订单和客户实体之间的关系将在这方面派上用场。
开始工作
为了快速启动和运行,从下面抓取启动项目。它包含了在屏幕上显示东西所需的所有部件,然而,这些部件没有任何类和数据可以使用--这将是我们现在要实现的工作。因为我们想专注于ObjectBox数据库,我们将使用一个简单的StatefulWidget 来进行状态管理。
包含AppBar 按钮的HomePage widget已经准备好了一切,所以我们可以立即开始编写非boilerplate代码。我们也要依靠启动项目中的faker 包。
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)}');
}
}
OrderDataTable widget也已经为你写好了所有的boilerplate。我们正在使用DataTable widget,它指定了它的columns ,最重要的是rows --这是从数据库来的数据将被显示。由于DataTable 是Flutter的一个核心部件,我已经希望你能理解它的工作原理。如果你不知道,我建议你去看看Flutter的官方文档。
同样,所有的东西(包括排序回调函数)都已经实现了,我们所需要的就是添加一些实际的数据来显示。
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);
}
}
添加依赖性

我们需要在pubspec中添加一系列的依赖,以便在我们的Flutter应用中使用ObjectBox--有objectbox,然后是一个插件objectbox_flutter_libs,最后是一个开发依赖objectbox_generator。除了这三个,我们还要依赖path_provider来获得我们可以在Android和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
从官方博文中了解更多关于1.0版本的信息。
特定平台的设置

ObjectBox只支持iOS上的64位架构,过去几年所有的设备都已经在这个架构上运行。尽管如此,你还是需要在XCode中说明这一点。如果你是在Mac上,右击Flutter项目中的ios 文件夹,选择**"在XCode中打开**"。一旦你到了那里,按照下图的步骤,将架构设置为$ARCHS_STANDARD_64_BIT 。

此外,只支持iOS 11及以上版本,所以我们要确保将其设置为最低的iOS版本。

如果你只使用本地的ObjectBox数据库,Android不需要对本地的Android项目进行任何这样的更新。然而,在本教程的最后,我将向你展示添加跨设备ObjectBox Sync是多么容易,为此我还需要将最小的安卓版本提升到SDK 21。如果你不使用Sync,你不需要这样做。
android/app/build.gradle
...
android {
...
defaultConfig {
applicationId "com.resocoder.objectbox_prep"
minSdkVersion 21 // 👈 bump this
targetSdkVersion 30
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
...
}
...
定义实体
ObjectBox与实体一起工作。在Dart中,这些是用@Entity() 注解的简单类。每个实体都需要有一个int id; 字段,它将由ObjectBox包自动填充,所以它不应该是required 构造函数参数。除此以外,实体可以有任何支持的类型的字段。让我们创建ShopOrder 和Customer 类,现在,它们之间没有任何关系。
实体.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,
});
}
正如你所看到的,实体类很简单--它们真的只包含我们感兴趣的数据,没有任何模板。相反,所有的模板将被生成到objectbox.g.dart文件中,该文件将始终位于lib 文件夹的根部。让我们运行 build_runner!
🧑💻终端
flutter pub run build_runner watch --delete-conflicting-outputs
尽管如此,我们还是想在ShopOrder 和Customer 实体之间定义一个关系,例如,轻松地获得一个特定客户的所有订单。幸运的是,ObjectBox支持各种关系的开箱即用!🙃
当你考虑到这一点,一个订单总是只属于一个客户。另一方面,客户可以做多个订单。因此,我们在这里寻找的关系是**"一对多"**。一个客户对许多订单,反过来说,许多订单对一个客户。
也支持其他类型的关系,如一对一和多对多。请查看文档。
关系的标志是创建一个字段,并将其立即设置为等于ToOne<NameOfOtherEntity>() 或ToMany<NameOfOtherEntity>() 。这些辅助类允许我们在Dart代码中的ShopOrder 对象中持有一个Customer 实例,尽管在实际的数据库中,这些数据将被完全分开存储。
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,
});
}
注意到orders 字段上的@Backlink() 注解了吗?这告诉实体,客户的订单将通过查看ShopOrder 类中的ToOne 关系来获得(反向链接)。
创建一个商店

你使用一个Store 来访问所有ObjectBox的数据。首先,你需要初始化它并指定数据库文件的存储位置。我们将从initState 我们的HomePage 状态中进行。
为了得到在用户的移动设备上安全存储数据库文件的目录,我们调用getApplicationDocumentsDirectory() ,然后继续使用这个异步调用里面的值then ,因为initState 不能被标记为异步的。
我们将把Store 放到一个late 字段中,然后用一个单独的布尔值表明它是否已经被初始化,这样我们就不必在整个代码库中处理空值了。
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();
}
...
}
设置客户和添加订单
应用程序的用户可以通过按下AppBar 上的*"人物图标"*,将 "当前客户 "设置为一个具有新ID和新名称的不同Customer 对象。

随后,当按下美元符号按钮时,这个Customer 对象将成为 "下订单 "的人。现在让我们在home_page.dart中实现setNewCustomer 和addFakeOrderForCurrentCustomer 方法。
为了使当前客户在整个类中都能被访问,我们将为它创建一个late 字段。
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);
}
}
现在让我们对addFakeOrderForCurrentCustomer 进行一下剖析。创建ShopOrder 对象是很简单的。然后,我们在_customer 字段内用当前的Customer 实体来填充ToOne<Customer> 关系。最后,我们将订单持久化地放入数据库中。
一个ObjectBoxStore 可以有许多Boxes。在这种情况下,会有一个用于ShopOrder 实体,另一个用于Customers。
target 虽然我们只是把数据放到ShopOrder 盒子里,但是数据库很聪明,会自动把指定为关系的Customer 实体放到它自己的Box<Customer> 。但这并不总是如此,请在官方文档中阅读更多信息。
观察数据

我们现在已经处理了将数据放入数据库的问题。但如果我们不能在用户界面上看到它,这就没有用了。为此,我们首先需要从数据库中读取数据。
用ObjectBox进行一次性读取是可能的,但在Flutter这样的反应式框架中,使用一个Stream 来持续观察数据几乎总是更好的,每当数据库中的数据被更新时(例如,我们添加了一个新的订单),就会产生一个新的事件。
我们想在应用程序启动时立即开始观察Box<ShopOrder> 内的数据,所以我们将创建一个简单的查询并在initState 内观察它。我们还将把返回的Stream 到一个状态字段中--我们要从build 方法中使用它。
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;
});
});
}
...
}
显示数据
我们有了ShopOrders的Stream ,现在我们需要在OrderDataTable 里面显示它。启动项目已经包含了所有的准备代码,所以我们就来让它可以传入一个List<ShopOrder> 对象。首先,我们当然需要为订单创建一个字段。
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();
}
一旦我们有了List,我们就可以在DataTable 里面显示它的数据。为此,我们只对DataTable widget的构造函数的rows 参数感兴趣。对于orders List里面的每个ShopOrder 对象,我们要把它映射到一个DataRow ,以便很好地把数据显示给用户。
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(),
),
),
);
}
...
}
现在让我们把实际的List<ShopOrder> 传递到OrderDataTable!由于我们在HomePage 状态下有一个Stream ,我们将使用一个StreamBuilder widget来重建OrderDataTable ,每当有新的ShopOrder 被添加到数据库和流发出一个新的事件。
主页_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
},
);
}
},
),
);
}
...
}
应用程序现在可以工作了!算是吧...虽然我们可以显示和添加新的订单,但我们也希望能够根据订单的ID或价格进行排序,删除一个订单,最后,在一个模态的底单中只显示一个特定客户的订单。让我们逐一来看看。
对订单进行排序

排序是非常容易实现的。我们已经在OrderDataTable widget上有一个onSort callback。让我们简单地在这个回调函数中创建一个新的数据库查询,它将被适当地排序,然后重置StreamBuilder 所用的Stream ,以包含排序后的数据。
每个实体,如我们的ShopOrder ,都有一个生成的 "伴侣类",它的名字后面有一个下划线,在我们的例子中是一个ShopOrder_ 。这个类是用来指定字段的,我们希望根据这个字段对查询进行排序(或排序--但排序ShopOrders ,听起来很傻......)。
onSort 回调接收被用户点过的columnIndex 。我们应该根据这个索引对数据进行排序--索引0是指ID列,而索引2是指价格列。
我们是升序排序还是降序排序,由flags 参数处理。默认(0)意味着升序排序,否则我们使用Order (不是我们的ShopOrder !!)类中的一个值来指定降序排序。
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());
});
},
);
ObjectBox中的排序是相当强大的,请在官方文档中阅读更多关于它的信息。
删除订单

在我们的OrderDataTable 中按下DataCell 里面的垃圾桶按钮,应该可以从数据库中删除订单。为此,我们需要访问Store ,甚至从OrderDataTable ,所以让我们确保我们可以传入它。
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();
}
现在在最后一个DataCell ,在映射的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(),
),
),
);
}
...
}
从数据库中删除实体就像把它们放进去一样简单。现在,我们当然需要将Store 从HomePage 状态传入widget。
home_page.dart
return OrderDataTable(
orders: snapshot.data!,
onSort: (columnIndex, ascending) {
...
},
store: _store,
);
显示一个特定客户的订单
你还记得一个Customer 是如何与许多ShopOrders 有关的吗?我的意思是,它实际上有一个字段初始化为ToMany<ShopOrder>!这正是我们现在要使用的,以显示一个特定客户的所有订单,而不必担心自己以某种方式找到这些与客户相关的订单。ObjectBox支持的一对多关系将为我们处理所有的复杂性,我们可以像以前一样使用漂亮的Dart对象。
在显示客户姓名的DataCell ,我们将显示一个模态的底层页面,它将简单地包含一个ListView ,显示所有被点击的客户的订单。或者,我应该说,DataCell's order's customer's orders。
是的,前面的句子肯定让你感到困惑,但这正是我们在ListView 。每个订单与客户有一个To-One关系,然后每个客户与该客户的订单有一个To-Many关系。因此,我们实际上是利用两个关系来获得我们需要的数据。
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(),
),
);
},
);
},
),
跨设备同步
如果你已经使用ObjectBox作为你的本地数据库,在多个设备间同步数据是非常容易的。ObjectBox Sync是一项付费服务,它是一个完整的开箱即用的数据同步解决方案,可以始终保持你的数据是最新的。
如果你对使用Sync感兴趣,只需联系ObjectBox的人,他们将为你的项目设置最好的解决方案。一旦你有了ObjectBox Sync的可执行程序,你就可以在你的服务器上运行它,并马上开始同步数据。
你也可以免费试用Sync,看看它是否适合你的应用程序。
Sync有一个非常好的文档,一旦你有了自己的可执行文件或Docker镜像,可以在几分钟内让你运行。
如果你想看一下Sync的演示,那么一定要看看本文所附的视频教程,从这个时间戳开始。
总结
ObjectBox是一个快速的本地数据库,具有令人愉快的API,丰富的查询,关系支持,你也可以通过使用ObjectBox Sync轻松地在多个设备上同步你的数据。
它与其他的数据库相比如何?说到性能,它把大多数其他Flutter数据库吹得天花乱坠。与其他NoSQL数据库如Sembast甚至Shared Preferences相比,它因支持关系和高级排序能力而大放异彩。当你加入轻松同步数据的选项时,ObjectBox绝对应该是你为下一个Flutter应用选择数据库时考虑的一个选项。