Alura > Cursos de Mobile > Cursos de Android > Conteúdos de Android > Primeiras aulas do curso Jetpack Compose: mantendo estados com ViewModel

Jetpack Compose: mantendo estados com ViewModel

Descrição dinâmica - Apresentação

Olá! Eu sou o Alex Felipe da Alura e vim te dar boas-vindas ao Curso Jetpack Compose: Mantendo estados e ViewModel!

A partir deste curso, vamos assumir que você tenha alguns pré-requisitos, como por exemplo:

Caso haja alguma dúvida sobre os pré-requisitos, recomendamos que assista o curso Jetpack Compose: formulário e gerenciamento de estado antes de progredir com o nosso conteúdo. No curso citado trabalhamos no aplicativo Aluvery e focamos em implementar um formulário com as técnicas de gerenciamento de estados para dar cada vez mais funcionalidades a ele.

O que aprenderemos neste curso ? Em nível de produto, comparando ao Aluvery do curso anterior, adicionaremos mais uma funcionalidade visual. Sabemos que alguns produtos já possuem descrições, entretanto, no momento em que realizarmos um filtro no aplicativo, essas descrições não aparecem na tela. Adicionaremos uma função de clique para que elas se tornem visíveis ou sejam ocultadas a partir de um evento de clique.

Essa funcionalidade parece bastante simples. Entretanto, devemos perceber que para implementá-la teremos desafios, como por exemplo, aprender a manter este comportamento na tela mesmo se a rolarmos ou a virarmos na horizontal. Nem todo aplicativo consegue realizar essa tarefa. A partir deste curso entenderemos por que esse problema ocorre, como esta funcionalidade trabalha e por que precisamos nos preocupar com esse tipo de situação.

Já no ambiente de cadastro de itens, se estivermos no meio do processo e rotacionarmos a tela, por padrão perderemos as nossas informações. Para conseguirmos evitar esse problema, precisaremos entender cada vez mais o funcionamento dos estados, quais as preocupações que precisamos ter e quais são as boas práticas recomendadas — como por exemplo a utilização do ViewModel — para mantermos tudo funcionando com códigos cada vez mais simples e bonitos. Este processo é cada vez mais comum em aplicações Android que utilizam Jetpack Compose.

No que tange a funcionalidade este conteúdo pode parecer simples, contudo haverá bastante profundidade nas partes de código e de lógica — elementos muito importantes para o desenvolvimento da carreira como dev Android.

Vamos começar? Nos vemos no próximo vídeo.

Descrição dinâmica - Inserindo descrição com o clique

Antes de falarmos sobre formas de utilizar o Jetpack Compose para manter o estado do nosso aplicativo, adicionaremos a ele uma nova funcionalidade.

No momento da filtragem pelo campo de busca, são exibidos cartões de itens com as informações de nome, preço e descrição. Podemos ver que o campo de descrição pode aumentar bastante de tamanho conforme o comprimento do texto em seu interior. Portanto, adicionaremos uma função de clique para que inicialmente as descrições dos itens não sejam exibidas. Elas devem se tornar visíveis ou devem ser ocultadas somente a partir de uma ação — como, por exemplo, um clique.

Já vimos esse comportamento no curso de introdução sobre Jetpack Compose, no exemplo da própria documentação. Adicionaremos exatamente essa funcionalidade.

Por que adicionar essa funcionalidade? A partir dela teremos um exemplo muito comum em diversos aplicativos que utilizam Jetpack Compose. Além de ser comum, esse exemplo relaciona-se aos problemas que resolveremos quando tratamos de manter o estado no Jetpack Compose. Vamos começar com a implementação.

