Join us at MongoDB.local London on 7 May to unlock new possibilities for your data. Use WEB50 to save 50%.
Register now >
Docs Menu
Docs Home
/ /
Atlas App Services

Tutorial: Atlas Device Sync para Kotlin

Tiempo estimado para completar: 30 minutos, según tu experiencia con Kotlin

Realm proporciona un SDK de Kotlin que le permite crear una aplicación móvil Android con Kotlin usando Jetpack ComposeEste tutorial se basa en la aplicación de plantilla de sincronización flexible de Kotlin, llamada kotlin.todo.flex, que ilustra la creación de una aplicación de gestión de listas de tareas pendientes. Esta aplicación permite a los usuarios:

  • Registrar su correo electrónico como una nueva cuenta de usuario.

  • Sign in to their account with their email and password (and sign out later).

  • Ver, crear, modificar y eliminar sus propias tareas.

  • View all tasks, even where the user is not the owner.

La aplicación de plantilla también proporciona un interruptor que simula el dispositivo en modo sin conexión. Este interruptor permite probar rápidamente la sincronización del dispositivo en el simulador, simulando que el usuario no tiene conexión a internet. Sin embargo, es probable que este interruptor se elimine en una aplicación de producción.

Este tutorial añade funcionalidad a la Aplicación Plantilla. Agregarás un nuevo campo Priority al modelo Item existente y actualizarás la suscripción Flexible Sync para mostrar solo los elementos dentro de un rango de prioridades. Este ejemplo ilustra cómo podrías adaptar la aplicación plantilla para tus propias necesidades.

Este tutorial ilustra cómo puedes adaptar la aplicación de plantilla a tus propias necesidades.

En este tutorial aprenderás a:

  • Update a Realm object model with a non-breaking change.

  • Actualizar una suscripción de sincronización de dispositivos.

  • Add a queryable field to the Device Sync configuration on the server to change which data is synchronized.

Nota

Consulte el inicio rápido

If you prefer to get started with your own application rather than follow a guided tutorial, check out the Kotlin Quick Start. It includes copyable code examples and the essential information that you need to set up an Atlas App Services backend.

  • Android Studio Bumblebee 2021.1.1 or higher.

  • JDK 11 or higher.

  • Complemento Kotlin para Android Studio, versión 1.6.10 o superior.

  • An Android Virtual Device (AVD) using a supported CPU architecture.

  • Este tutorial comienza con una aplicación de Plantilla. Necesitas una Cuenta de Atlas, una clave API y App Services CLI para crear una aplicación de plantilla.

    • Para obtener más información sobre cómo crear una cuenta de Atlas, consulte la sección Primeros pasos con Atlas. Para este tutorial, necesita una cuenta de Atlas con un clúster de nivel gratuito.

    • También necesita una clave API de Atlas para la cuenta de MongoDB Cloud con la que desea iniciar sesión. Debe ser propietario del proyecto para crear una aplicación de plantilla mediante la CLI de App Services.

    • Para aprender más sobre cómo instalar App Services CLI, consulta Instalar App Services CLI. Después de instalar, ejecuta el comando login utilizando la clave API de tu proyecto Atlas.

Este tutorial se basa en la aplicación de plantilla de sincronización flexible del SDK de Kotlin, denominada kotlin.todo.flex. Comenzamos con la aplicación predeterminada y creamos nuevas funciones a partir de ella.

Para obtener más información sobre las aplicaciones de plantilla, consulte Aplicaciones de plantilla.

Si aún no tiene una cuenta Atlas, regístrese para implementar una aplicación de plantilla.

Siga el procedimiento descrito en la guía Crear una aplicación y seleccione Create App from TemplateSeleccione la plantilla Real-time Sync. Esto crea una aplicación de Servicios de Aplicaciones preconfigurada para usarla con uno de los clientes de la plantilla de Sincronización de Dispositivos.

Después de crear una aplicación plantilla, la Interfaz de Usuario muestra un modal etiquetado como Get the Front-end Code for your Template. Este modal proporciona instrucciones para descargar el código cliente de la aplicación plantilla como un archivo .zip o para usar App Services CLI y obtener el cliente.

Después de seleccionar el método .zip o la CLI de App Services, siga las instrucciones en pantalla para obtener el código de cliente. Para este tutorial, seleccione el código de cliente Kotlin (Android).

Nota

La utilidad ZIP predeterminada de Windows podría mostrar el archivo .zip vacío. Si esto ocurre, use un programa de compresión de terceros disponible.

El comando appservices apps create configura el backend y crea una aplicación plantilla de Kotlin para usar como base en este tutorial.

Ejecute el siguiente comando en una ventana de terminal para crear una aplicación llamada "MyTutorialApp" que se implementa en la región US-VA con su entorno configurado en "desarrollo" (en lugar de producción o control de calidad).

