Alura > Cursos de Mobile > Cursos de Android > Conteúdos de Android > Primeiras aulas do curso Android com Kotlin: Migrations e relacionamento com o Room

Android com Kotlin: Migrations e relacionamento com o Room

Estrutura do banco de dados - Apresentação

Oi, pessoal! Eu sou Alex Felipe, instrutor da Alura, e vou apresentar para você o curso de Android com Kotlin: Migrations e relacionamento com o Room.

Logo no começo, percebemos que esse projeto vem com um visual um pouco diferente do anterior, com a adição de duas telas novas: a tela inicial, que chamamos de "Tela de Autenticação" ou de "Login", e uma nova tela que chamamos de "Cadastro de Usuário", que pode ser acessada clicando no botão "CADASTRAR USUÁRIO", no centro inferior.

Com essas duas novas telas, vamos começar o nosso projeto a partir de uma nova situação, que é, de fato, adicionar novas entidades. No nosso caso, elas representam o usuário. Entenderemos como fazer isso.

Um ponto muito importante é que, mesmo que você tenha feito o curso anterior e tenha o projeto anterior, essas telas vieram a partir desse projeto, e eu já vou oferecer como projeto inicial.

Então, novamente, mesmo que você já tenha o projeto anterior, é necessário que você baixe esse projeto com as novas adições: as novas telas de autenticação de cadastro e a implementação do Room, utilizando coroutines, ou seja, operações assíncronas. Eu também oferecerei suporte e os links necessários para você conseguir baixar e abrir esse projeto.

Uma vez que temos essas novas telas, também temos novas funcionalidades. Podemos até fazer uma simulação em uma delas, porque eu limpei as informações do aplicativo. Cadastraremos um usuário, então podemos colocar o ID que quisermos. Pode ser "alex" ou qualquer nome. No nome, nesse caso, você coloca Alex, e vou colocar a senha "1234".

Quando clicamos em "CADASTRAR", no final do formulário, percebemos que volta para a tela de autenticação. Sim, conseguimos fazer o cadastro. Agora para acessarmos o aplicativo, precisamos realizar a autenticação, preenchendo os campos com os dados cadastrados e clicando no botão "ENTRAR". Aprenderemos como fazer essas funções no nosso aplicativo.

Além de entrar no nosso aplicativo, aprenderemos como é possível fazer o "Deslogar", ou seja, sair do aplicativo, clicando no menu de opções, no canto superior direito. Perceberemos que, de fato, deslogamos. Poderemos entrar novamente com o mesmo usuário ou outro usuário cadastrado.

As demais funcionalidades vão ser mantidas, como a funcionalidade de cadastrar um produto, clicando no botão "+", no canto inferior direito. Cadastraremos o "produto do Alex", preenchendo o campo nome. Colocaremos até uma descrição de teste: "desc de teste", e no campo valor "1234".

Clicando em "Salvar", no final do formulário, mantemos a informação do produto. Contudo, nesse projeto, é um produto do Usuário. Então se quisermos acessar o "produto do alex", só conseguiremos se acessarmos o aplicativo como "alex". Faremos uma simulação.

Cadastraremos um novo usuário. Nesse caso, o usuário será "fran", o nome será "Fran" e a senha "1234". Na tela de autenticação, faremos login com a "fran" e perceberemos que o produto que cadastramos com o "alex" não está disponível para ela. Isso porque também colocaremos essa funcionalidade.

Percebam que nosso projeto, no momento, tem características que foram implementadas no conteúdo anterior, mas as funcionalidades serão desenvolvidas na parte mais técnica, que aprenderemos nesse projeto.

Em questões técnicas, o que veremos será sobre Migrations no Room. Entenderemos as situações em que precisamos utilizar migration, quando elas aparecem e como implementamos migration. Entenderemos também como conseguimos fazer essa implementação de fluxo de usuário, porque precisamos salvar essa informação: seja para cadastrar um produto, realizar apenas a listagem dos produtos do usuário e assim por diante.