Acessaremos o nosso Android Studio e buscaremos o código do componente de cartão. Dado que se trata de uma função Composable podemos buscá-lo de outras maneiras que não sejam através da classe, como por exemplo pressionando "Shift + Shift" duas vezes para abrir a barra de pesquisa "Search Everywhere" ("Buscar em tudo", em português) na seção superior da IDE. Esta barra permite que alternemos entre arquivos, symbols e assim por diante. Se nela buscarmos o termo "Card" o sistema abrirá uma lista suspensa com as opções disponíveis: em primeiro lugar, o arquivo CardProductItem.kt e logo abaixo a opção CardProductItem(Product,Modifier,Dp) que representa a nossa função Composable. Clicaremos nele e a tela principal abrirá o código diretamente nesta seção em conjunto com uma tela de pré-visualização do design à direita.

@Composable
fun CardProductItem(
    product: Product,
    modifier: Modifier = Modifier,
    elevation: Dp = 4.dp
) {
    Card(
        modifier
            .fillMaxWidth()
            .heightIn(150.dp),
            elevation = elevation
    ) {
            Column { 
                AsyncImage(
                    model = product.image,
                    contentDescription = null,
                    Modifier
                        .fillMaxWidth()
                        .heightIn(150.dp),
                        elevation = elevation
                ) {
                Column {
                    AsyncImage(
                        model = product.image,
                        contentDescription = null,
                        Modifier
                            .fillMaxWidth()
                            .height(100.dp),
                        placeholder = painterResource(id = R.drawable.placeholder),
                        contentScale = ContentScale.Crop
                    )
                    Column(
                        Modifier
                            .fillMaxWidth()
                            .background(MaterialTheme.colors.primaryVariant)
                            .padding(16.dp)
                        ) {
                            Text(
                                text = product.name
                            )
                            Text(
                                text = product.price.toBrazilianCurrency()
                            )
                        }
                        product.description?.let {
                            Text(
                                text = product.description,
                                Modifier
                                    .padding(16.dp)
                            )
                        }
                    }
                }
            }

Vamos implementar o código que manterá a lógica que desejamos. Anteriormente vimos um exemplo na documentação e entendemos como lidar com estados, portanto vamos aplicar tudo o que aprendemos.

Precisamos de um estado que determine se o conteúdo da descrição será expandido ou não. Vamos começar por ele. Criaremos no interior de fun CardProductItem, acima do Card(), uma var expanded que será um by remember{} cuja inicialização será um mutableState. Importaremos o nosso remember e dentro das chaves dele adicionaremos um mutableStateOf() que receberá inicialmente o valor value:false, pois este comando determina que a descrição será escondida.

@Composable
fun CardProductItem(
    product: Product,
    modifier: Modifier = Modifier,
    elevation: Dp = 4.dp
) {
    var expanded by remember {
        mutableStateOf(value:false)
    }
    Card(
        modifier
            .fillMaxWidth()
            .heightIn(150.dp),
        elevation = elevation
    ) {

Em seguida faremos o import de getValue e mutableStateOf para que o sistema seja capaz de lê-los e inserir valores em nosso delegate. Precisamos utilizar esse estado para criar a lógica que apresenta ou não o conteúdo. Vamos inseri-la na seção que mostra a descrição do produto: product.description?.let {}.

            product.description?.let {
                            Text(
                                text = product.description,
                                Modifier
                                    .padding(16.dp)
                            )

Vamos envolver esta seção com um if (expanded) {}. Desta forma, se a condicional retornar true exibiremos a descrição do item.

            if (expanded){
                product.description?.let {
                    Text(
                        text = product.description,
                        Modifier
                            .padding(16.dp)
                    )
            }

Este comando não é o suficiente para apresentarmos o conteúdo como esperamos, pois é necessário que haja uma mudança de valor com base em um evento, que em nosso caso é um clique. Vamos identificar em qual seção devemos adicionar o clique. Dado que o clique poderá ser feito em todo o produto — seja nas imagens, nomes, preços ou descrições — adicionaremos esse elemento diretamente na seção Card. Dentro do modifier chamaremos a função .clickable{}, com a qual adicionaremos a habilidade de clique dentro de nossos componentes. Em sua criação podemos adicionar um comportamento que seja capaz de alternar entre os estados que retornam verdadeiro ou falso de acordo com o que desejarmos. Para isso, dentro das chaves de .clickable adicionaremos o estado expanded junto a um !expanded que indica o valor contrário do estado naquele momento. Se em outro momento o estado for verdadeiro esse valor será falso e assim por diante. Esta funcionalidade também é conhecida como Toggle, ou alternância.

@Composable
fun CardProductItem(
    product: Product,
    modifier: Modifier = Modifier,
    elevation: Dp = 4.dp
) {
    var expanded by remember {
        mutableStateOf(value:false)
    }
    Card(
        modifier
            .fillMaxWidth()
            .heightIn(150.dp),
            .clickable{
                expanded = !expanded
            }
        elevation = elevation
    ) {

Vamos testar este código para analisarmos aos poucos o que estamos aplicando. Daremos "Shift + F10" para compilarmos nosso código e abrir o emulador. Na tela emulada acessaremos o campo de busca de itens e filtraremos digitando "lorem". Quando filtramos a aplicação exibe os produtos sem as descrições. Se clicarmos em um item, a descrição é exibida. Isso significa que a nova funcionalidade está ativa como esperamos. O único ponto com o qual temos que lidar é o preview que antes exibia um cartão com a descrição e agora não exibe mais. Para consertá-lo precisamos utilizar os parâmetros ao invés de apenas um estado interno. Faremos isso retornando ao código, acessando a seção fun CardProductItem() e definindo dentro dos parênteses um parâmetro isExpanded o qual fornecerá a habilidade de apresentar ou não o conteúdo. Vamos indicá-lo como valor Boolean e adicionaremos a ele um valor padrão false, já que outros códigos que utilizam este cartão não correm o risco de "quebrar" (apresentar erros).

@Composable
fun CardProductItem(
    product: Product,
    modifier: Modifier = Modifier,
    elevation: Dp = 4.dp
    isExpanded: Boolean = false
) {
    var expanded by remember {
        mutableStateOf(value:false)
    }
    Card(
        modifier
            .fillMaxWidth()
            .heightIn(150.dp),
            .clickable{
                expanded = !expanded
            }
        elevation = elevation
    ) {

Retornaremos à seção mutableStateOf(value:false) para indicar que isExpanded será o valor inicial do estado no lugar do false. Desta forma o nosso estado poderá mudar internamente sem nenhum problema.

@Composable
fun CardProductItem(
    product: Product,
    modifier: Modifier = Modifier,
    elevation: Dp = 4.dp
    isExpanded: Boolean = false
) {
    var expanded by remember {
        mutableStateOf(isExpanded)
    }
    Card(
        modifier
            .fillMaxWidth()
            .heightIn(150.dp),
            .clickable{
                expanded = !expanded
            }
        elevation = elevation
    ) {

Dentro das chaves da seção fun CardProductItemWithDescriptionView(){} que trata do preview, ao lado dos parênteses de CardProductItem() adicionaremos uma vírgula. Em seguida daremos "Enter" para descermos de linha e indicaremos que haverá um isExpanded = true.

@Preview
@Composable
fun CardProductItemWithDescriptionPreview() {
    AluveryTheme {
        Surface {
            CardProductItem(
                product = Product(
                    "teste",
                    BigDecimal("9.99"),
                    description = LoremIpsum(50).values.first(),
                ),
                isExpanded = true
            )
        }
    }
}

Antes de realizarmos a build limparemos o projeto acessando novamente a barra de pesquisa através do atalho "Shift + Shift". Nela digitaremos "clean" e selecionaremos a opção Clean Project. Em seguida faremos a build clicando no ícone denominado "Make Project" ou através do atalho "Ctrl + F9". Após o término da build, veremos na janela de pré-visualização dois componentes:

Vamos testar o código executando a aplicação no emulador. Após este se abrir percereberemos que, ao acessarmos o campo de busca de itens e filtrarmos digitando "lorem", a aplicação exibe agora os produtos com as descrições.

Adicionamos essa nova funcionalidade. A seguir exploraremos um novo assunto: como manter o estado no Jetpack Compose. Até lá!

Descrição dinâmica - Situações de perda de estado

Nossa aplicação já é capaz de exibir ou ocultar a descrição da nossa lista de produtos a partir do clique. Seguiremos para o próximo passo: uma introdução sobre o que significa manter estados no Jetpack Compose. Quando falamos na ideia de "manter estado", tratamos da capacidade do aplicativo de manter o estado que vemos na tela independentemente do que acontecer. O ideal é que nosso aplicativo apresente exatamente a mesma tela de itens caso decidamos navegar pelo nosso formulário e voltar. Se escrevermos "lorem" no campo de pesquisa, a tela deverá manter esse texto presente.

A princípio nossa aplicação consegue recuperar esses estados. Entretanto, perceberemos que nem sempre essa recuperação funciona como esperamos.

Se repetirmos o processo de filtragem, digitando "lorem" e clicando no primeiro produto da lista, a descrição será exibida. Entretanto, se rolarmos a tela para baixo até o produto escolhido sumir e retornarmos ao topo, veremos que o item não estará mais clicado e a descrição que abrimos terá sumido. Portanto o estado não foi mantido pela aplicação. Este comportamento está relacionado com a forma de implementação que criamos anteriormente. Veremos um outro caso: se rotacionarmos a tela clicando no ícone "Rotate Right" localizado no topo da janela do emulador, o nosso filtro com o texto "lorem" será apagado. Se digitarmos novamente "lorem" na busca com a tela rotacionada, veremos que o filtro e o clique ainda funcionam. Contudo se rotacionarmos novamente, perderemos novamente esse estado. Esse problema se estende para a tela de formulário também.

Dica: Se você não conseguir rotacionar a tela, ative a opção "Auto-rotate" acessando as opções do dispositivo dentro do próprio emulador. É possível encontrá-la arrastando o topo da tela para baixo.

Por que ocorre esse problema? Para mantermos o estado em algumas situações, precisaremos aplicar técnicas extras.

Vamos entender cada um dos problemas. Iniciaremos pela lista exibida após aplicarmos o filtro. O conteúdo some após rolarmos a tela porque o escopo do Lazy possui uma técnica de otimização da apresentação dos dados para garantir a performance da aplicação. Ou seja, no momento em que o Composable é visível, ele é mantido em memória, contudo a partir do momento em que ele sai da parte visível da tela esse Composable é destruído e reconstruído pelo Lazy. Devido a essa reconstrução, a memória é liberada e a informação do estado é perdida.

Já no caso da rotação, o problema vai além do Compose. Temos uma situação na qual o próprio sistema do Android dispara um evento chamado mudança de configuração. Nele o Android recarrega os recursos necessários para apresentar outra configuração — no nosso caso, a tela horizontal. As telas horizontal e vertical possuem duas configurações diferentes. A consequência desta mudança é a destruição da activity, causando portanto a reexecução do código escrito em nossa activity.

Existem outros eventos, até mesmo do sistema operacional que causam a destruição da activity, como falta de memória ou mesmo quando o sistema operacional não consegue manter determinada activity por algum motivo.

Portanto, este problema é bastante comum no Android. Através dele conheceremos novas técnicas para lidar com cada situação e garantir que nossos comportamentos funcionem da maneira esperada. Vamos imaginar que uma pessoa está usando a nossa aplicação e, após preencher todo o formulário, por algum motivo, rotaciona a tela ou tem a activity destruída pelo sistema operacional, o que causa a perda de todas as informações que ela preencheu. Essa experiência não é legal, e precisamos saber evitá-la.

Precisamos entender que a característica de manter um estado no Jetpack Compose se aplica exatamente nesses casos em que a activity é destruída. Através dessa técnica conseguiremos manter o que já foi feito.

A seguir, conheceremos as principais ferramentas existentes no SDK do Android e no Jetpack Compose para nos auxiliar nessa tarefa. Até lá!

Sobre o curso Jetpack Compose: mantendo estados com ViewModel

O curso Jetpack Compose: mantendo estados com ViewModel possui 153 minutos de vídeos, em um total de 51 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