appservices app create \
--name MyTutorialApp \
--template kotlin.todo.flex \
--deployment-model global \
--environment development

El comando crea un nuevo directorio en su ruta actual con el mismo nombre que el valor del indicador --name.

You can fork and clone a GitHub repository that contains the Device Sync client code. The Kotlin client code is available at https://github.com/mongodb/template-app-kotlin-todo.

Si usas este proceso para obtener el código del cliente, debes crear una aplicación de plantilla para usar con el cliente. Sigue las instrucciones en Crear una aplicación de plantilla para usar la Interfaz de Usuario de Atlas App Services, los App Services CLI o la API de Administración para crear una aplicación de plantilla Device Sync.

1

In Android Studio, open the kotlin.todo.flex folder.

Si descargaste el cliente como archivo .zip o clonaste su repositorio de GitHub, debes insertar manualmente el ID de la aplicación de App Services en el lugar correspondiente del cliente. Sigue las instrucciones Configuration del cliente README.md para saber dónde insertar el ID de la aplicación.

2

Dedica unos minutos a explorar la organización del proyecto mientras Android Studio lo indexa. En el directorio app/java/com.mongodb.app, puedes ver algunos archivos importantes:

Archivo
Propósito

ComposeItemActivity.kt

Clase Activity que define el diseño y proporciona funcionalidad para abrir un realm, escribir elementos en el realm, cerrar sesión de un usuario y cerrar un realm.

ComposeLoginActivity.kt

Activity class that defines the layout and provides functionality for registering a user and logging a user in.

TemplateApp.kt

Clase que inicializa la aplicación App Services.

In this tutorial, you'll be working in the following files:

Archivo
Propósito

Item.kt

Situado en el directorio domain. Define el objeto Realm que almacenamos en la base de datos.

Agregar artículo.kt

Ubicado en el directorio ui/tasks. Contiene la función componible que define el diseño utilizado al agregar un elemento.

AddItemViewModel.kt

Situado en el directorio presentation/tasks. El modelo de vista que contiene la lógica del negocio y gestiona el estado cuando se agrega un ítem.

SyncRepository.kt

Located in the data directory. Repository used to access Realm Sync and defines the Flexible Sync subscription.

Cadenas.xml

Located in the res/values directory. Defines the text string resources used in the app.

3

Sin realizar ningún cambio en el código, debería poder ejecutar la aplicación en un emulador de Android usando Android Studio o en un dispositivo físico.

Ejecute la aplicación, registre una nueva cuenta de usuario y luego agregue un nuevo elemento a Su lista de tareas.

4

Inicia sesión en Atlas App Services. En la pestaña Data Services, haz clic en Browse Collections. En la lista de bases de datos, busca y expande la base de datos todo y luego la colección Item. Debería ver el documento que creó en esta colección.

1

Now that you have confirmed everything is working as expected, we can add changes. In this tutorial, we have decided that we want to add a "priority" property to each Item so that we can filter Items by their priority level. The priority property will be mapped to a PriorityLevel enum to constrain the possible values, and we will use the ordinal of each enum to correspond to the priority integer so we can query based on a numeric priority level later.

Para ello siga estos pasos:

  1. Dentro de la carpeta app/java/com.mongodb.app/domain, abra el archivo de clase Item.

  2. Agregue una enumeración PriorityLevel para restringir los valores posibles. También agregue una propiedad priority a la clase Item, que establece la prioridad predeterminada en 3, lo que indica que es una tarea pendiente de baja prioridad:

    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. Desde la carpeta ui/tasks, abra el archivo AddItem.kt. Este archivo define las funciones componibles para la interfaz de usuario que se muestra cuando un usuario hace clic en el botón "+" para añadir una nueva tarea.

  2. Primero, agregue las siguientes importaciones debajo de 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. Ahora podemos añadir un campo desplegable a la función componible AddItemPrompt que permitirá al usuario elegir un nivel de prioridad de una lista utilizando los enums PriorityLevel como valores disponibles:

    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 identificará varios errores. Los corregiremos en los próximos pasos añadiendo las funciones correspondientes.

  4. A continuación, definiremos la etiqueta del campo desplegable como un recurso de cadena. Abra el archivo res/values/strings.xml y agregue lo siguiente antes de cerrar el elemento "resource":

    res/values/strings.xml
    <string name="item_priority">Item Priority</string>
  5. Ahora, dentro de la carpeta presentation/tasks, abre el archivo AddItemViewModel.kt. Aquí agregaremos la lógica de negocio relacionada con nuestro nuevo campo de menú desplegable.

    Agregue la importación PriorityLevel debajo de package com.mongodb.app, luego agregue las variables y funciones a la clase AddItemViewModel necesarias para manejar los cambios de estado dentro del menú desplegable:

    presentation/tareas/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
    }

    Ahora actualice las funciones addTask() y cleanUpAndClose() para incluir el nuevo parámetro taskPriority, actualice el mensaje con la información de prioridad y restablezca el campo de prioridad a bajo una vez que se cierre la vista Agregar elemento:

    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. Finalmente, desde la carpeta data, abra el archivo SyncRepository.kt para reflejar los mismos cambios en la función addTask(), que escribe el elemento en el reino.

    Primero, agregue la importación PriorityLevel debajo de package com.mongodb.app, luego actualice las funciones addTask() para pasar taskPriority como parámetro y escriba el campo priority en el reino como un entero (usando el ordinal de enumeración):

    datos/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