Para isso, utilizaremos preferences, uma estrutura onde conseguimos armazenar informações através de "chave-valor". No caso, tipos primitivos, e utilizaremos o data store para isso. Então será outra técnica.

Aprenderemos como conseguimos reutilizar essa lógica que envolve o usuário, como acessar o usuário em diferentes telas e, por fim, o que mais trabalharemos é a questão do relacionamento, acessando o usuário na tela de cadastro de produtos e vinculando o usuário a um produto.

Da mesma forma, quando acessamos a nossa tela de lista de produtos, aprenderemos como vincular uma busca a partir de um usuário, mostrando apenas as informações daquele usuário. Podemos até realizar uma simulação. Na tela "Cadastrar produtos", criamos o "produto da fran". Na descrição, colocamos "desc de teste" e valor "1234". Perceberemos que essa busca é feita apenas pela Fran.

Se deslogarmos e realizarmos o login com "alex", notaremos que não temos acesso ao "produto da fran", porque a busca é feita a partir desse filtro de usuário, que está associado à questão de relacionamento no Room.

Então, de conteúdo do curso, é isso que eu queria passar para vocês. Espero que estejam animados e animadas para começarem o curso. Aguardo vocês na primeira aula.

Até já!

Estrutura do banco de dados - Apresentando o projeto

Antes de mexermos no nosso código, é muito importante conhecermos o projeto que trabalharemos durante nosso curso. Ele será oferecido para você baixar, mas é importante que saiba que esse projeto é uma continuação do que viemos desenvolvendo em outros conteúdos da Alura.

Sendo mais específico, esse projeto, conhecido como "Orgs" tem base no projeto "android com kotlin: persistência de dados com room", disponível no GitHub da Alura. Nele fizemos uma introdução do que é Room e uma integração com o nosso aplicativo Android.

Quais as novidades que utilizaremos neste curso? No projeto anterior, colocamos funcionalidades para inserir, alterar e remover produtos, assim como a sua listagem. Então, anteriormente, conseguimos mostrar o resultado do que fazemos com banco de dados em uma lista de produtos. Quando inserimos, esse produto é exibido, quando alteramos, as alterações também são mostradas e quando removemos, o produto é retirado da lista. Também desenvolvemos outras atividades, como os desafios.

A partir deste novo curso, continuaremos o que construímos no outro projeto, mas irei oferecer algumas funcionalidades já adicionadas. Porque o meu objetivo neste curso será continuar com assuntos mais específicos em relação ao Room, tais como, migrations e relacionamentos.

Então o que vou adicionar de imediato e oferecer para vocês, sem que precisemos focar na implementação, são funcionalidades que não estão relacionadas diretamente com o Room. É o caso das novas telas, que são as de "Autenticação" e de "Cadastro de Usuário", e algumas implementações que envolvem a comunicação com o banco de dados, utilizando operações assíncronas com coroutines.

Agora aproveitarei para mostrar essas novidades em relação ao projeto anterior, oferecendo uma ideia do que teremos de novo. Caso você tenha feito o curso anterior, irá baixar esse novo projeto para entender o que há de novo e o que é necessário compreender.

Vamos começar e, aos poucos, conheceremos essas novidades. Novamente, se quiser explorar o projeto anterior, você pode acessar o repositório da Alura.

Só não recomendo que você acesse buscando por "android-com-kotlin-migrations-e-relacionamento-com-room", porque quando você acessar esse conteúdo, é possível que o nome esteja diferente, tanto por uma questão de estratégia de nome ou um nome mais simplificado. Caso queira acessar esse conteúdo e baixar o projeto, confira nas atividades deste curso, porque certamente haverá o link correto.

Então eu vou acessar nosso Android Studio e começaremos a explorar nosso projeto, entendendo as novidades em relação à versão anterior. A primeira coisa que podemos observar é a Tela de Autenticação. Ela não existia no projeto anterior.

A partir desse projeto, a tela de autenticação aparece assim que executamos o nosso aplicativo. Vou executar novamente nosso código e poderemos observar que ela já aparece como tela inicial. Neste estado inicial do projeto, a tela de autenticação é nosso novo launcher.

