教程:适用于 Kotlin 的 Atlas Device Sync
在此页面上
预计完成时间: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)。
从模板开始
本教程基于名为 kotlin.todo.flex
的 Kotlin SDK Flexible Sync 模板应用。我们从默认应用入手,并在该应用上构建新的功能。
要了解有关模板应用的更多信息,请参阅模板应用。
如果您还没有 Atlas 帐号,请注册以部署“模板应用程序”。
按照“创建应用程序”指南中描述的步骤操作,然后选择 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 方法后,请按照屏幕上的说明获取客户端代码。对于本教程,请选择 Kotlin (Android) 客户端代码。
注意
默认的 Windows ZIP 实用程序可能会将 .zip 文件显示为空。如果遇到这种情况,请使用可用的第三方 zip 程序。
appservices apps create 命令设置后端,并创建一个 Kotlin 模板应用,供您用作本教程的基础。
在终端窗口中运行以下命令,创建一个名为“MyTutorialApp”的应用,将该应用部署在 US-VA
地区,并将其环境设置为“开发”(而不是生产或 QA)。
appservices app create \ --name MyTutorialApp \ --template kotlin.todo.flex \ --deployment-model global \ --environment development
该命令在当前路径中创建一个新目录,其名称与 --name
标志的值相同。
您可以创建分支并克隆包含 Device Sync 客户端代码的 GitHub 存储库。 Kotlin 客户端代码位于 https://github.com/mongodb/template-app-kotlin-todo。
如果您使用此过程来获取客户端代码,则必须创建一个模板应用以配合客户端使用。按照创建模板应用中的说明操作,使用 Atlas App Services 用户界面、App Services CLI 或 Admin API 创建 Device Sync 模板应用。
设置模板应用程序
探索应用结构
在 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 目录中。定义应用中使用的文本字符串资源。 |
运行应用
无需对代码进行任何更改,您就能够在使用 Android Studio 的 Android 模拟器或物理设备上运行应用。
运行应用,注册一个新用户帐户,然后将新事项添加到待办事项清单中。
检查后端
登录 Atlas App Services。在 Data Services 标签页中,单击 Browse Collections。在数据库列表中,找到并展开 todo 数据库,然后找到并展开 Item 集合。您应该会看到在此集合中创建的文档。
修改应用程序
添加新属性
为模型添加新属性
您现已确认一切正常工作,我们可以添加更改了。在本教程中,我们决定为每个项目添加一个“priority”属性,以便我们可以按项目的优先级进行筛选。priority 属性将映射到 PriorityLevel
枚举以限制可能的值,我们将使用每个枚举的序数以对应于 priority 整数,以便我们稍后根据数字优先级进行查询。
为此,请按照以下步骤操作:
在
app/java/com.mongodb.app/domain
文件夹中,打开Item
类文件。添加
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 { 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 }
创建新事项时设置优先级
从
ui/tasks
文件夹中,打开AddItem.kt
文件。该文件定义用户单击“+”按钮以添加新的待办事项时显示的用户界面的可组合函数。首先,在
package com.mongodb.app
下面添加以下导入:ui/tasks/AddItem.ktimport 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 现在,我们可以在
AddItemPrompt
可组合函数中添加一个下拉字段,以使用户能够从列表中选择优先级(将 PriorityLevel 枚举作为可用的值):ui/tasks/AddItem.kt// ... imports 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 将找到一些错误。我们在接下来的步骤中添加相关的函数以纠正这些错误。
接下来,我们将下拉字段标签定义为字符串资源。打开
res/values/strings.xml
文件,并在 'resource' 元素结束之前添加以下内容:res/values/strings.xml<string name="item_priority">Item Priority</string> 现在,在
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 } 最后,从
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 }
更改订阅
更新订阅
在 app/java/com.mongodb.app/data
文件夹中,打开 SyncRepository.kt
文件,我们在其中定义了 Flexible Sync 订阅。该订阅定义我们与用户的设备和帐户之间同步的文档。找到 getQuery()
函数。您可以看到我们目前订阅了两个订阅:
MINE
:ownerId
属性与经过身份验证的用户匹配的所有文档。ALL
:来自所有用户的所有文档。
我们希望更新 MINE
订阅,以仅 同步标记为高或严重优先级的项目。
您可能还记得,priority
字段的类型为 int
,其中最高优先级(“严重”)的值为 0,最低优先级(“低”)的值为 3。我们可以直接将一个整数与 priority 属性进行比较。为此,请编辑 RQL 语句以包含 priority 等于或小于 PriorityLevel.High(或 1)的文档,如下所示:
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()
函数。首先,添加设置为 true
的 reRunOnOpen
参数,然后将 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()
运行和测试
再次运行应用程序。使用本教程前面创建的帐户登录。
在 Realm 最初重新同步文档集合后,您将看到创建的新的高优先级项目。
提示
在启用开发者模式的情况下更改订阅
在本教程中,当您首次更改优先级字段的订阅和查询时,该字段将自动添加到 Device Sync Collection Queryable Fields 中。出现这种情况是因为模板应用默认启用了开发模式。如果未启用开发模式,您必须手动将该字段添加为可查询字段,以便在客户端同步查询中使用它。
有关更多信息,请参阅可查询字段。
如果要进一步测试功能,可以创建具有各种优先级的项。您会注意到,如果您尝试添加优先级低于 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 对象添加属性是一项非重大更改,并且开发模式可确保模式更改反映在服务器端。
接下来的步骤
阅读我们的 Kotlin SDK 文档。
在 MongoDB 开发者中心查找面向开发者的博客文章和集成教程。
加入 MongoDB Community 论坛,向其他 MongoDB 开发者和技术专家学习。
探索工程和专家团队提供的示例项目。