Menu Docs
Página inicial do Docs
/ /
Serviços Atlas App
/

Tutorial: Atlas Device Sync para Kotlin

Nesta página

  • Objetivos de aprendizagem
  • Pré-requisitos
  • Comece com o Modelo
  • Configurar o Aplicativo Modelo
  • Abra o aplicativo
  • Explore a estrutura do aplicativo
  • Execute o aplicativo
  • Verificar o Backend
  • Modificar a Aplicação
  • Adicionar uma nova propriedade
  • Adicionar uma nova propriedade ao modelo
  • Definir a prioridade ao criar um novo item
  • Executar e testar
  • Alterar a assinatura
  • Atualizar a assinatura
  • Executar e testar
  • Conclusão
  • O que vem a seguir?

Tempo estimado para conclusão: 30 minutos, dependendo da sua experiência com o Kotlin

Realm fornece um SDK Kotlin que permite criar um aplicativo móvel para Android com o Kotlin usando o Jetpack Compose. Este tutorial é baseado no modelo de aplicativo do Kotlin Flexible Sync, denominado kotlin.todo.flex, que ilustra a criação de um aplicativo de gerenciamento de lista de tarefas. Este aplicativo permite que o usuário:

  • Registre seu e-mail como uma nova conta de usuário.

  • Faça login na conta deles com o e-mail e a senha (e saia mais tarde).

  • Visualize, crie, modifique e exclua suas próprias tarefas.

  • Visualizar todas as tarefas, mesmo onde o usuário não é o proprietário.

O aplicativo de modelo também oferece uma alternância que simula o dispositivo no "Modo Offline". Essa alternância permite que você teste rapidamente a funcionalidade Device Sync no simulador, emulando o usuário que não tem conexão com a Internet. No entanto, você provavelmente removeria essa opção em um aplicativo de produção.

Este tutorial adiciona funcionalidades ao aplicativo modelo. Você adicionará um novo campo Priority ao modelo de Item existente e atualizará a inscrição do Flexible Sync para mostrar apenas itens dentro de uma faixa de priorities. Este exemplo ilustra como você pode adaptar o aplicativo modelo às suas próprias necessidades.

Este tutorial ilustra como você pode adaptar o aplicativo modelo para suas próprias necessidades.

Neste tutorial, você aprenderá como:

  • Atualize um modelo de objeto Realm com uma alteração não inovadora.

  • Atualizar uma inscrição do Device Sync.

  • Adicione um campo de query à configuração do Device Sync no servidor para alterar quais dados são sincronizados.

Observação

Confira o início rápido

Se você preferir começar com seu próprio aplicativo em vez de seguir um tutorial guiado, confira o Início rápido do Kotlin. Ele inclui exemplos de código copiáveis e as informações essenciais necessárias para configurar um backend do Atlas App Services.

  • Android Studio Bumblebee 2021.1.1 ou superior.

  • JDK 11 ou versão mais recente.

  • Plugin do Kotlin para Android Studio, versão 1.6.10 ou mais recente.

  • Um dispositivo virtual Android (AVD) usando uma arquitetura de CPU compatível.

  • Este tutorial começa com um aplicativo de modelo. Você precisa de uma Conta Atlas, uma chave de API e appservices para criar um Aplicativo Modelo.

    • Você pode aprender mais sobre a criação de uma conta do Atlas na documentação de Comece a usar o Atlas. Para este tutorial, você precisa de uma conta do Atlas com um cluster de camada livre.

    • Você também precisa de uma chave de API Atlas para a conta do MongoDB Cloud com a qual deseja fazer login. Você deve ser proprietário do projeto para criar um aplicativo modelo usando appservices.

    • Para saber mais sobre como instalar serviços de aplicativos, consulte Instalar CLI do App Services. Após instalar, execute o comando login utilizando a chave API para seu projeto Atlas.

Este tutorial baseia-se no aplicativo modelo do Kotlin SDK Flexible Sync denominado kotlin.todo.flex. Começamos com o aplicativo padrão e construímos novos recursos nele.

Para saber mais sobre os aplicativos de modelo, consulte Aplicativos de modelo.

Se você ainda não tiver uma conta do Atlas, cadastre-se para implantar um Aplicativo Modelo.

1

No Android Studio, abra a pasta kotlin.todo.flex localizada na pasta frontend do aplicativo modelo.

