Docs 菜单

Docs 主页Atlas App Services

教程:Atlas Device Sync for Kotlin

在此页面上

  • 学习目标
  • 先决条件
  • 从模板开始
  • 设置模板应用程序
  • 打开应用
  • 探索应用结构
  • 运行应用
  • 检查后端
  • 修改应用程序
  • 添加新属性
  • 为模型添加新属性
  • 创建新事项时设置优先级
  • 运行和测试
  • 更改订阅
  • 更新订阅
  • 运行和测试
  • 结论
  • 接下来的步骤

预计完成时间: 30分钟,具体取决于您使用 Kotlin 的经验

Realm 提供了一个 Kotlin SDK,允许您使用 Jetpack Compose 通过 Kotlin 创建 Android 移动应用程序 。本教程基于名为 的 Kotlin Flexible Synckotlin.todo.flex 模板应用,它说明了如何创建待办事项列表管理应用程序。该应用程序使用户能够:

  • 将他们的电子邮件注册为新用户帐户。

  • 使用电子邮件和密码登录他们的帐户(稍后退出)。

  • 查看、创建、修改和删除自己的任务。

  • 查看所有任务,即使用户不是所有者。

模板应用还提供了一个切换开关,用于模拟处于“离线模式”的设备。此切换开关可以让您在模拟器上模拟没有互联网连接的用户,以快速测试 Device Sync 功能。但是,您可能会在生产应用程序中删除此切换开关。

本教程向 Template 应用添加功能。您将向现有 Item 模型添加一个新的 Priority 字段,并更新 Flexible Sync 订阅以仅显示优先级范围内的项目。这个示例说明如何根据自己的需要调整模板应用。

本教程说明如何根据自己的需要调整模板应用。

在本教程中,您将学习如何:

  • 使用非重大更改更新 Realm 对象模型。

  • 更新 Device Sync 订阅。

  • 将可查询字段添加到服务器上的 Device Sync 配置中,以更改要同步的数据。

注意

查看“快速入门”

如果更想从自己的应用程序开始,而不是跟随引导式教程,则请查看 Kotlin 快速入门。其中包括可复制的代码示例,以及设置 Atlas App Services 后端所需的基本信息。

  • Android Studio 大黄蜂2021 。1 。1 或更高版本。

  • JDK 11 或更高版本。

  • 适用于 Android Studio 的 Kotlin 插件,1.6.10 或更高版本。

  • 采用受支持 CPU 架构的 Android 虚拟设备 (AVD)。

  • 本教程从“模板应用程序”开始。创建“模板应用程序”需要 Atlas 帐号、API 密钥和应用程序服务。

    • 如需了解有关创建 Atlas 帐户的更多信息,请参阅 Atlas 入门文档。在本教程中,您需要一个带有免费层级集群的 Atlas 帐户。

    • 您还需要用于登录的 MongoDB Cloud 帐户的一个 API 密钥。您必须是项目所有者才能使用 appservices 创建模板应用。

    • 要了解有关安装 App Services 的详情,请参阅安装 App Services CLI。安装后,使用 Atlas 项目的 API 密钥运行登录命令。

本教程基于名为 kotlin.todo.flex 的 Kotlin SDK Flexible Sync 模板应用。我们从默认应用入手,并在该应用上构建新的功能。

要了解有关模板应用的更多信息,请参阅模板应用

如果您还没有 Atlas 帐号,请注册以部署“模板应用程序”。

1

在 Android Studio 中,打开位于模板应用的 frontend 文件夹中的 kotlin.todo.flex 文件夹。

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

2

在 Android Studio 为您的项目编制索引时,请花几分钟时间探索项目组织。在 app/java/com.mongodb.app 目录中,您可以看到一些值得注意的文件:

file
用途
ComposeItemActivity.kt
活动类,用于定义布局,并提供打开 Realm、将项目写入到 Realm、注销用户和关闭 Realm 的功能。
ComposeLoginActivity.kt
活动类,用于定义布局,并提供用户注册和登录功能。
TemplateApp.kt
初始化 App Services App 的类。

在本教程中,你将使用以下文件:

file
用途
Item.kt
位于 domain 目录中。定义我们在数据库中存储的 Realm 对象。
AddItem.kt
位于 ui/tasks 目录中。包含可组合函数以定义添加项目时使用的布局。
AddItemViewModel.kt
位于 presentation/tasks 目录中。包含业务逻辑并在添加项目时管理状态的视图模型。
SyncRepository.kt
位于 data 目录中。用于访问 Realm Sync 并定义 Flexible Sync 订阅的存储库。
Strings.xml
位于 res/values 目录中。定义应用中使用的文本字符串资源
3