Agora o que precisamos compreender é que, se colocarmos, como um usuário, uma informação qualquer nessa tela, que seria o ID e a senha do usuário, e clicarmos em "ENTRAR", seremos conduzidos diretamente para tela de lista de produtos. A partir desta ação, teremos as mesmas funcionalidades do projeto anterior.

Se apertarmos o botão "Voltar", no canto inferior esquerdo, seremos direcionados novamente para tela de login. Também temos mais uma ação, que é o botão "CADASTRAR USUÁRIO", que está com o fundo invisível. Ao clicarmos nele, teremos acesso a uma nova tela que permitirá o cadastro de usuários.

Percebam que essas duas novas funcionalidades foram implementadas para explorarmos a adição de novas entidades no nosso projeto. Não era o objetivo que vocês construíssem essas telas, porque nelas utilizamos os mesmos conceitos que já aprendemos em outros conteúdos da Alura. Sendo assim, não há muitos motivos para vocês focarem na implementação dessa tela, por isso já deixei ela disponibilizada para vocês.

Nosso foco será na parte mais lógica, porque, até então, não conseguíamos cadastrar usuários. O que conseguimos fazer é colocar as informações, como neste exemplo em que preencheremos o usuário como "teste", o nome como "teste 1" e a senha como "teste 2". Ao clicarmos em cadastrar, ele fará a ação e voltará para tela de Login.

Se formos até o "Logcat", teremos um log que irá mostrar a informação de usuário. Na barra de pesquisa, tentarei filtrar por "teste" para mostrar para vocês. Apareceu um log informando que é possível criar o usuário que cadastramos, então já temos um modelo de usuário específico para trabalharmos e avançarmos para o próximo passo do Room.

De funcionalidades novas, basicamente são essas que temos disponíveis para o projeto inicial. O próximo passo que precisamos entender é como está o código. Se, por exemplo, buscarmos por "LoginActivity" , teremos o código que apresenta todo conteúdo da tela de autenticação.

Na nossa tela de autenticação temos o val binding , um layout específico para activity e assim por diante. É todo conteúdo que já aprendemos, com uma ressalva da função vaiPara(), que eu criei. Está é uma extensão onde conseguimos reutilizar a nossa Intent. Conseguimos enviar apenas a classe da Activity que queremos abrir e, dessa forma, ela já abre para nós.

Então eu escrevi essa extensão para facilitar o modo como abrimos novas activities, porque estamos aumentando o número delas no projeto. Em relação à tela de Login é isso. Basicamente, quando clicamos no botão entrar, fazemos um processo similar à tela de cadastro, que será colher essas informações e realizar uma impressão.

Posso demonstrar isso clicando em "ENTRAR", na nossa aplicação. Procurando "oncreate" no Logcat, notaremos que temos o log similar ao que vimos na tela de cadastro. Vamos voltar na tela de cadastro e verificaremos o código do cadastro.

Para tela de cadastro, teremos o "FormularioCadastroUsuarioActivity" . Percebemos que esse código também é similar ao que observamos na tela de Login, a diferença é que temos as informações de cadastro como val binding . As mesmas técnicas que já aprendemos também estão sendo utilizadas nesse código.

Agora que falei das funcionalidades em relação às mudanças no código, e o que temos de novo, outro passo que preciso esclarecer é que, nesses códigos, já estamos utilizando operações assíncronas com o Room.

No nosso primeiro contato o Room, utilizamos ele de forma que permitisse uma operação com o banco de dados na main thread. Vimos que isso não é uma boa prática e, a partir desse projeto, já estou utilizando corotines para usar uma operação assíncrona. Inclusive, como pré-requisito deste projeto, eu recomendo que vocês assistam a nossa websérie, que é um conteúdo dedicado à operação assíncrona. A websérie ensina também como utilizamos corotines dentro do Android. Especificamente, uma implementação do Room. Também estarão disponíveis atividades, então vocês poderão assistir e aprender, sendo bem tranquilo de absorver essa nova maneira de trabalhar com operação assíncronas e o Room.

