Flutter本地模型管理插件katana_model_local的使用
Flutter本地模型管理插件katana_model_local的使用
引言
在实现数据读写时,过程往往较为繁琐。通过RDB RestAPI,必须实现与数据库结构匹配的模式,即使只是为了本地存储数据,也需要大量工作来实现读写流程。
虽然像Firestore这样的强大数据库可以轻松地应用于移动和Web应用程序,但在某些情况下,它们对于特定类型的应用程序来说并不必要。根据我们开发各种应用的经验,我认为以下功能的模型足以创建90%以上的所有应用:
- 能够执行CRUD(创建、读取、更新、删除)
- 数据结构可以是任何Map(字典)类型对象及其列表
- 支持类似搜索和简单的查询过滤
- 提供测试、模拟、本地和远程数据库
我发现,通过提供与Firestore对齐接口的本地数据库和模拟测试数据库,同时以Firestore作为远程数据库的轴心,可以实现这些功能。
因此,我创建了以下包来实现它们:
- 接口和数据结构简化为与Firestore匹配,使得使用起来更加简单。
- 可以轻松地通过更改适配器在数据模拟、本地数据库和Firestore之间切换。
- 提供事务处理功能,便于实现关注/关注功能以及简单的Like搜索(当然在Firestore中)。
- 提供ClientJoin的简易集成,这是Firestore所需的进一步阅读引用文档中嵌入在特定文档字段中的文档引用。
- 结构允许安全地实现Immutable类,如
freezed
。 - 继承自ChangeNotifier,易于与
provider
、riverpod
等一起使用。
模型实现
class DynamicMapDocument extends DocumentBase<Map<String, dynamic>> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
Map<String, dynamic> fromMap(Map<String, dynamic> map) => map;
[@override](/user/override)
Map<String, dynamic> toMap(Map<String, dynamic> value) => value;
}
class DynamicMapCollection extends CollectionBase<DynamicMapDocument> {
DynamicMapCollection(super.modelQuery);
[@override](/user/override)
DynamicMapDocument create([String? id]) {
return DynamicMapDocument(modelQuery.create(id));
}
}
如何使用
// 创建
final doc = collection.create();
doc.save({"first": "masaru", "last": "hirose"});
// 读取
await collection.load();
collection.forEach((doc) => print(doc.value));
// 更新
doc.save({"first": "masaru", "last": "hirose"});
// 删除
doc.delete();
安装
导入以下包:
flutter pub add katana_model
如果使用本地数据库,请一起导入以下包:
flutter pub add katana_model_local
如果使用Firestore,请一起导入以下包:
flutter pub add katana_model_firestore
结构
它基于CloudFirestore的数据模型。
https://firebase.google.com/docs/firestore/data-model
#### 文档
轻量级记录,包含映射到值的字段。
在Dart中,它对应于`Map<String, dynamic>`。
```dart
<String, dynamic>{
first : "Ada"
last : "Lovelace"
born : 1815
}
集合
包含多个文档的列表。
在Dart中,这相当于List<Map<String, dynamic>>
。
<Map<String, dynamic>>[
<String, dynamic>{
first : "Ada"
last : "Lovelace"
born : 1815
},
<String, dynamic>{
first : "Alan"
last : "Turing"
born : 1912
},
]
按路径放置
数据按/collection/document
的路径结构存储,并可以通过指定路径检索。
记住从路径顶部开始的奇数编号路径是指定集合的路径,而偶数编号路径是指定文档。
// `User`集合。
/user
// "Ada"用户在`User`集合中的文档。
/user/ada
实时更新
默认情况下,此包支持实时更新。
当外部(或内部)进行数据变更时,已加载的相关文档和集合会自动更新并通知模型。
所有模型都继承自ChangeNotifier
,如果通过addListener
或其他方式监控更新,则可以立即重绘小部件。
实现
准备工作
例如,在MaterialApp
顶部放置ModelAdapterScope
,并指定ModelAdapter
。
// main.dart
import 'package:flutter/material.dart';
import 'package:katana_scoped/katana_scoped.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return ModelAdapterScope(
adapter: const RuntimeModelAdapter(), // 仅在应用程序运行时用于模拟等的适配器。
child: MaterialApp(
home: const ScopedTestPage(),
title: "Flutter Demo",
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
);
}
}
创建模型
通过指定要存储的值来创建文档和集合类。
此示例使用Map<String, dynamic>
。
文档
通过继承DocumentBase<T>
来实现T fromMap(Map<String, dynamic> map)
和Map<String, dynamic> toMap(T value)
。
允许modelQuery
传递给构造函数。
class DynamicMapDocument extends DocumentBase<Map<String, dynamic>> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
Map<String, dynamic> fromMap(Map<String, dynamic> map) => map;
[@override](/user/override)
Map<String, dynamic> toMap(Map<String, dynamic> value) => value;
}
集合
通过继承CollectionBase<TDocument extends DocumentBase>
来实现TDocument create([String? id])
。
在TDocument create([String? id])
中实现创建关联文档(在这种情况下为DynamicMapDocument
)的过程。
允许modelQuery
传递给构造函数。
class DynamicMapCollection extends CollectionBase<DynamicMapDocument> {
DynamicMapCollection(super.modelQuery);
[@override](/user/override)
DynamicMapDocument create([String? id]) {
return DynamicMapDocument(modelQuery.create(id));
}
}
使用freezed
使用freezed
,可以更安全地定义和实现模式。
[@freezed](/user/freezed)
class UserValue with _$UserValue {
const factory UserValue({
required String first,
required String last,
@Default(1900) int born
}) = UserValue;
factory UserValue.fromJson(Map<String, Object?> json)
=> _$UserValueFromJson(json);
}
class UserValueDocument extends DocumentBase<UserValue> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
UserValue fromMap(Map<String, dynamic> map) => UserValue.fromJson(map);
[@override](/user/override)
Map<String, dynamic> toMap(UserValue value) => value.toJson();
}
class UserValueCollection extends CollectionBase<UserValueDocument> {
UserValueCollection(super.modelQuery);
[@override](/user/override)
UserValueDocument create([String? id]) {
return UserValueDocument(modelQuery.create(id));
}
}
使用Record
也可以使用自Dart3以来可用的Record。
class UserRecordDocumentModel
extends DocumentBase<({String first, String last, int? born})> {
RuntimeRecordDocumentModel(super.query);
[@override](/user/override)
({String first, String last, int? born}) fromMap(DynamicMap map) {
return (
born: map.get("born", 0),
first: map.get("first", ""),
last: map.get("last", ""),
);
}
[@override](/user/override)
DynamicMap toMap(({String first, String last, int? born}) value) {
return {
"born": value.born,
"first": value.first,
"last": value.last,
};
}
}
如何使用
提供了以下方法根据CRUD。
- 新数据创建:
create()
- 数据加载:
load()
- 数据更新:
save(T value)
- 数据删除:
delete()
但是,以下限制适用:
- 数据创建只能通过在集合上执行
create()
或直接指定文档路径来完成。 - 数据加载是通过集合和文档的
load()
来完成。 - 数据更新应仅通过文档的
save(T value)
来完成。(可以循环遍历集合并对每个文档执行save(T value)
。) - 数据删除只能通过文档的
delete()
来完成。(可以循环遍历集合并对每个文档执行delete()
。)
此外,以下规则适用于通知:
- 如果文档中的某个字段发生变化,只有相应的文档会被通知。
- 当集合中的文档被添加或删除(即集合中文档的数量增加或减少)时,相应的集合会被通知。
以下是代码示例:在列表中显示集合的元素,使用FAB添加元素,点击每个ListTile
随机更新字段内容,并点击删除按钮删除元素。
import 'dart:math';
import 'package:katana_model/katana_model.dart';
import 'package:flutter/material.dart';
class ModelDocument extends DocumentBase<Map<String, dynamic>> {
ModelDocument(super.modelQuery);
[@override](/user/override)
Map<String, dynamic> fromMap(DynamicMap map) => map;
[@override](/user/override)
DynamicMap toMap(Map<String, dynamic> value) => value;
}
class ModelCollection extends CollectionBase<ModelDocument> {
ModelCollection(super.modelQuery);
[@override](/user/override)
ModelDocument create([String? id]) {
return ModelDocument(modelQuery.create(id));
}
}
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
[@override](/user/override)
Widget build(BuildContext context) {
return ModelAdapterScope(
adapter: const RuntimeModelAdapter(),
child: MaterialApp(
home: const ModelPage(),
title: "Flutter Demo",
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
);
}
}
class ModelPage extends StatefulWidget {
const ModelPage({super.key});
[@override](/user/override)
State<StatefulWidget> createState() => ModelPageState();
}
class ModelPageState extends State<ModelPage> {
final collection = ModelCollection(const CollectionModelQuery("/user"));
[@override](/user/override)
void initState() {
super.initState();
collection.addListener(_handledOnUpdate);
collection.load();
}
void _handledOnUpdate() {
setState(() {});
}
[@override](/user/override)
void dispose() {
super.dispose();
collection.removeListener(_handledOnUpdate);
collection.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Flutter Demo")),
body: FutureBuilder(
future: collection.loading ?? Future.value(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView(
children: [
...collection.mapListenable((doc) { // 监控文档并在更新时仅重绘内容小部件。
return ListTile(
title: Text(doc.value?["count"].toString() ?? "0"),
trailing: IconButton(
onPressed: () {
doc.delete();
},
icon: const Icon(Icons.delete),
),
onTap: () {
doc.save({
"count": Random().nextInt(100),
});
},
);
}),
],
);
},
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
final doc = collection.create();
doc.save({
"count": Random().nextInt(100),
});
},
),
);
}
}
首先,在指定集合路径CollectionModelQuery
的同时创建并持有集合对象。
当屏幕由addListner
等监控并更新时,使用setState
重新绘制屏幕。
(在此示例中,仅当集合中文档被添加或移除时才会重绘ModelPage
。)
使用collection.load()
加载数据。
如果加载时间较长,由于collection.loading
是Future
,可以通过将其直接传递给FutureBuilder
等来实现加载指示器。
由于集合本身实现了List
接口,一旦从collection
中读取了数据,就可以使用for循环和map等方法检索集合中的文档。
在此情况下,使用mapListenable
可以在文档内的回调中返回一个部件,从而可以监控文档的变化并在字段更新时仅重绘相应的部件。
要更新文档的值,只需将更新后的值传递给doc.save(T value)
中的值并执行即可。
要删除文档,只需执行doc.delete()
。当文档被删除时,相关的集合也会被通知并重绘ModelPage
。
要添加新文档,使用collection.create()
创建新文档,并使用doc.save(T value)
更新其值。
仅调用collection.create()
不会将文档添加到集合;文档将在执行doc.save(T value)
时添加到集合。
切换使用的数据库
通过更改传递给ModelAdapterScope
的ModelAdapter
,可以从用于数据模拟的数据库切换到local DB
或Firestore
。
- 在使用Firestore之前,请先创建Firebase项目并导入设置等。
- 有关Firestore的详细信息,请参阅:
目前可用的适配器如下:
RuntimeModelAdapter
- 仅在应用程序运行时存储数据的数据库适配器。
- 如果应用程序停止或重启,所有数据都会丢失。
- 应用程序启动时可以种植数据,可用于
数据模拟
和测试
。
LocalModelAdapter
- 存储在终端上的本地数据库适配器。
- 即使应用程序停止或重启,数据也会保留。
- 如果应用程序被删除或重新安装,数据将会丢失。
- 存储在设备上的数据是加密的,只能由应用程序打开。
- 在不希望或不需要使用服务器的情况下可用。
FirestoreModelAdapter
- 存储在
CloudFirestore
中的数据库适配器。 - 即使应用程序停止、重启、删除或重新安装,数据也会保留。
- 适用于在服务器上存储数据并通过服务器与其他用户通信。
- 存储在
ListenableFirestoreModelAdapter
- 存储在
CloudFirestore
中的数据库适配器。 - 即使应用程序停止、重启、删除或重新安装,数据也会保留。
- 适用于在服务器上存储数据并通过服务器与其他用户通信。
- 使用Firestore的
实时更新功能
,可以立即将服务器端的更新传输到应用程序端。 - 请使用它来实现聊天等功能。
- 存储在
CsvCollectionSourceModelAdapter
、CsvDocumentSourceModelAdapter
- 可以处理CSV作为数据源的数据源适配器。
- 无法保存或删除值。
- CSV可以通过以下方式获得:
- 直接在源代码中
- 存储并加载在
assets
文件夹下 - 从URL获取(例如,已经发布在Web上的Google电子表格)
使用本地数据库:
ModelAdapterScope(
adapter: const LocalModelAdapter(), // 用于在本地数据库中读取和保存的适配器。
child: MaterialApp(
home: const ScopedTestPage(),
title: "Flutter Demo",
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
)
使用Firestore数据库:
ModelAdapterScope(
adapter: FirestoreModelAdapter(options: DefaultFirebaseOptions.currentPlatform), // 用于Firestore的适配器,可以通过给定选项切换连接目的地。
home: const ScopedTestPage(),
title: "Flutter Demo",
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
)
其他用法
集合查询
您可以像Firestore一样过滤collection query
中的元素。
通过连接CollectionModelQuery
(在创建集合对象时传递)的各种方法,可以指定过滤条件。
final collection = ModelCollection(
const CollectionModelQuery(
"/user",
).greaterThanOrEqual("born", 1900)
);
在每个方法的key
中指定目标字段名称(**存储在DB上;在toMap
转换后为Map<String, dynamic>
的键)。
可以指定以下过滤条件: 可以连接多个方法,但某些组合可能因适配器而不可用。
equal(key, value)
:仅返回值等于指定值的文档。notEqual(key, value)
:返回值不等于指定值的文档。lessThan(key, value)
:返回值低于指定值的文档。lessThanOrEqual(key, value)
:返回值等于或低于指定值的文档。greaterThan(key, value)
:返回值高于指定值的文档。greaterThanOrEqual(key, value)
:返回值等于或高于指定值的文档。contains(key, value)
:如果目标值是列表格式,则返回包含指定值的文档。containsAny(key, values)
:如果目标值是列表格式,则返回包含指定列表之一的文档。where(key, values)
:返回目标值包含在给定列表中的文档。notWhere(key, values)
:返回目标值未包含在指定列表中的文档。isNull(key)
:返回目标值为null的文档。isNotNull(key)
:返回目标值不为null的文档。
还可以对值进行排序并限制获取数量。
orderByAsc(key)
:按指定键的升序对值进行排序。orderByDesc(key)
:按指定键的降序对值进行排序。limitTo(value)
:如果指定了数字,即使集合中存在更多文档,也将限制集合中文档的数量为指定数量。
文本搜索
使用Bigram在Firestore内创建可搜索的数据结构,从而在Firestore内的集合中实现文本搜索(Like搜索)。
(也适用于RuntimeModelAdapter
和LocalModelAdapter
)
首先,将SearchableDocumentMixin<T>
混入要创建的文档中。
此时,定义buildSearchText
以创建要搜索的文本。
在下面的示例中,搜索包含在name
和text
字段中的字符串。
class SearchableMapDocument extends DocumentBase<Map<String, dynamic>>
with SearchableDocumentMixin<Map<String, dynamic>> {
SearchableMapDocument(super.query);
[@override](/user/override)
Map<String, dynamic> fromMap(Map<String, dynamic> map) => map;
[@override](/user/override)
Map<String, dynamic> toMap(Map<String, dynamic> value) => value;
[@override](/user/override)
String buildSearchText(DynamicMap value) {
return (value["name"] ?? "") + (value["text"] ?? "");
}
}
接下来,将SearchableCollectionMixin<TDocument>
混入要搜索的集合中。
在这种情况下,TDocument
必须混入SearchableDocumentMixin<T>
。
class SearchableMapCollection
extends CollectionBase<SearchableMapDocument>
with SearchableCollectionMixin<SearchableMapDocument> {
SearchableMapCollection(super.query);
[@override](/user/override)
SearchableMapDocument create([String? id]) {
return SearchableMapDocument(
modelQuery.create(id),
{},
);
}
}
现在你可以开始了。
可以通过将所需数据传递给SearchableMapDocument
,保存它,然后使用SearchableMapCollection
的search
方法来执行搜索。
final query = CollectionModelQuery("user");
final collection = SearchableMapCollection(query);
final queryMasaru = DocumentModelQuery("user/masaru");
final modelMasaru = SearchableMapDocument(queryMasaru);
await modelMasaru.save({
"name": "masaru",
"text": "vocaloid producer",
});
final queryHirose = DocumentModelQuery("user/hirose");
final modelHirose = SearchableMapDocument(queryHirose);
await modelHirose.save({
"name": "hirose",
"text": "flutter engineer",
});
await collection.search("hirose");
print(collection); // [{ "name": "hirose", "text": "flutter engineer",}]
事务
可以像Firestore的transaction
功能一样执行事务。
可以将多个文档的更新合并为一个,并实现每个文档相互注册对方的信息
的关注/关注功能。
要执行事务,必须执行文档或集合的transaction()
方法以创建ModelTransactionBuilder
。
生成的ModelTransactionBuilder
可以原样执行,事务处理描述在其回调中。
回调传递ModelTransactionRef
和原始文档(集合)。
使用ModelTransactionRef.read(document)
将文档转换为ModelTransactionDocument
。
ModelTransactionDocument
可以加载(load()
)、保存(save(T value)
)和删除(delete()
)数据。
但是一定要先加载数据(load()
),然后再保存(save(T value)
)和删除(delete()
)。
保存和删除过程在ModelTransactionBuilder
回调过程完成后执行,并且可以等待await
完成。
final myDocument = ModelDocument(const DocumentModelQuery("/user/me/follow/you"));
final yourDocument = ModelDocument(const DocumentModelQuery("/user/you/follower/me"));
final transaction = myDocument.transaction();
await transaction(
(ref, doc) {
final myDoc = ref.read(doc);
final yourDoc = ref.read(yourDocument);
myDoc.save({"to": "you"});
yourDoc.save({"from": "me"});
},
);
print(myDocument.value); // {"to": "you"}
print(yourDocument.value); // {"from": "me"}
可以使用DocumentBase
的extension
来组织事务处理。
extension FollowFollowerExtensions on DocumentBase<Map<String, dynamic>> {
Future<void> follow(DocumentBase<Map<String, dynamic>> target) async {
final tr = transaction();
await tr(
(ref, doc) {
final me = ref.read(doc);
final tar = ref.read(target);
me.save({"to": tar["id"]});
tar.save({"from": me["id"]});
},
);
}
}
final myDocument = ModelDocument(const DocumentModelQuery("/user/me/follow/you"));
final yourDocument = ModelDocument(const DocumentModelQuery("/user/you/follower/me"));
await myDocument.follow(yourDocument);
批处理
可以像Firestore的batch
功能一样执行批处理。
可以一次运行多个文档以获得更高的性能。
在需要一次更新数千或数万条数据时执行。
批处理需要执行文档或集合的batch()
方法以生成ModelBatchBuilder
。
生成的ModelBatchBuilder
可以原样执行,批处理描述在其回调中。
回调传递ModelBatchRef
和原始文档(集合)。
使用ModelBatchRef.read(document)
将文档转换为ModelBatchDocument
。
ModelBatchDocument
可以保存(save(T value)
)和删除(delete()
)数据。
保存和删除过程在ModelBatchBuilder
回调过程完成后执行,并且可以等待await
完成。
final myDocument = ModelDocument(const DocumentModelQuery("/user/me/follow/you"));
final yourDocument = ModelDocument(const DocumentModelQuery("/user/you/follower/me"));
final batch = myDocument.batch();
await batch(
(ref, doc) {
final myDoc = ref.read(doc);
final yourDoc = ref.read(yourDocument);
myDoc.save({"to": "you"});
yourDoc.save({"from": "me"});
},
);
print(myDocument.value); // {"to": "you"}
print(yourDocument.value); // {"from": "me"}
特殊字段值
Firestore提供了一些FieldValues
。简而言之,通过将FieldValue
传递给客户端,服务器端可以处理客户端无法完全处理的过程并使其正常工作。
Katana_model为此提供了一些特殊的字段值。
ModelCounter
- 对应于
FieldValue.increment
。可以用来确保“Like”功能中的“赞”计数。 - 可以使用
increment(int i)
方法增加或减少i
的值。
- 对应于
ModelTimestamp
- 对应于
FieldValue.serverTimestamp
。当你想在服务器同步的时间点存储时间戳时使用。 - 可以通过传递一个值作为参数来指定日期,但会在传递到服务器时同步到服务器的时间。
- 对应于
const query = DocumentModelQuery("/test/doc");
final model = ModelDocument(query);
await model.save({
"counter": const ModelCounter(0),
"time": ModelTimestamp(DateTime(2022, 1, 1))
});
print((model.value!["counter"] as ModelCounter).value); // 0
print((model.value!["time"] as ModelTimestamp).value); // DateTime(2022, 1, 1)
await model.save({
"counter": (model.value!["counter"] as ModelCounter).increment(1),
"time": ModelTimestamp(DateTime(2022, 1, 2))
});
print((model.value!["counter"] as ModelCounter).value); // 1
print((model.value!["time"] as ModelTimestamp).value); // DateTime(2022, 1, 2)
在使用freezed
时,使用ModelCounter
和ModelTimestamp
类型的实例来定义。
[@freezed](/user/freezed)
class UserValue with _$UserValue {
const factory UserValue({
required String first,
required String last,
@Default(1900) int born
@Default(ModelCounter(0)) ModelCounter likeCount,
@Default(ModelTimestamp()) ModelTimestamp createdTime,
}) = UserValue;
factory UserValue.fromJson(Map<String, Object?> json)
=> _$UserValueFromJson(json);
}
class UserValueDocument extends DocumentBase<UserValue> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
UserValue fromMap(Map<String, dynamic> map) => UserValue.fromJson(map);
[@override](/user/override)
Map<String, dynamic> toMap(UserValue value) => value.toJson();
}
final userDocument = UserValueDocument(const DocumentModelQuery("user/masaru"));
await userDocument.load();
print(userDocument.value.likeCount.value); // 0
await userDocument.save(
userDocument.value.copyWith(
likeCount: userDocument.value.likeCount.increment(1),
)
);
print(userDocument.value.likeCount.value); // 1
引用字段
例如,假设你正在user
集合中管理用户数据,并在shop
集合中存储数据。
如果你希望通过user
定义shop
管理员,那么从shop
文档中引用相关的user
文档会更高效,这样user
的变化可以反映到shop
中。
Firestore有一个Reference
类型,它指向另一个文档,允许客户端读取额外的数据。
Katana_model以预先声明的形式定义它们之间的关系,并自动读取数据。
首先,为引用文档混入ModelRefMixin<T>
。
class UserDocument extends DocumentBase<Map<String, dynamic>>
with ModelRefMixin<Map<String, dynamic>> {
UserDocument(super.query);
[@override](/user/override)
Map<String, dynamic> fromMap(Map<String, dynamic> map) => map;
[@override](/user/override)
Map<String, dynamic> toMap(Map<String, dynamic> value) => value;
}
然后,为引用文档混入ModelRefLoaderMixin<T>
并实现List<ModelRefBuilderBase<TSource>> get builder
。
为List<ModelRefBuilderBase<TSource>> get builder
定义一组ModelRefBuilder<TSource, TResult>
。定义哪个文档从字段的引用类型传递到这个ModelRefBuilder
中的哪些值。
下面的示例定义了一个将UserDocument
放入ShopDocument
名为user
的字段中的定义。
class ShopDocument extends DocumentBase<Map<String, dynamic>>
with ModelRefLoaderMixin<Map<String, dynamic>> {
ShopDocument(super.query);
[@override](/user/override)
Map<String, dynamic> fromMap(Map<String, dynamic> map) => map;
[@override](/user/override)
Map<String, dynamic> toMap(Map<String, dynamic> value) => value;
[@override](/user/override)
List<ModelRefBuilderBase<DynamicMap>> get builder => [
ModelRefBuilder(
modelRef: (value) => value.getAsModelRef("user", "/user/doc"),
document: (modelQuery) => UserDocument(modelQuery),
value: (value, document) {
return {
...value,
"user": document,
};
},
),
];
}
数据现在将自动获取并实时更新,如下所示。
// {
// "user/doc": {"name": "user_name", "text": "user_text"},
// "shop/doc": {"name": "shop_name", "text": "shop_text"}
// }
final user = UserDocument(const DocumentModelQuery("user/doc"));
final shop = ShopDocument(const DocumentModelQuery("shop/doc"));
await user.load();
await shop.load();
print(user.value); // {"name": "user_name", "text": "user_text"}
print(shop.value); // {"name": "shop_name", "text": "shop_text", "user": UserDocument({"name": "user_name", "text": "user_text"})}
如果你想创建一个新的引用,创建一个ModelRef<T>
并传递进去。
shop.value = {
...shop.value,
"user": ModelRef<Map<String, dynamic>>(const DocumentModelQuery("user/doc2")),
};
在使用freezed时,定义ModelRef<T>
本身的类型。
它不是常量,所以不能使用@Default
设置初始值。添加required
或?
使其可选。
ModelRefBuilder
可以更简洁地编写。
[@freezed](/user/freezed)
class UserValue with _$UserValue {
const factory UserValue({
required String name,
required String text,
}) = UserValue;
factory UserValue.fromJson(Map<String, Object?> json)
=> _$UserValueFromJson(json);
}
[@freezed](/user/freezed)
class ShopValue with _$ShopValue {
const factory ShopValue({
required String name,
required String text,
ModelRef<UserValue>? user,
}) = ShopValue;
factory ShopValue.fromJson(Map<String, Object?> json)
=> _$ShopValueFromJson(json);
}
class UserValueDocument extends DocumentBase<UserValue> with ModelRefMixin<UserValue> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
UserValue fromMap(Map<String, dynamic> map) => UserValue.fromJson(map);
[@override](/user/override)
Map<String, dynamic> toMap(UserValue value) => value.toJson();
}
class ShopValueDocument extends DocumentBase<ShopValue> with ModelRefLoaderMixin<ShopValue> {
DynamicMapDocument(super.modelQuery);
[@override](/user/override)
UserValue fromMap(Map<String, dynamic> map) => UserValue.fromJson(map);
[@override](/user/override)
Map<String, dynamic> toMap(UserValue value) => value.toJson();
[@override](/user/override)
List<ModelRefBuilderBase<ShopValue>> get builder => [
ModelRefBuilder(
modelRef: (value) => value.user,
document: (modelQuery) => UserValueDocument(modelQuery),
value: (value, document) {
return value.copyWith(
user: document,
);
},
),
];
}
// {
// "user/doc": {"name": "user_name", "text": "user_text"},
// "shop/doc": {"name": "shop_name", "text": "shop_text"}
// }
final user = UserValueDocument(const DocumentModelQuery("user/doc"));
final shop = ShopValueDocument(const DocumentModelQuery("shop/doc"));
await user.load();
await shop.load();
print(user.value); // {"name": "user_name", "text": "user_text"}
print(shop.value); // {"name": "shop_name", "text": "shop_text", "user": UserValueDocument({"name": "user_name", "text": "user_text"})}
单元测试
如果要对涉及模型的逻辑部分进行单元测试,请使用RuntimeModelAdapter
。
RuntimeModelAdapter
有一个内部的NoSqlDatabase
,其中存储了所有数据。
NoSqlDatabase
可以作为数据库参数传递给RuntimeModelAdapter
,以便在测试中处理封闭的数据。
此外,可以将rawData
传递给RuntimeModelAdapter
,因此可以在那里设置初始值。
test("runtimeDocumentModel.test", () async {
final adapter = RuntimeModelAdapter(
database: NoSqlDatabase(),
rawData: const {
"test/doc": {"name": "aaa", "text": "bbb"},
},
);
final query = DocumentModelQuery("test/doc", adapter: adapter);
final document = ModelDocument(query);
await document.load();
expect(document.value, {"name": "aaa", "text": "bbb"});
});
更多关于Flutter本地模型管理插件katana_model_local的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter本地模型管理插件katana_model_local的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何使用Flutter本地模型管理插件katana_model_local
的代码案例。这个插件允许你在Flutter应用中轻松管理本地机器学习模型。
首先,确保你已经在你的pubspec.yaml
文件中添加了katana_model_local
依赖:
dependencies:
flutter:
sdk: flutter
katana_model_local: ^最新版本号 # 请替换为实际的最新版本号
然后运行flutter pub get
来安装依赖。
接下来,我们编写一些代码来演示如何使用这个插件。
1. 导入插件
在你的Dart文件中导入katana_model_local
插件:
import 'package:katana_model_local/katana_model_local.dart';
2. 初始化插件
在使用插件之前,需要初始化它。通常,你可以在应用的main.dart
文件中进行初始化:
void main() {
WidgetsFlutterBinding.ensureInitialized();
// 初始化KatanaModelLocal
KatanaModelLocal.instance.init(
modelsDirectory: 'path/to/your/models', // 指定模型存放的目录
).then((_) {
runApp(MyApp());
}).catchError((error) {
print('Failed to initialize KatanaModelLocal: $error');
});
}
3. 加载模型
假设你有一个TensorFlow Lite模型文件model.tflite
,你可以使用以下代码加载它:
import 'package:flutter/material.dart';
import 'package:katana_model_local/katana_model_local.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('KatanaModelLocal Example'),
),
body: Center(
child: FutureBuilder<void>(
future: loadModel(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
return Text('Failed to load model: ${snapshot.error}');
} else {
return Text('Model loaded successfully!');
}
},
),
),
),
);
}
Future<void> loadModel() async {
try {
// 加载模型
var model = await KatanaModelLocal.instance.loadModel(
modelName: 'my_model', // 模型的名称
modelFilePath: 'path/to/your/models/model.tflite', // 模型文件的路径
);
// 在这里,你可以使用model对象进行推理等操作
print('Model loaded: $model');
} catch (error) {
print('Error loading model: $error');
throw error;
}
}
}
4. 使用模型进行推理(假设模型是图像分类模型)
这里我们假设你有一个图像分类模型,并且你需要对一张图片进行推理。你需要将图片转换为模型可以接受的输入格式(例如,TensorFlow Lite通常接受一个浮点数组)。
以下是一个简单的示例,演示如何进行推理(注意:这里的代码是假设性的,具体实现取决于你的模型输入要求):
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:katana_model_local/katana_model_local.dart';
import 'package:tflite_flutter/tflite_flutter.dart'; // 假设你使用tflite_flutter进行推理
// 假设的输入处理函数(将图像转换为模型输入)
Uint8List processImage(ui.Image image) {
// 这里你需要将图像转换为模型可以接受的格式
// 例如,调整图像大小、归一化等
// 这里只是一个占位符
return Uint8List(0);
}
Future<List<String>> classifyImage(Uint8List input) async {
// 假设你已经加载了模型,并且它返回一个包含类别概率的列表
var interpreter = await Interpreter.fromAsset('path/to/your/models/model.tflite');
var outputTensor = TensorBuffer.createFixedSize(interpreter.getOutputTensor(0).shape, DataType.float32);
interpreter.run(input, outputTensor.buffer.asUint8List());
// 处理输出(假设输出是类别概率)
var probabilities = outputTensor.getFloatArray();
var sortedProbabilities = probabilities
.asMap()
.entries
.sortedByDescending((a, b) => b.value.compareTo(a.value))
.map((e) => e.key)
.toList();
// 假设你有一个类别名称列表
var labels = ['cat', 'dog', 'bird', ...]; // 用你的实际标签替换
return sortedProbabilities.map((index) => labels[index]).take(5).toList(); // 返回前5个概率最高的类别
}
// 在你的UI中调用
Future<void> classifyAndShowResult(ui.Image image) async {
var input = processImage(image);
var result = await classifyImage(input);
print('Classification result: $result');
// 显示结果,例如使用Snackbar或对话框
}
注意:
processImage
函数需要根据你的模型输入要求来实现。classifyImage
函数中的Interpreter.fromAsset
是假设性的,因为katana_model_local
本身不提供推理功能,但你可以使用tflite_flutter
或其他推理库。- 在实际使用中,你可能需要处理更多的错误和异常情况。
希望这个代码案例能帮助你理解如何在Flutter中使用katana_model_local
插件进行本地模型管理。