无需对代码进行任何更改,您就能够在使用 Android Studio 的 Android 模拟器或物理设备上运行应用

运行应用,注册一个新用户帐户,然后将新事项添加到待办事项清单中。

4

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

1

您现已确认一切正常工作,我们可以添加更改了。在本教程中,我们决定为每个项目添加一个“priority”属性,以便我们可以按项目的优先级进行筛选。priority 属性将映射到 PriorityLevel 枚举以限制可能的值,我们将使用每个枚举的序数以对应于 priority 整数,以便我们稍后根据数字优先级进行查询。

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

  1. app/java/com.mongodb.app/domain 文件夹中,打开 Item 类文件。

  2. 添加 PriorityLevel 枚举以限制可能的值。另外,将 priority 属性添加到 Item 类中,以将默认优先级设置为 3,表示它是低优先级待办事项:

    domain/Item.kt
    // ... imports
    enum class PriorityLevel() {
    Severe, // priority 0
    High, // priority 1
    Medium, // priority 2
    Low // priority 3
    }
    class Item() : RealmObject {
    @PrimaryKey
    var _id: ObjectId = ObjectId.create()
    var isComplete: Boolean = false
    var summary: String = ""
    var owner_id: String = ""
    var priority: Int = PriorityLevel.Low.ordinal
    constructor(ownerId: String = "") : this() {
    owner_id = ownerId
    }
    // ... equals() and hashCode() functions
    }