At this point, you can rerun the application. Log in using the account you created earlier in this tutorial. You will see the one Item you previously created. Add a new Item, and you will see that you can now set the priority. Choose High for the priority and save the Item.

Ahora, regrese a la página de datos del Atlas en su navegador y actualice la Item colección. Debería ver el nuevo elemento con el priority campo agregado y configurado en 1. El elemento existente no tiene el priority campo.

Dos artículos en una colección
haga clic para ampliar

Nota

¿Por qué no se rompió esta sincronizar?

Adding a property to a Realm object is not a breaking change and therefore does not require a client reset. The template app has Development Mode enabled, so changes to the client Realm object are reflected in the server-side schema. For more information, see Development Mode and Update Your Data Model.

1

Dentro de la carpeta app/java/com.mongodb.app/data, abre el archivo SyncRepository.kt, donde definimos la suscripción Flexible Sync. La suscripción define qué documentos sincronizamos con el dispositivo y la cuenta del usuario. Busca la función getQuery(). Puedes ver que actualmente estamos suscritos a dos suscripciones:

  • MINE:Todos los documentos donde la propiedad ownerId coincide con el usuario autenticado.

  • ALL: Todos los documentos de todos los usuarios.

Queremos actualizar la MINE suscripción para sincronizar únicamente los elementos que estén marcados como prioridad alta o grave.

Como recordarás, el campo priority es de tipo int, donde la prioridad más alta ("Grave") tiene un valor de 0 y la prioridad más baja ("Baja") tiene un valor de 3. Podemos hacer comparaciones directas entre un entero y la propiedad de prioridad. Para ello, edita la instrucción RQL para incluir documentos donde la prioridad sea igual o menor que PriorityLevel.High (o 1), como se muestra aquí:

datos/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()
}

We'll also force the subscription query to recalculate which documents to sync every time we open the app.

Para ello, busque la función SyncConfiguration.Builder().initialSubscriptions() que nuestra aplicación llama al inicio. Primero, añada el parámetro reRunOnOpen a true y, a continuación, configure updateExisting a true, lo que permite actualizar la consulta existente.

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

Vuelve a ejecutar la aplicación. Inicia sesión usando la cuenta que creaste anteriormente en este tutorial.

Después de un momento inicial cuando Realm vuelve a sincronizar la colección de documentos, verá el nuevo elemento de alta prioridad que creó.

Tip

Cambiar suscripciones con el modo de desarrollador habilitado

In this tutorial, when you change the subscription and query on the priority field for the first time, the field is automatically added to the Device Sync Collection Queryable Fields. This occurs because the template app has Development Mode enabled by default. If Development Mode was not enabled, you would have to manually add the field as a queryable field to use it in a client-side Sync query.

Para obtener más información, consulte Campos consultables.

Si desea probar la funcionalidad con más detalle, puede crear elementos con diferentes prioridades. Observará que si intenta agregar un elemento con una prioridad inferior a Alta, recibirá un mensaje de notificación indicando que no tiene permiso. Si revisa sus registros con Logcat, verá un mensaje indicando que el elemento se agregó correctamente, seguido de un error de sincronización:

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

Esto se debe a que, en este escenario, Realm crea el elemento localmente, lo sincroniza con el backend y luego revierte la escritura porque no cumple con las reglas de suscripción.

También, notarás que el documento que creaste inicialmente no está sincronizado porque tiene una prioridad de null. Si deseas que este elemento se sincronice, puedes editar el documento en la interfaz de usuario de Atlas y agregar un valor para el campo de prioridad.

Agregar una propiedad a un objeto Realm existente es un cambio no disruptivo, y el modo de desarrollo asegura que el cambio de esquema se refleje en el servidor.

Nota

Share Feedback

¿Cómo te fue? Usa el Rate this page widget en la parte inferior derecha de la página para evaluar su efectividad. O reporta un problema en el repositorio de GitHub si tuviste algún problema.

Next

¿Qué son los servicios de aplicación Atlas?