教程:适用于 Flutter 的 Atlas Device Sync
在此页面上
预计完成时间: 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
标志的值相同。
Github您可以创建分支并克隆包含Device Sync 客户端代码的 存储库。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 | 用途 |
---|---|
lib/main.dart | 应用的入口点。包含路由和状态管理。 |
lib/realm/schemas.dart | 定义“Realm 数据库”模式。 |
lib/realm/schemas.realm.dart | 生成的 Realm 对象类。 |
lib/realm/app_services.dart | 处理与 Atlas App Services 的交互。 |
lib/realm/realm_services.dart | 处理与 Realm Database 和 Atlas Device Sync 的交互。 |
lib/components/ | 以 Flutter 小部件为特色的应用程序组件。 |
lib/screens/ | 应用页面。 |
运行应用
无需对代码进行任何更改,您应该能够在 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 开发者中心查找面向开发者的博客文章和集成教程。
加入 MongoDB Community 论坛,向其他 MongoDB 开发者和技术专家学习。
探索工程和专家团队提供的示例项目。