2
  1. ui/tasks 文件夹中,打开 AddItem.kt 文件。该文件定义用户单击“+”按钮以添加新的待办事项时显示的用户界面的可组合函数。

  2. 首先,在 package com.mongodb.app 下面添加以下导入:

    ui/tasks/AddItem.kt
    import androidx.compose.foundation.layout.fillMaxWidth
    import androidx.compose.foundation.layout.padding
    import androidx.compose.material3.DropdownMenuItem
    import androidx.compose.material3.ExposedDropdownMenuBox
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp
    import com.mongodb.app.domain.PriorityLevel
  3. 现在,我们可以在 AddItemPrompt 可组合函数中添加一个下拉字段,以使用户能够从列表中选择优先级(将 PriorityLevel 枚举作为可用的值):

    ui/tasks/AddItem.kt
    // ... imports
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    fun AddItemPrompt(viewModel: AddItemViewModel) {
    AlertDialog(
    containerColor = Color.White,
    onDismissRequest = {
    viewModel.closeAddTaskDialog()
    },
    title = { Text(stringResource(R.string.add_item)) },
    text = {
    Column {
    Text(stringResource(R.string.enter_item_name))
    TextField(
    colors = ExposedDropdownMenuDefaults.textFieldColors(containerColor = Color.White),
    value = viewModel.taskSummary.value,
    maxLines = 2,
    onValueChange = {
    viewModel.updateTaskSummary(it)
    },
    label = { Text(stringResource(R.string.item_summary)) }
    )
    val priorities = PriorityLevel.values()
    ExposedDropdownMenuBox(
    modifier = Modifier.padding(16.dp),
    expanded = viewModel.expanded.value,
    onExpandedChange = { viewModel.open() },
    ) {
    TextField(
    readOnly = true,
    value = viewModel.taskPriority.value.name,
    onValueChange = {},
    label = { Text(stringResource(R.string.item_priority)) },
    trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = viewModel.expanded.value) },
    colors = ExposedDropdownMenuDefaults.textFieldColors(),
    modifier = Modifier
    .fillMaxWidth()
    .menuAnchor()
    )
    ExposedDropdownMenu(
    expanded = viewModel.expanded.value,
    onDismissRequest = { viewModel.close() }
    ) {
    priorities.forEach {
    DropdownMenuItem(
    text = { Text(it.name) },
    onClick = {
    viewModel.updateTaskPriority(it)
    viewModel.close()
    }
    )
    }
    }
    }
    }
    },
    // ... buttons
    )
    }

    Android Studio 将找到一些错误。我们在接下来的步骤中添加相关的函数以纠正这些错误。

  4. 接下来,我们将下拉字段标签定义为字符串资源。打开 res/values/strings.xml 文件,并在 'resource' 元素结束之前添加以下内容:

    res/values/strings.xml
    <string name="item_priority">Item Priority</string>
  5. 现在,在 presentation/tasks 文件夹中,打开 AddItemViewModel.kt 文件。在此处,我们添加与新的下拉字段相关的业务逻辑。

    package com.mongodb.app 下面添加 PriorityLevel 导入,然后在 AddItemViewModel 类中添加处理下拉字段中的状态变化所需的变量和函数:

    presentation/tasks/AddItemViewModel.kt
    // ... imports
    import com.mongodb.app.domain.PriorityLevel
    // ... events
    class AddItemViewModel(
    private val repository: SyncRepository
    ) : ViewModel() {
    private val _addItemPopupVisible: MutableState<Boolean> = mutableStateOf(false)
    val addItemPopupVisible: State<Boolean>
    get() = _addItemPopupVisible
    private val _taskSummary: MutableState<String> = mutableStateOf("")
    val taskSummary: State<String>
    get() = _taskSummary
    private val _taskPriority: MutableState<PriorityLevel> = mutableStateOf(PriorityLevel.Low)
    val taskPriority: State<PriorityLevel>
    get() = _taskPriority
    private val _expanded: MutableState<Boolean> = mutableStateOf(false)
    val expanded: State<Boolean>
    get() = _expanded
    private val _addItemEvent: MutableSharedFlow<AddItemEvent> = MutableSharedFlow()
    val addItemEvent: Flow<AddItemEvent>
    get() = _addItemEvent
    fun openAddTaskDialog() {
    _addItemPopupVisible.value = true
    }
    fun closeAddTaskDialog() {
    cleanUpAndClose()
    }
    fun updateTaskSummary(taskSummary: String) {
    _taskSummary.value = taskSummary
    }
    fun updateTaskPriority(taskPriority: PriorityLevel) {
    _taskPriority.value = taskPriority
    }
    fun open() {
    _expanded.value = true
    }
    fun close() {
    _expanded.value = false
    }
    // addTask() and cleanUpAndClose() functions
    }

    现在更新 addTask()cleanUpAndClose() 函数以包含新的 taskPriority 参数,使用优先级信息更新消息,并在关闭“添加项目”视图后将优先级字段重置为低优先级:

    fun addTask() {
    CoroutineScope(Dispatchers.IO).launch {
    runCatching {
    repository.addTask(taskSummary.value, taskPriority.value)
    }.onSuccess {
    withContext(Dispatchers.Main) {
    _addItemEvent.emit(AddItemEvent.Info("Task '$taskSummary' with priority '$taskPriority' added successfully."))
    }
    }.onFailure {
    withContext(Dispatchers.Main) {
    _addItemEvent.emit(AddItemEvent.Error("There was an error while adding the task '$taskSummary'", it))
    }
    }
    cleanUpAndClose()
    }
    }
    private fun cleanUpAndClose() {
    _taskSummary.value = ""
    _taskPriority.value = PriorityLevel.Low
    _addItemPopupVisible.value = false
    }
  6. 最后,从 data 文件夹中,打开 SyncRepository.kt 文件以反映 addTask() 函数中的相同更改(该函数将项目写入到 Realm 中)。

    首先,在 package com.mongodb.app 下面添加 PriorityLevel 导入,然后更新 addTask() 函数以将 taskPriority 传递为参数,并将 priority 字段作为整数写入到 Realm 中(使用枚举序数):

    data/SyncRepository.kt
    // ... imports
    import com.mongodb.app.domain.PriorityLevel
    interface SyncRepository {
    // ... Sync functions
    suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel)
    // ... Sync functions
    }
    class RealmSyncRepository(
    onSyncError: (session: SyncSession, error: SyncException) -> Unit
    ) : SyncRepository {
    // ... variables and SyncConfiguration initializer
    // ... Sync functions
    override suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel) {
    val task = Item().apply {
    owner_id = currentUser.id
    summary = taskSummary
    priority = taskPriority.ordinal
    }
    realm.write {
    copyToRealm(task)
    }
    }
    override suspend fun updateSubscriptions(subscriptionType: SubscriptionType) {
    realm.subscriptions.update {
    removeAll()
    val query = when (subscriptionType) {
    SubscriptionType.MINE -> getQuery(realm, SubscriptionType.MINE)
    SubscriptionType.ALL -> getQuery(realm, SubscriptionType.ALL)
    }
    add(query, subscriptionType.name)
    }
    }
    // ... additional Sync functions
    }
    class MockRepository : SyncRepository {
    override fun getTaskList(): Flow<ResultsChange<Item>> = flowOf()
    override suspend fun toggleIsComplete(task: Item) = Unit
    override suspend fun addTask(taskSummary: String, taskPriority: PriorityLevel) = Unit
    override suspend fun updateSubscriptions(subscriptionType: SubscriptionType) = Unit
    override suspend fun deleteTask(task: Item) = Unit
    override fun getActiveSubscriptionType(realm: Realm?): SubscriptionType = SubscriptionType.ALL
    override fun pauseSync() = Unit
    override fun resumeSync() = Unit
    override fun isTaskMine(task: Item): Boolean = task.owner_id == MOCK_OWNER_ID_MINE
    override fun close() = Unit
    // ... companion object
    }
