Docs 菜单
Docs 主页
/ /
Atlas App Services

教程:适用于 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 应用。

1

用代码编辑器打开 Flutter 应用程序。

如果您将客户端下载为 .zip 文件或已克隆客户端 GitHub 存储库,则须手动将 App Services App ID 插入客户端中的相应位置。按照客户端 README.md 中的 Configuration 说明来了解在何处插入 App ID。

2

在代码编辑器中,花几分钟时间探索项目的组织方式。 这是一个标准的多平台 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/
应用页面。
3

无需对代码进行任何更改,您应该能够在 Android 模拟器、iOS 模拟器、物理移动设备或桌面模拟器中运行该应用。

首先,通过在终端中运行以下命令来安装所有依赖项:

flutter pub get

然后,连接到设备并运行 Flutter 应用程序。

此应用程序运行后,注册一个新的用户帐户,然后将新项目添加到待办事项列表中。

提示

有关使用开发工具运行Flutter应用的更多信息,请参阅 Flutter Test Drive 文档。

4

登录 MongoDB Atlas。 在 Data Services 标签页中,单击 Browse Collections。在数据库列表中,找到并展开 todo 数据库,然后找到并展开 Item 集合。您应该会看到在此集合中创建的文档。

1

现在您已经确认一切都按预期进行,我们可以添加更改。在本教程中,我们决定为每个事项添加“priority”属性,以便我们可以按事项的优先级过滤事项。

为此,请按照以下步骤操作:

  1. flutter.todo.flex 项目中,打开文件 lib/realm/schemas.dart

  2. 将以下属性添加到 _Item 类:

    lib/realm/schemas.dart
    late int? priority;
  3. 重新生成 Item Realm 对象类:

    dart run realm generate
2
  1. 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();
    }
    @override
    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;
    }
  2. 添加一个新文件以包含用于设置项目优先级的控件。创建文件 lib/components/select_priority.dart

    lib/components/select_priority.dart
    import '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);
    @override
    State<SelectPriority> createState() => _SelectPriorityState();
    }
    class _SelectPriorityState extends State<SelectPriority> {
    @override
    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")),
    ],
    ),
    ],
    ),
    );
    }
    }
  3. 现在,将 SelectPriority 小组件添加到 CreateItemModifyItem 小组件中。您还需添加某些其他逻辑处理优先级的设置。您必须添加的代码如下所示。

    待添加文件的某些部分将替换为以下代码示例中的注释,从而重点关注已更改的相关代码部分。

    编辑 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);
    @override
    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
    @override
    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);
    @override
    _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);
    @override
    void initState() {
    _summaryController = TextEditingController(text: item.summary);
    _isCompleteController = ValueNotifier<bool>(item.isComplete)
    ..addListener(() => setState(() {}));
    _priority = widget.item.priority;
    super.initState();
    }
    @override
    void dispose() {
    _summaryController.dispose();
    _isCompleteController.dispose();
    super.dispose();
    }
    @override
    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);
    }
    }
  4. 现在,在 lib/components/todo_item.dartItemCard 小组件中添加一个可视化优先级指示器。创建新的小组件 _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);
    @override
    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();
    }
    }
    @override
    Widget build(BuildContext context) {
    return getIconForPriority(priority);
    }
    }
3

再次运行应用程序之前,请执行热重启。这可确保同步会话使用新架构重新启动,并防止同步错误。

然后,使用本教程前面创建的帐户登录。您将看到先前创建的一个列项。添加新列项,您将看到现在可以设置优先级。为优先级选择 High 并保存条目。

现在切换回浏览器中的 Atlas 数据页面,并刷新 Item 集合。您现在应该会看到添加了 priority 字段并将其设置为1 的新项目。您还会注意到,现有项目现在也有一个priority 字段,并且它设置为 null,如以下屏幕截图所示:

集合中的两个项目
点击放大

注意

为什么没有中断同步?

向 Realm 对象添加属性不是一项重大更改,因此不需要重置客户端。模板应用启用了开发模式,因此对客户端 Realm 对象的更改会反映在服务器端模式中。有关更多信息,请参阅开发模式更新数据模型

现在,我们已添加了优先级字段,我们想要更新 Device Sync 订阅,仅同步优先级标记为 High(高)或 Severe(严重)的项。

1

lib/realm/realm_services.dart文件中,我们定义了灵活同步订阅,其中定义了我们与用户设备和帐户同步的文档。目前,我们正在同步owner属性与经过身份验证的用户匹配的所有文档。

当前订阅:

lib/realm/realm_services.dart
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"

更新订阅,删除旧订阅,并添加使用优先级的新订阅:

lib/realm/realm_services.dart
// ... 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
}
2

再次运行应用程序。使用本教程前面创建的帐户登录。

在 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"

lib/realm/realm_services.dart
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 对象添加属性是一项非重大更改,并且开发模式可确保模式更改反映在服务器端。

注意

分享反馈

怎么样?使用页面右下角的 Rate this page 小组件来评价其有效性。如果有任何问题,也可以在 Github 存储库 上提交问题。

来年

什么是 Atlas App Services?