Para vocês observarem onde estão essas mudanças, acessaremos a "ListaProdutosActivty". Nela percebemos que, para realizar uma busca, utilizamos uma corotine a partir do lifecycleScope.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        configuraRecyclerView()
        configuraFab()
          lifecycleScope.launch {
               dao.buscaTodos().collect { produtos ->
                  adapter.atualiza(produtos)
               }
          }
}

Nesse trecho é possível observar o processo de busca, o acesso ao flow, que também é um assunto comentado na websérie, e dessa forma conseguimos buscar as informações de maneira assíncrona, como é o caso do cadastro das informações e assim por diante. Podemos testar.

Vamos cadastrar novamente um produto. Podemos reparar que conseguimos realizar a listagem sem nenhum problema. Então se acessarmos o "FormularioProdutoActivity" e conferirmos o botão "SALVAR", notaremos que também utilizamos corotines.

 private fun configuraBotaoSalvar() {
        val botaoSalvar = binding.activityFormularioProdutoBotaoSalvar

        botaoSalvar.setOnClickListener {
            val produtoNovo = criaProduto()
            lifecycleScope.launch {
                produtoDao.salva(produtoNovo)
                finish()
            }
        }
 }

A partir do lifecycleScope , salvamos nosso produto utilizando a comunicação assíncrona para nos comunicarmos com o banco de dados, a partir do Room.

Essas são as novidades em relação ao nosso projeto. Também vou disponibilizar um commit indicando as atualizações de biblioteca que fizemos, possibilitando que vejam mais da parte técnica de atualizações.

Agora que passei todos os pontos que eu queria mostrar, podemos começar a modificar nosso código e trabalhar no nosso próximo exemplo, que será focado, inicialmente, nas telas de "Autenticação" e de "Cadastro de Usuário".

Até lá!

Estrutura do banco de dados - Configurando a entidade usuário

Agora que conhecemos o projeto que iremos utilizar durante o curso, podemos começar com nosso primeiro desafio, que é a integração do modelo que representa um usuário no aplicativo.

Para analisarmos esse modelo, podemos pressionar "Ctrl + N" e escrever "Usuario" para acessarmos a nossa classe Usuario , que é um data class com id , nome e senha .

data class Usuario(
    val id: String,
    val nome: String,
    val senha: String
)

Este Usuario é utilizado tanto na nossa "Tela de Autenticação", para realizar a autenticação, como na "Tela de Cadastro". Contudo, para realizarmos uma autenticação, é necessário que esse usuário exista em um meio persistente do nosso aplicativo, que é o banco de dados. Então, antes de tudo, o que precisamos fazer é adicionar um comportamento para conseguirmos salvar esse usuário a partir da nossa tela de cadastro.

Será exatamente isso que faremos: adicionar uma nova entidade, configurar um novo "dao" e, dessa forma, integrar com a nossa tela de cadastro. Para isso, a primeira coisa que precisamos fazer é transformar esse usuário em uma entidade do Room, a partir da anotação @Entity . Com ela definimos que nossa classe Usuario é uma entidade.

Como sabemos, as entidades precisam de chaves primárias, então precisamos escolher uma dessas propriedades para isso. Neste caso, será a id , porque a ideia é que o "id" seja único para usuário. Portanto, vamos declarar a @PrimaryKey.