3

此时,您可以重新运行应用程序。使用本教程前面创建的帐户登录。您将看到一个以前创建的项目。添加新的项目,您将看到现在可以设置优先级。为优先级选择 High 并保存该项目。

现在切换回浏览器中的 Atlas 数据页面,并刷新 Item 集合。您现在应该会看到添加了 priority 字段并将其设置为1 的新项目。现有项目没有 priority 字段。

集合中的两个项目

注意

为什么没有中断同步?

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

1

app/java/com.mongodb.app/data 文件夹中,打开 SyncRepository.kt 文件,我们在其中定义了 Flexible Sync 订阅。该订阅定义我们与用户的设备和帐户之间同步的文档。找到 getQuery() 函数。您可以看到我们目前订阅了两个订阅:

  • MINEownerId 属性与经过身份验证的用户匹配的所有文档。

  • ALL:来自所有用户的所有文档。

我们希望更新 MINE 订阅,以 同步标记为高或严重优先级的项目。

您可能还记得,priority 字段的类型为 int,其中最高优先级(“严重”)的值为 0,最低优先级(“低”)的值为 3。我们可以直接将一个整数与 priority 属性进行比较。为此,请编辑 RQL 语句以包含 priority 等于或小于 PriorityLevel.High(或 1)的文档,如下所示:

data/SyncRepository.kt
private fun getQuery(realm: Realm, subscriptionType: SubscriptionType): RealmQuery<Item> =
when (subscriptionType) {
SubscriptionType.MINE -> realm.query("owner_id == $0 AND priority <= ${PriorityLevel.High.ordinal}", currentUser.id)
SubscriptionType.ALL -> realm.query()
}

我们还强制订阅查询在每次打开应用时重新计算要同步的文档。

为此,找到应用程序在启动时调用的 SyncConfiguration.Builder().initialSubscriptions() 函数。首先,添加设置为 truereRunOnOpen 参数,然后将 updateExisting 设置为 true,以允许更新现有的查询。

config = SyncConfiguration.Builder(currentUser, setOf(Item::class))
.initialSubscriptions(rerunOnOpen = true) { realm ->
// Subscribe to the active subscriptionType - first time defaults to MINE
val activeSubscriptionType = getActiveSubscriptionType(realm)
add(getQuery(realm, activeSubscriptionType), activeSubscriptionType.name, updateExisting = true)
}
.errorHandler { session: SyncSession, error: SyncException ->
onSyncError.invoke(session, error)
}
.waitForInitialRemoteData()
.build()
2

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

在 Realm 最初重新同步文档集合后,您将看到创建的新的高优先级项目。

如果要进一步测试功能,可以创建具有各种优先级的项。您会注意到,如果您尝试添加优先级低于 High 的项目,您将收到一条 Toast 消息,指示您没有权限。如果您使用 Logcat 检查您的日志,您将会看到一条信息,显示该项目 "added successfully",然后显示一个同步错误:

ERROR "Client attempted a write that is outside of permissions or query
filters; it has been reverted"

这是因为,在这种情况下,Realm 在本地创建该项目,将其与后端同步,然后撤销写入,因为它不符合订阅规则。

您还会注意到,您最初创建的文档未进行同步,因为其优先级为 null。如果要同步该项目,您可以在 Atlas 用户界面中编辑该文档并为 priority 字段添加一个值。

向现有 Realm 对象添加属性是一项非重大更改,并且开发模式可确保模式更改反映在服务器端。

注意

分享反馈

怎么样?使用页面右下角的 Share Feedback(分享反馈)选项卡,告诉我们本教程是否有帮助或者您是否有任何问题。

← 教程:Atlas Device Sync for Flutter