Se você baixou o cliente como um arquivo .zip ou clonou o repositório GitHub do cliente, você deverá inserir manualmente o ID do aplicativo App Services no local apropriado em seu cliente. Siga as instruções Configuration no cliente README.md para saber onde inserir seu ID do aplicativo.

2

Reserve alguns minutos para explorar a organização do projeto enquanto o Android Studio indexa seu projeto. No diretório app/java/com.mongodb.app, você pode ver alguns arquivos dignos de nota:

arquivo
Propósito
ComposeItemActivity.kt
Classe de atividade que define o layout e fornece funcionalidade para abrir um domínio, gravar itens no domínio, desconectar um usuário e fechar um domínio.
ComposeLoginActivity.kt
Classe de atividade que define o layout e oferece funcionalidade para registrar e conectar um usuário.
TemplateApp.kt
Classe que inicializa o App Services App.

Neste tutorial, você estará trabalhando nos seguintes arquivos:

arquivo
Propósito
Item.kt
Localizado no diretório domain. Define o objeto de Realm que armazenamos no banco de dados.
AddItem.kt
Localizado no diretório ui/tasks. Contém a função composta que define o layout utilizado ao adicionar um item.
AddItemViewModel.kt
Localizado no diretório presentation/tasks. O modelo de exibição que contém lógica de negócios e gerencia o estado ao adicionar um item.
SyncRepository.kt
Localizado no diretório data. Repositório usado para acessar o Realm Sync e definir a inscrição Flexible Sync.
Strings.xml
Localizado no diretório res/values. Define os recursos de string de texto usados no aplicativo.
3

Sem fazer nenhuma alteração no código, você deve conseguir executar o aplicativo em um emulador Android usando o Android Studio ou em um dispositivo físico.

Execute a aplicação, registre uma nova conta de usuário e, em seguida, adicione um novo item à sua lista de tarefas.

4

Faça login no Atlas App Services. Na aba Data Services , clique em Browse Collections. Na lista de bancos de dados, localize e expanda o banco de dados do todo e então a coleção do Item . Você deve ver o documento que criou nesta coleção.

1

Agora que você confirmou que tudo está funcionando conforme o esperado, podemos adicionar as alterações. Neste tutorial, decidimos que queremos adicionar uma propriedade "priority" a cada item para que possamos filtrar os itens por seu nível de prioridade. A propriedade priority será mapeada a um enum PriorityLevel para restringir os valores possíveis, e usaremos o ordinal de cada enum para corresponder ao número inteiro priority para que possamos consultar com base em um nível de priority numérico posteriormente.

Para fazer isso, siga estas etapas:

  1. Dentro da pasta app/java/com.mongodb.app/domain, abra o arquivo de classe Item.

  2. Adicione um enum PriorityLevel para restringir os valores possíveis. Adicione também uma propriedade priority à classe Item, que define a priority padrão como 3, indicando que é um item de tarefas de baixa priority:

    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. Na pasta ui/tasks, abra o arquivo AddItem.kt. Esse arquivo define as funções que podem ser compostas para a interface do usuário que são exibidas quando um usuário clica no botão '+' para adicionar um novo item de tarefa.

  2. Primeiro, adicione as seguintes importações abaixo do 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. Agora podemos adicionar um campo suspenso à função composta do AddItemPrompt que permitirá ao usuário escolher um nível de prioridade em uma lista usando os enums PriorityLevel como valores disponíveis:

    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
    )
    }

    O Android Studio identificará vários erros. Corrigiremos isso nas próximas etapas adicionando as funções relacionadas.

  4. Em seguida, definiremos a etiqueta do campo suspenso como um recurso de string. Abra o arquivo res/values/strings.xml e adicione o seguinte antes de fechar o elemento "recurso":

    res/values/strings.xml
    <string name="item_priority">Item Priority</string>
  5. Agora, na pasta presentation/tasks, abra o arquivo AddItemViewModel.kt . Aqui adicionaremos a lógica de negócios relacionada ao nosso novo campo suspenso.

    Adicione a importação PriorityLevel abaixo do package com.mongodb.app e, em seguida, adicione as variáveis e funções à classe AddItemViewModel necessária para lidar com as alterações de estado no menu suspenso:

    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
    }

    Agora, atualize as funções addTask() e cleanUpAndClose() para incluir o novo parâmetro taskPriority, atualize a mensagem com as informações de prioridade e redefina o campo de priority para baixo assim que a exibição Adicionar item for fechado:

    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, na pasta data, abra o arquivo SyncRepository.kt para refletir as mesmas alterações na função addTask(), que grava o item no realm.

    Primeiro, adicione a importação PriorityLevel abaixo do package com.mongodb.app e, em seguida, atualize as funções addTask() para passar o taskPriority como parâmetro e gravar o campo priority no domínio como um número inteiro (usando o enum ordinal):

    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

