预计完成时间: 30分钟,具体取决于您使用 Flutter 的经验
适用于 Flutter 的Atlas Device SDK允许您使用Dart和 Flutter 创建多平台应用程序。 本教程基于名为 flutter.todo.flex的 Flutter Flexible Sync模板应用,该应用演示了待办事项应用程序的创建。 该应用程序使用户能够:
将他们的电子邮件注册为新用户帐户。
使用电子邮件和密码登录他们的帐户(稍后退出)。
查看、创建、修改和删除任务项目。
查看所有任务,即使用户不是所有者。
模板应用还提供了一个切换开关,用于模拟处于“离线模式”的设备。此切换开关可以让您快速测试 Device Sync 功能,模拟没有互联网连接的用户。但是,您可能会在生产应用程序中删除此切换开关。
本教程将附加到模板应用中。您可向现有 Item 模型添加一个新的 priority 字段,并更新 Flexible Sync 订阅以仅显示优先级范围内的项目。
学习目标
本教程说明如何根据自己的需要调整模板应用。考虑到模板应用的当前结构,您不一定会进行该更改。
在本教程中,您将学习如何:
使用非重大更改更新 Realm 对象模型。
更新 Device Sync 订阅
将可查询字段添加到服务器上的 Device Sync 配置中,以更改要同步的数据。
如果更想开始使用自己的应用程序而不是跟随引导式教程,请查看 Flutter 快速入门。 其中包括可复制的代码示例以及设立Flutter SDK应用程序所需的基本信息。
先决条件
您应具备将 Flutter 应用部署到 Android 模拟器、iOS 模拟器和/或物理设备的相关经验。
本教程从“模板应用程序”开始。创建“模板应用程序”需要 Atlas 帐号、API 密钥和 App Services CLI。
要学习;了解有关创建Atlas帐户的更多信息,请参阅Atlas入门。对于本教程,您需要一个具有免费层级集群的Atlas帐户。
您还需要一个用于登录的 MongoDB Cloud 账户的 Atlas API 密钥。您必须是项目所有者才能使用 App Services CLI 创建模板应用。
要了解有关安装 App Services CLI 的更多信息,请参阅安装 App Services CLI 。安装后,使用 Atlas 项目的 API 密钥运行登录命令。
注意
支持的平台
可以在以下平台上构建本教程应用程序:
iOS 版
Android
macOS
Windows
Linux
Flutter SDK 不支持构建网络应用程序。
从模板开始
本教程基于名为 flutter.todo.flex 的 Flutter 灵活同步模板应用程序。我们会先使用默认应用程序,并在其上构建新功能。
要了解有关模板应用的更多信息,请参阅模板应用。
如果您还没有 Atlas 帐号,请注册以部署“模板应用程序”。
按照“创建App Services App”指南中描述的步骤操作,然后选择 Create App from Template 。 选择Real-time Sync模板。 这将创建一个预先配置为与Device Sync模板应用客户端之一一起使用的App Services App 。
创建模板应用后,用户界面会显示一个标有 Get the Front-end Code for your Template 的模态窗口。此模态窗口提供有关将模板应用客户端代码下载为 .zip 文件或使用 App Services CLI 获取客户端的说明。
选择 .zip 或 App Services CLI 方法后,请按照屏幕上的说明获取客户端代码。对于本教程,请选择 Dart (Flutter) 客户端代码。
解压下载的应用,您会看到 Flutter 应用。
注意
默认的Windows ZIP 实用程序可能会显示 .zip 文件为空。 如果遇到这种情况,请使用可用的第三方 zip 程序。
appservices apps create 命令设置后端,并创建一个 Flutter 模板应用,供您用作本教程的基础。
在终端窗口中运行以下命令,创建一个名为“MyTutorialApp”的应用,将该应用部署在 US-VA 地区,并将其环境设置为“开发”(而不是生产或 QA)。
appservices app create \ --name MyTutorialApp \ --template flutter.todo.flex \ --deployment-model global \ --environment development
该命令在当前路径中创建一个新目录,其名称与 --name 标志的值相同。
您可以创建分支并克隆包含Device Sync客户端代码的Github存储库。 The Flutter客户端代码位于 https://github.com/mongodb/template-app-dart-flutter-todo。
如果您使用此过程来获取客户端代码,则必须创建一个模板应用以配合客户端使用。按照创建模板应用中的说明操作,使用 Atlas App Services 用户界面、App Services CLI 或 Admin API 创建 Device Sync 后端模板应用。
克隆存储库并按照 README 中的说明操作,将后端应用 ID 添加到 Flutter 应用。
设置模板应用程序
探索应用结构
在代码编辑器中,花几分钟时间探索项目的组织方式。 这是一个标准的多平台 Flutter应用程序,已根据我们的特定用途进行了修改。 具体来说,以下文件包含Flutter SDK的重要用法:
file | 用途 |
|---|---|
| 应用的入口点。包含路由和状态管理。 |
| 定义“Realm 数据库”模式。 |
| 生成的 Realm 对象类。 |
| 处理与 Atlas App Services 的交互。 |
| 处理与 Realm Database 和 Atlas Device Sync 的交互。 |
| 以 Flutter 小部件为特色的应用程序组件。 |
| 应用页面。 |
运行应用
无需对代码进行任何更改,您应该能够在 Android 模拟器、iOS 模拟器、物理移动设备或桌面模拟器中运行该应用。
首先,通过在终端中运行以下命令来安装所有依赖项:
flutter pub get
然后,连接到设备并运行 Flutter 应用程序。
此应用程序运行后,注册一个新的用户帐户,然后将新项目添加到待办事项列表中。
提示
有关使用开发工具运行 Flutter 应用的更多信息,请参阅 Flutter Test Drive 文档。
检查后端
登录 MongoDB Atlas。 在 Data Services 标签页中,单击 Browse Collections。在数据库列表中,找到并展开 todo 数据库,然后找到并展开 Item 集合。您应该会看到在此集合中创建的文档。
修改应用程序
添加新属性
创建和更新项目时设置优先级
在
lib/realm/realm_services.dart中,添加逻辑以设置和更新priority。还要在RealmServices类下面添加一个PriorityLevel抽象类以限制可能的值。lib/realm/realm_services.dart// ... imports class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; // ... RealmServices initializer and updateSubscriptions(), // sessionSwitch() and switchSubscription() methods void createItem(String summary, bool isComplete, int? priority) { final newItem = Item(ObjectId(), summary, currentUser!.id, isComplete: isComplete, priority: priority); realm.write<Item>(() => realm.add<Item>(newItem)); notifyListeners(); } void deleteItem(Item item) { realm.write(() => realm.delete(item)); notifyListeners(); } Future<void> updateItem(Item item, {String? summary, bool? isComplete, int? priority}) async { realm.write(() { if (summary != null) { item.summary = summary; } if (isComplete != null) { item.isComplete = isComplete; } if (priority != null) { item.priority = priority; } }); notifyListeners(); } Future<void> close() async { if (currentUser != null) { await currentUser?.logOut(); currentUser = null; } realm.close(); } void dispose() { realm.close(); super.dispose(); } } abstract class PriorityLevel { static int severe = 0; static int high = 1; static int medium = 2; static int low = 3; } 添加一个新文件以包含用于设置项目优先级的控件。创建文件
lib/components/select_priority.dart。lib/components/select_priority.dartimport 'package:flutter/material.dart'; import 'package:flutter_todo/realm/realm_services.dart'; class SelectPriority extends StatefulWidget { int priority; void Function(int priority) setFormPriority; SelectPriority(this.priority, this.setFormPriority, {Key? key}) : super(key: key); State<SelectPriority> createState() => _SelectPriorityState(); } class _SelectPriorityState extends State<SelectPriority> { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Priority'), DropdownButtonFormField<int>( onChanged: ((int? value) { final valueOrDefault = value ?? PriorityLevel.low; widget.setFormPriority(valueOrDefault); setState(() { widget.priority = valueOrDefault; }); }), value: widget.priority, items: [ DropdownMenuItem( value: PriorityLevel.low, child: const Text("Low")), DropdownMenuItem( value: PriorityLevel.medium, child: const Text("Medium")), DropdownMenuItem( value: PriorityLevel.high, child: const Text("High")), DropdownMenuItem( value: PriorityLevel.severe, child: const Text("Severe")), ], ), ], ), ); } } 现在,将
SelectPriority小组件添加到CreateItem和ModifyItem小组件中。您还需添加某些其他逻辑处理优先级的设置。您必须添加的代码如下所示。待添加文件的某些部分将替换为以下代码示例中的注释,从而重点关注已更改的相关代码部分。
编辑
lib/components/create_item.dart中的CreateItemForm小组件:lib/components/create_item.dart// ... other imports import 'package:flutter_todo/components/select_priority.dart'; // ... CreateItemAction widget class CreateItemForm extends StatefulWidget { const CreateItemForm({Key? key}) : super(key: key); createState() => _CreateItemFormState(); } class _CreateItemFormState extends State<CreateItemForm> { final _formKey = GlobalKey<FormState>(); late TextEditingController _itemEditingController; int _priority = PriorityLevel.low; void _setPriority(int priority) { setState(() { _priority = priority; }); } // ... initState() and dispose() @override functions Widget build(BuildContext context) { TextTheme theme = Theme.of(context).textTheme; return formLayout( context, Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ // ... Text and TextFormField widgets SelectPriority(_priority, _setPriority), // ... Padding widget ], ), )); } void save(RealmServices realmServices, BuildContext context) { if (_formKey.currentState!.validate()) { final summary = _itemEditingController.text; realmServices.createItem(summary, false, _priority); Navigator.pop(context); } } } 编辑
lib/components/modify_item.dart中的ModifyItemForm小组件:lib/components/modify_item.dart// ... other imports import 'package:flutter_todo/components/select_priority.dart'; class ModifyItemForm extends StatefulWidget { final Item item; const ModifyItemForm(this.item, {Key? key}) : super(key: key); _ModifyItemFormState createState() => _ModifyItemFormState(item); } class _ModifyItemFormState extends State<ModifyItemForm> { final _formKey = GlobalKey<FormState>(); final Item item; late TextEditingController _summaryController; late ValueNotifier<bool> _isCompleteController; late int? _priority; void _setPriority(int priority) { setState(() { _priority = priority; }); } _ModifyItemFormState(this.item); void initState() { _summaryController = TextEditingController(text: item.summary); _isCompleteController = ValueNotifier<bool>(item.isComplete) ..addListener(() => setState(() {})); _priority = widget.item.priority; super.initState(); } void dispose() { _summaryController.dispose(); _isCompleteController.dispose(); super.dispose(); } Widget build(BuildContext context) { TextTheme myTextTheme = Theme.of(context).textTheme; final realmServices = Provider.of<RealmServices>(context, listen: false); return formLayout( context, Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: <Widget>[ // ... Text and TextFormField widgets SelectPriority(_priority ?? PriorityLevel.medium, _setPriority), // ... StatefulBuilder widget Padding( padding: const EdgeInsets.only(top: 15), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ cancelButton(context), deleteButton(context, onPressed: () => delete(realmServices, item, context)), okButton(context, "Update", onPressed: () async => await update( context, realmServices, item, _summaryController.text, _isCompleteController.value, _priority)), ], ), ), ], ))); } Future<void> update(BuildContext context, RealmServices realmServices, Item item, String summary, bool isComplete, int? priority) async { if (_formKey.currentState!.validate()) { await realmServices.updateItem(item, summary: summary, isComplete: isComplete, priority: priority); Navigator.pop(context); } } void delete(RealmServices realmServices, Item item, BuildContext context) { realmServices.deleteItem(item); Navigator.pop(context); } } 现在,在
lib/components/todo_item.dart的ItemCard小组件中添加一个可视化优先级指示器。创建新的小组件_PriorityIndicator,用于提供列项优先级的可视化指示器。_PriorityIndicator将刚刚创建的
_PriorityIndicator小组件添加到TodoItem小组件中。lib/components/todo_item.dart// ... imports enum MenuOption { edit, delete } class TodoItem extends StatelessWidget { final Item item; const TodoItem(this.item, {Key? key}) : super(key: key); Widget build(BuildContext context) { final realmServices = Provider.of<RealmServices>(context); bool isMine = (item.ownerId == realmServices.currentUser?.id); return item.isValid ? ListTile( // ... leading property and child content title: Row( children: [ Padding( padding: const EdgeInsets.only(right: 8.0), child: _PriorityIndicator(item.priority), ), SizedBox(width: 175, child: Text(item.summary)), ], ), // ... subtitle, trailing, and shape properties with child content ) : Container(); } // ... handleMenuClick() function } class _PriorityIndicator extends StatelessWidget { final int? priority; const _PriorityIndicator(this.priority, {Key? key}) : super(key: key); Widget getIconForPriority(int? priority) { if (priority == PriorityLevel.low) { return const Icon(Icons.keyboard_arrow_down, color: Colors.blue); } else if (priority == PriorityLevel.medium) { return const Icon(Icons.circle, color: Colors.grey); } else if (priority == PriorityLevel.high) { return const Icon(Icons.keyboard_arrow_up, color: Colors.orange); } else if (priority == PriorityLevel.severe) { return const Icon( Icons.block, color: Colors.red, ); } else { return const SizedBox.shrink(); } } Widget build(BuildContext context) { return getIconForPriority(priority); } }
运行和测试
再次运行应用程序之前,请执行热重启。这可确保同步会话使用新架构重新启动,并防止同步错误。
然后,使用本教程前面创建的帐户登录。您将看到先前创建的一个列项。添加新列项,您将看到现在可以设置优先级。为优先级选择 High 并保存条目。
现在切换回浏览器中的 Atlas 数据页面,并刷新 Item 集合。您现在应该会看到添加了 priority 字段并将其设置为1 的新项目。您还会注意到,现有项目现在也有一个priority 字段,并且它设置为 null,如以下屏幕截图所示:

更改订阅
现在,我们已添加了优先级字段,我们想要更新 Device Sync 订阅,仅同步优先级标记为 High(高)或 Severe(严重)的项。
更新订阅
在lib/realm/realm_services.dart文件中,我们定义了灵活同步订阅,其中定义了我们与用户设备和帐户同步的文档。目前,我们正在同步owner属性与经过身份验证的用户匹配的所有文档。
当前订阅:
Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>(r'owner_id == $0', [currentUser?.id]), name: queryMyItemsName); } }); await realm.subscriptions.waitForSynchronization(); }
现在,我们将更改订阅,仅同步优先级为 High(高)或 Severe(严重)的项。您可能还记得,优先级字段的类型为 int,其中最高优先级 ("Severe") 的值为 0,最低优先级 ("Low") 的值为 3。
我们可以在 int 和 priority 属性之间进行直接比较。为此,我们将重构订阅查询以包含优先级小于或等于 PriorityLevel.high(或 1)的项目。我们还将为订阅指定新名称 "getMyHighPriorityItemsSubscription"。
更新订阅,删除旧订阅,并添加使用优先级的新订阅:
// ... imports class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; static const String queryMyHighPriorityItemsName = "getMyHighPriorityItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; RealmServices(this.app) { if (app.currentUser != null || currentUser != app.currentUser) { currentUser ??= app.currentUser; realm = Realm(Configuration.flexibleSync(currentUser!, [Item.schema])); showAll = (realm.subscriptions.findByName(queryAllName) != null); // Check if subscription previously exists on the realm final subscriptionDoesNotExists = (realm.subscriptions.findByName(queryMyHighPriorityItemsName) == null); if (realm.subscriptions.isEmpty || subscriptionDoesNotExists) { updateSubscriptions(); } } } Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>( r'owner_id == $0 AND priority <= $1', [currentUser?.id, PriorityLevel.high], ), name: queryMyHighPriorityItemsName); } }); await realm.subscriptions.waitForSynchronization(); } // ... other methods }
运行和测试
再次运行应用程序。使用本教程前面创建的帐户登录。
在 Realm 重新同步文档集合的初始时刻后,可能会看到类似于以下内容的错误消息:
The following RangeError was thrown building StreamBuilder<RealmResultsChanges<Item>>(dirty, state: _StreamBuilderBaseState<RealmResultsChanges<Item>, AsyncSnapshot<RealmResultsChanges<Item>>>#387c4): RangeError (index): Invalid value: Only valid value is 0: 3
更新订阅时,StreamBuilder 小组件可能会出现此错误。在生产应用程序中,可以添加错误处理功能。但就本教程而言,只需执行热刷新,错误就会消失。
现在,您应看到新创建的高优先级项目。
提示
在启用开发者模式的情况下更改订阅
在本教程中,当您首次更改优先级字段的订阅和查询时,该字段将自动添加到 Device Sync Collection Queryable Fields 中。出现这种情况是因为模板应用默认启用了开发模式。如果未启用开发模式,您必须手动将该字段添加为可查询字段,以便在客户端同步查询中使用它。
有关更多信息,请参阅可查询字段。
如果要进一步测试此功能,您可以创建各种优先级的项目。您将看到一个优先级较低的新项目短暂出现在项目列表中,继而又消失。Realm 会在本地创建该项目,并将其与后端进行同步,然后由于该项目不符合订阅规则而将其删除。这便是所谓的补偿写入。
您还会注意到,您最初创建的文档并没有同步,因为其优先级为 null。如果您希望同步此列项,可以在 Atlas UI 中编辑文档,然后为优先级字段添加值,或者更改订阅以包含具有空值的文档。我们还将为订阅指定新名称 "getUserItemsWithHighOrNoPriority"。
class RealmServices with ChangeNotifier { static const String queryAllName = "getAllItemsSubscription"; static const String queryMyItemsName = "getMyItemsSubscription"; static const String queryMyHighPriorityItemsName = "getMyHighPriorityItemsSubscription"; static const String queryMyHighOrNoPriorityItemsName = "getMyHighOrNoPriorityItemsSubscription"; bool showAll = false; bool offlineModeOn = false; bool isWaiting = false; late Realm realm; User? currentUser; App app; RealmServices(this.app) { if (app.currentUser != null || currentUser != app.currentUser) { currentUser ??= app.currentUser; realm = Realm(Configuration.flexibleSync(currentUser!, [Item.schema])); // Check if subscription previously exists on the realm final subscriptionDoesNotExists = realm.subscriptions.findByName(queryMyHighOrNoPriorityItemsName) == null; if (realm.subscriptions.isEmpty || subscriptionDoesNotExists) { updateSubscriptions(); } } } Future<void> updateSubscriptions() async { realm.subscriptions.update((mutableSubscriptions) { mutableSubscriptions.clear(); if (showAll) { mutableSubscriptions.add(realm.all<Item>(), name: queryAllName); } else { mutableSubscriptions.add( realm.query<Item>( r'owner_id == $0 AND priority IN {$1, $2, $3}', [currentUser?.id, PriorityLevel.high, PriorityLevel.severe, null], ), name: queryMyHighPriorityItemsName); } }); await realm.subscriptions.waitForSynchronization(); } // ... other methods }
同样,在首次使用新订阅打开应用程序发生 StreamBuilder 错误时,请执行热刷新,查看预期数据。
结论
向现有 Realm 对象添加属性是一项非重大更改,并且开发模式可确保模式更改反映在服务器端。
接下来的步骤
阅读我们的 Flutter SDK 文档。
在 MongoDB 开发者中心查找面向开发者的博客文章和集成教程。
加入Reddit或Stack Overflow上的MongoDB社区,向其他MongoDB开发者和技术专家学习;了解。
探索工程和专家团队提供的示例项目。