Flutter本地模型管理插件katana_model_local的使用

发布于 1周前 作者 h691938207 来自 Flutter

Flutter本地模型管理插件katana_model_local的使用

引言

在实现数据读写时,过程往往较为繁琐。通过RDB RestAPI,必须实现与数据库结构匹配的模式,即使只是为了本地存储数据,也需要大量工作来实现读写流程。

虽然像Firestore这样的强大数据库可以轻松地应用于移动和Web应用程序,但在某些情况下,它们对于特定类型的应用程序来说并不必要。根据我们开发各种应用的经验,我认为以下功能的模型足以创建90%以上的所有应用:

  • 能够执行CRUD(创建、读取、更新、删除)
  • 数据结构可以是任何Map(字典)类型对象及其列表
  • 支持类似搜索和简单的查询过滤
  • 提供测试、模拟、本地和远程数据库

我发现,通过提供与Firestore对齐接口的本地数据库和模拟测试数据库,同时以Firestore作为远程数据库的轴心,可以实现这些功能。

因此,我创建了以下包来实现它们:

  • 接口和数据结构简化为与Firestore匹配,使得使用起来更加简单。
  • 可以轻松地通过更改适配器在数据模拟、本地数据库和Firestore之间切换。
  • 提供事务处理功能,便于实现关注/关注功能以及简单的Like搜索(当然在Firestore中)。
  • 提供ClientJoin的简易集成,这是Firestore所需的进一步阅读引用文档中嵌入在特定文档字段中的文档引用。
  • 结构允许安全地实现Immutable类,如freezed
  • 继承自ChangeNotifier,易于与providerriverpod等一起使用。

模型实现

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.loadingFuture,可以通过将其直接传递给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)时添加到集合。

切换使用的数据库

通过更改传递给ModelAdapterScopeModelAdapter,可以从用于数据模拟的数据库切换到local DBFirestore

目前可用的适配器如下:

  • RuntimeModelAdapter
    • 仅在应用程序运行时存储数据的数据库适配器。
    • 如果应用程序停止或重启,所有数据都会丢失。
    • 应用程序启动时可以种植数据,可用于数据模拟测试
  • LocalModelAdapter
    • 存储在终端上的本地数据库适配器。
    • 即使应用程序停止或重启,数据也会保留。
    • 如果应用程序被删除或重新安装,数据将会丢失。
    • 存储在设备上的数据是加密的,只能由应用程序打开。
    • 在不希望或不需要使用服务器的情况下可用。
  • FirestoreModelAdapter
    • 存储在CloudFirestore中的数据库适配器。
    • 即使应用程序停止、重启、删除或重新安装,数据也会保留。
    • 适用于在服务器上存储数据并通过服务器与其他用户通信。
  • ListenableFirestoreModelAdapter
    • 存储在CloudFirestore中的数据库适配器。
    • 即使应用程序停止、重启、删除或重新安装,数据也会保留。
    • 适用于在服务器上存储数据并通过服务器与其他用户通信。
    • 使用Firestore的实时更新功能,可以立即将服务器端的更新传输到应用程序端。
    • 请使用它来实现聊天等功能。
  • CsvCollectionSourceModelAdapterCsvDocumentSourceModelAdapter
    • 可以处理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搜索)。 (也适用于RuntimeModelAdapterLocalModelAdapter

首先,将SearchableDocumentMixin<T>混入要创建的文档中。 此时,定义buildSearchText以创建要搜索的文本。 在下面的示例中,搜索包含在nametext字段中的字符串。

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,保存它,然后使用SearchableMapCollectionsearch方法来执行搜索。

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)将文档转换为ModelTransactionDocumentModelTransactionDocument可以加载(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"}

可以使用DocumentBaseextension来组织事务处理。

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)将文档转换为ModelBatchDocumentModelBatchDocument可以保存(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时,使用ModelCounterModelTimestamp类型的实例来定义。

[@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"})}

单元测试

如果要对涉及模型的逻辑部分进行单元测试,请使用RuntimeModelAdapterRuntimeModelAdapter有一个内部的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

1 回复

更多关于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或对话框
}

注意

  1. processImage函数需要根据你的模型输入要求来实现。
  2. classifyImage函数中的Interpreter.fromAsset是假设性的,因为katana_model_local本身不提供推理功能,但你可以使用tflite_flutter或其他推理库。
  3. 在实际使用中,你可能需要处理更多的错误和异常情况。

希望这个代码案例能帮助你理解如何在Flutter中使用katana_model_local插件进行本地模型管理。

回到顶部