Agora você pode executar o aplicativo novamente. Faça login usando a conta que você criou neste tutorial. Você verá o único item que criou anteriormente. Adicione um novo item e você verá que agora pode definir a priority. Escolha High para a priority e salve o item.

Agora volte para a página de dados do Atlas em seu navegador e atualize a coleção do Item. Você deve agora visualizar o novo Item com o campo priority adicionado e configurado para 1. O item existente não tem um campo priority.

Dois itens em uma coleção

Observação

Por que isso não interrompeu a sincronização?

A adição de uma propriedade a um objeto Realm não é uma alteração de ruptura e, portanto, não exige uma reinicialização do cliente. O aplicativo de modelo tem o Modo de Desenvolvimento habilitado, portanto, as alterações no objeto Realm do cliente são refletidas no esquema do lado do servidor. Para obter mais informações, consulte Modo de desenvolvimento e Atualizar seu modelo de dados.

1

Na pasta app/java/com.mongodb.app/data, abra o arquivo SyncRepository.kt, onde definimos a inscrição Flexible Sync. A inscrição define quais documentos sincronizamos com o dispositivo e a conta do usuário. Encontre a função getQuery(). Você pode ver que atualmente estamos assinando duas inscrições:

  • MINE: Todos os documentos onde a propriedade ownerId corresponde ao usuário autenticado.

  • ALL: Todos os documentos de todos os usuários.

Queremos atualizar a inscrição MINE para somente itens de sincronização marcados como prioridade Alta ou Grave.

Como você deve se lembrar, o campo priority é do tipo int, em que a prioridade mais alta ("Severe") tem o valor 0 e a prioridade mais baixa ("Low") tem o valor 3. Podemos fazer comparações diretas entre um número inteiro e a propriedade de prioridade. Para isso, edite a instrução RQL para incluir documentos em que a prioridade seja igual ou inferior a PriorityLevel.High (ou 1), conforme mostrado aqui:

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

Também forçaremos a query de inscrição a recalcular quais documentos sincronizar toda vez que abrirmos o aplicativo.

Para tal, localize a função SyncConfiguration.Builder().initialSubscriptions() que nosso aplicativo chama no início. Primeiro, adicione o parâmetro reRunOnOpen definido para true e, em seguida, defina updateExisting para true, o que permite que a query existente seja atualizada.

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

Execute o aplicativo novamente. Faça login usando a conta que você criou neste tutorial.

Após um momento inicial em que o Realm ressincroniza a collection de documentos, você verá o novo item de alta priority que você criou.

Se quiser testar ainda mais a funcionalidade, crie itens de várias prioridades. Você verá que, se tentar adicionar um item com prioridade menor que Alta, receberá uma mensagem do sistema indicando que não tem permissão. E, se você verificar seus logs usando o Logcat, verá uma mensagem indicando que o item foi "adicionado com sucesso", seguida por um erro de sincronização:

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

Isso porque, nesse cenário, o Realm cria o item localmente, sincroniza-o com o backend e, em seguida, reverte a escrita porque ele não atende às regras de assinatura.

Você também notará que o documento que você criou inicialmente não está sincronizado, pois tem uma priority de null. Se você quiser que este Item seja sincronizado, você pode editar o documento na interface do usuário do Atlas e adicionar um valor para o campo prioritário.

Adicionar uma propriedade a um objeto de Realm existente não é uma alteração significativa, e o modo de desenvolvimento garante que a alteração de esquema se reflita no lado do servidor.

Observação

Compartilhar feedback

Como foi? Use a aba Compartilhar feedback no canto inferior direito da página para nos informar se esse tutorial foi útil ou se você teve algum problema.

← Tutorial: Atlas Device Sync para Flutter