@Entity
data class Usuario(
    @PrimaryKey
    val id: String,
    val nome: String,
    val senha: String

Assim temos um setting inicial para transformar o nosso usuário em uma entidade e realizar a configuração e integração com o Room. Agora precisamos configurar o "dao", porque ele quem permitirá a comunicação direta com o banco de dados, como é o caso de salvar os novos usuários. Para isso, precisamos acessar o pacote onde colocamos nossos "daos". Faremos isso navegando na coluna da esquerda por "app > java > br.com.alura.orgs > database > dao".

Criaremos um arquivo dentro da pasta "dao" clicando nela com o botão direito do mouse. Vamos escolher a opção "Kotlin Class/File" e, na janela que surge, selecionamos a opção "Interface", que é onde conseguimos fazer as nossas implementações, tal como fizemos na ProdutoDao . Neste caso, será UsuarioDao .

O que precisamos fazer para colocar um "dao", ou seja, uma interface, é a anotação @Dao . A partir desta anotação, colocamos um dao no nosso projeto e ele poderá ter os comportamentos que desejamos, como salvar novos usuários. Para isso, codaremos uma função.

@Dao
interface UsuarioDao {

    fun salva(usuario: Usuario)

Portanto a função salva() envia um usuario: Usuario , que segue o nosso modelo. Também precisamos definir o comportamento que queremos para essa função. Neste caso, queremos apenas que o dao faça a inserção, portanto utilizaremos @Insert .

@Dao
interface UsuarioDao {

    @Insert
    fun salva(usuario: Usuario)

Poderemos até voltar à discussão: "Quando houver o cadastro de um usuário com o mesmo id, queremos que ele substitua?". Neste caso, não. A nossa intenção é que, neste momento ele falhe, justamente para que, se o cadastro for do mesmo usuário, nós possamos fazer um tratamento. De repente um "try/catch" para informar que houve uma falha na criação de usuário e solicitar algo como tentar um usuário diferente. Por isso que, neste momento, não usaremos a técnica de substituição, assim como fizemos em ProduroDao . Esse é um ponto muito importante para considerar.

Outro detalhe legal para pensarmos é que, uma vez que utilizaremos coroutines, a fun salva() precisa ser uma função suspendida. É necessário que ela faça uma suspensão justamente para dar suporte a coroutines. Dessa forma será implementado o conceito de utilizar o dispatcher.io e evitar a execução na thread principal. Por isso escrevemos suspend fun salva(usuario: Usuario) e o Room irá nos gerar esse código.

Em seguida, o que faremos é configurar o nosso "AppDatabase" . O que faremos de diferente nele? Eu não comentei tanto, mas agora estamos utilizando um singleton nessa classe. Também podemos entender mais sobre o tema na nossa websérie para utilizar o flow. O que podemos realmente fazer é colocar a nossa nova entidade.

@Database(
    entities = [
        Produto::class,
        Usuario::class
    ],

Além disso, precisamos indicar que será criado um UsuarioDao() . Para isso codamos abstract fun usuarioDao(): UsuarioDao após a abstract fun produtoDao() . Perceberemos que, até então, essa configuração inicial é muito parecida com o que fizemos com o ProdutoDao . Não temos tantas novidades. O que precisamos fazer agora são os próximos passos, que é utilizar essa lógica que implementamos dentro da "Tela de Cadastro".

Para isso, acessaremos "FormularioCadastroUsuarioActivity" . Dentro dela, tentaremos acessar o nosso novo "dao" , a partir da mesma técnica de delegation property, que é a partir da inicialização by lazy{}. Portanto será uma property val que chamaremos de dao . Dentro do by lazy{} , vamos pedir a instância do database e acessaremos o usuarioDao .

private val dao by lazy {
    AppDatabase.instancia(this).usuarioDao()
}

Com esse trecho, conseguimos mandar nossa activity com AppDatabase.instancia(this) e, em seguida, acessar o dao com usuarioDao() . Então, neste momento, temos acesso ao nosso dao. Para fazer a integração, precisamos pegar o dao e, quando clicarmos para criar um usuário, precisamos modificar o código para a informação ser salva no nosso banco de dados. Após salvar, poderemos finalizar nossa activity.

Então faremos essa implementação. Começaremos estendendo nosso código com "Crtl + Shift + F12". Como estamos criando coroutines, nosso primeiro passo é criar uma coroutines a partir do nosso lifecycleScope a partir do launch{}.

Em seguida, utilizaremos o dao , chamando a função salva() e enviaremos o novoUsuario , criado quando clicamos no botão que salva, no caso, "CADASTRAR". Depois de salvo, se tudo estiver certo, queremos chamar o finish().

val novoUsuario = criaUsuario()
    Log.i("CadastroUsuario", "onCreate: $novoUsuario")
    lifecycleScope.launch {
        dao.salva(novoUsuario)
        finish()
    }

Como vimos, é possível existir a situação na qual criamos o mesmo usuário. Para esse caso específico, o que faremos? Colocaremos um "try/catch" no código para só finalizarmos se não ocorrer nenhuma exception.

Então selecionaremos dao.salva(novoUsuario) finish() e pressionaremos "Ctrl + Alt + T". No menu que abre, colocaremos "try" e selecionaremos "try/catch" e apertaremos "Enter". Assim o template do "try/catch" será implementado no nosso código.

lifecycleScope.launch {
    try {
        dao.salva(novoUsuario)
        finish()
    } catch (e: Exception)

Ele é bem genérico, mas será para captar algum erro. Caso não consigamos salvar o usuário, ele não finaliza a tela.

Quanto à informação para o usuário, podemos enviar, por exemplo, um Toast. O contexto será o this da activity. A mensagem vou deixar como "Falha ao cadastrar o usuário", mas você pode colocar a mensagem que preferir. Quando pressionamos "Enter", o editor já coloca cada argumento em uma linha, sem que precisemos fazer isso.

Toast.makeText(
    this@FormularioCadastroUsuarioActivity,
    "Falha ao cadastrar usuário",
    Toast.LENGTH_SHORT
)
    .show()

O que codamos é: se houver uma falha ao tentar cadastrar o usuário, vamos apresentar uma mensagem de falha. Dado que fizemos isso, outro passo legal para colocarmos neste ponto do código é a indicação de qual foi a exception que aconteceu. Assim poderemos ter um feedback exato do problema que surgiu neste comportamento.

Para isso, após o catch (e: Exception) escreveremos Log.e , para indicar que é um log de falha. Nossa tag será "CadastroUsuario". Por fim, colocaremos o e, indicando nossa exception. Portanto nosso código será

lifecycleScope.launch {
    try {
        dao.salva(novoUsuario)
        finish()
    } catch (e: Exception) {
        Log.e("CadastroUsuario", "configuraBotaoCadastrar: ", e)
        Toast.makeText(
            this@FormularioCadastroUsuarioActivity,
            "Falha ao cadastrar usuário",
             Toast.LENGTH_SHORT
        ).show()
    }
}

Então, de implementação, é basicamente isso que precisamos fazer neste primeiro momento, ou seja, codarmos a integração do nosso novo "dao", que salvará nosso usuário dentro da "Tela de Cadastro". Vamos executar e observar o que acontece com o nosso aplicativo.

Conseguimos executar nosso aplicativo e, até o momento, ele rodou sem nenhum problema. Então vamos tentar cadastrar um novo usuário para tentarmos salvá-lo no banco de dados. Para isso, acessaremos nossa "Tela de Cadastro".

Criaremos um usuário com informações simples. O usuário será "teste", o nome será "teste1" e a senha será "teste2". Clicando em cadastrar, aparece a mensagem "Falha ao cadastrar usuário". Ainda não temos esse usuário no banco de dados, então esse não é aquele problema de criar um usuário que já existe, portanto precisamos observar o que aconteceu no nosso Logcat.

Analisando o Logcat, perceberemos que o Room nos trouxe uma exception. Ele informou que não conseguiu verificar a integridade dos nossos dados. Também informa que parece que houve de uma mudança no nosso esquema, mas não mudamos a versão do banco de dados.

Isso significa que, quando colocarmos uma entidade nova, modificarmos a nossa entidade ou qualquer modificação na estrutura do Room e do banco de dados, sempre terá uma mensagem informando a necessidade de mudanças na nossa configuração. Uma vez que entramos nesse cenário, a seguir exploraremos como lidar com esse tipo de situação, ou seja, com qualquer tipo de mudança que tenhamos na estrutura do banco de dados.

Sobre o curso Android com Kotlin: Migrations e relacionamento com o Room

O curso Android com Kotlin: Migrations e relacionamento com o Room possui 205 minutos de vídeos, em um total de 49 atividades. Gostou? Conheça nossos outros cursos de Android em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Android acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas