Alura > Cursos de Mobile > Cursos de Android > Conteúdos de Android > Primeiras aulas do curso Jetpack Compose: utilizando Lazy Layout e estados

Jetpack Compose: utilizando Lazy Layout e estados

Carregando imagens via URL - Apresentação

Olá, pessoal! Eu sou Alex Felipe, instrutor da Alura, e vou apresentar para vocês o curso Jetpack Compose: Lazy layouts e estados. A partir deste curso, eu vou assumir que vocês têm alguns pré-requisitos em relação ao Jetpack Compose:

Caso não conheçam esses tópicos, acessem o curso Jetpack Compose: criando um app android, aqui da Alura. A partir dele, vocês conseguirão verificar e aprender todo o conteúdo necessário a partir deste segundo curso, que é uma continuação do primeiro.

Então daremos continuidade ao projeto do primeiro curso com lazy layout e estados. Se entrarmos no Android Studio e abrirmos nosso aplicativo, notem que ele tem algumas funcionalidades que o primeiro projeto não tinha. Por exemplo, na parte superior da tela inicial há um campo de texto, através do qual adicionamos uma nova funcionalidade ao nosso app, que é o Filtro de produtos.

Parte superior da tela do smartphone com o app aberto. No começo do app tem um campo de texto retangular com as bordas arredondadas, representando um campo de busca. Na parte superior esquerda desse campo, sobre o contorno azul escuro, está escrito "Produto" em letras pequenas". Dentro do campo de texto, tem uma lupa no lado esquerdo e o texto "O que você procura?". Embaixo do campo de texto está o texto "Promoções" e, depois dele, os cartões de produtos disposto em um carrossel. Os cartões são retangulares e têm uma faixa em degradê de lilás para roxo na metade superior. No centro do cartão está a foto redonda do produto. Na parte inferior esquerda do cartão está o nome e o preço do produto. Na imagem, o primeiro produto é um hambúrguer de R$ 12,99 e o segundo é uma pizza de R$ 19,99.

Por exemplo, se clicarmos nesse campo, o teclado do celular sobe na parte inferior da tela. Com ele, conseguimos digitar um conteúdo que queremos. Digitando tanto no teclado físico ou virtual, observamos a ação de filtragem dos resultados. Esse filtro irá pesquisar pelos resultados que queremos.

Sendo assim, se digitarmos a letra "a" nesse campo, ele busca todo conteúdo que tem "a" no nome ou descrição. Se escrevermos "hamburguer", ele irá procurar tudo que está relacionado a hamburguer.

No caso, teremos só um resultado porque só um produto tem esse nome. Enquanto isso, nas descrições estão preenchidas apenas com o Lorem Ipsum, que é o texto que utilizamos apenas para preencher conteúdo. Dessa forma, se escrevermos "ipsu" no campo de texto, aparecem outros produtos com essa descrição.

Então essa será a funcionalidade que adicionaremos ao nosso app e, para isso, utilizaremos novas técnicas. Um exemplo é a técnica de carregamento de imagens a partir de URL. Exploraremos bastante nas fotos dos nossos produtos, e aprenderemos como fazer isso a partir da Biblioteca Coil.

Também, como vimos, esse campo de texto é um Composable novo e aprenderemos a implementá-lo. Para adicionarmos essa funcionalidade e, quando buscarmos, a tela atualizar e mostrar apenas os resultados compatíveis, precisamos de uma introdução a estados dentro do Jetpack Compose.

Faremos o gerenciamento de estados e assim por diante e aprenderemos como lidar com isso e seus problemas. Outra técnica muito importante é o Lazy layout, que eu comentei anteriormente. Ele serve justamente para implementarmos essas soluções, porque temos uma coleção de composables.

Seja a nossa lista de produtos ou nossas seções, perceberemos que esse tipo de solução é bastante bem-vinda para garantir esse tipo de layout. Portanto entenderemos em detalhes como isso funciona e seus benefícios.

Aprenderemos também outras técnicas de boas-práticas, como o state hoisting. Entenderemos o que é o slot based, ou seja, a API de Slot, para personalizarmos os componentes. Descobriremos que existem slots para adicionarmos ícones, como a lupa, ou dicas do que escrever no campo, como o "o que você procura". Também entenderemos como isso funciona no Compose.

Notem que são várias informações e técnicas para esse projeto. Espero que tenham gostado e aguado vocês na primeira aula.

Vamos começar?

Carregando imagens via URL - Apresentando o projeto

Antes de modificarmos nosso código, nesse momento eu apresentarei algumas modificações que ocorreram no projeto do curso anterior:

Começando pela versão do Android Studio. Para saber qual a versão que estamos usando, na barra superior do Android Studio e navegamos por "Help > About". Com isso, uma janela é aberta no centro da tela contendo as informações da IDE. A partir desse curso usaremos a versão Android Studio Dolphin | 2021.3.1 RC 1, que está acima da versão do curso anterior.

Um pequeno detalhe que notamos é que essa não é a versão de release, porque neste momento ainda não há a versão de lançamento. Contudo, não se trata de uma versão beta ou Canary, e sim uma "RC", ou seja, uma Release Candidate (Candidata a Lançamento).

Na prática, isso significa que essa é a versão mais próxima de lançamento e talvez, durante a gravação, tenha esse lançamento, por isso já estou utilizando-a. Por isso recomendo que utilizem a versão Dolphin, seja alguma versão RC ou o próprio lançamento, para acompanhar o conteúdo.

Outro ponto que notamos é relacionado à visualização inicial do nosso projeto. Ao invés de usarmos as cores padrão que vêm na criação do projeto, agora foi definido um azul índigo para o header, que veremos as especificações com mais detalhes depois. Também observamos que as imagens estão apenas com o placeholder ao invés das fotos do curso anterior, e vamos entender o porquê.

Tela inicial do aplicativo Aluvery. São exibidas as seções "Promoções", "Doces" e o começo da seção de "Bebidas". Nas duas primeiras seções aparecem os dois primeiros cartões representando os itens de produto. A header, ou seja, a capa do cartão, está em um degradê de azul índigo da esquerda para direita. No lugar das imagens do produto estão círculos cinzas, que é a imagem do placeholder. Abaixo da imagem, alinhado à esquerda, estão o nome e o preço do prduto

Agora que passamos pela parte visual, vamos analisar as mudanças no código. Para isso, eu separei um commit no GitHub para observarem tudo que foi atualizado em relação ao curso anterior.

Então se baixarem o projeto inicial, acessando a próxima atividade, ele já estará atualizado com todas as mudanças que eu vou mostrar nesse commit. Então vamos analisar essas mudanças para sabermos ao que devemos nos atentar.

No commit do GitHub temos todas as mudanças. Inclusive na lateral esquerda dos arquivos temos um Explorer indicando quais arquivos que sofreram alteração.

Usaremos esse Explorer para acessarmos as mudanças que queremos verificar, então ao clicarmos em build.gradle, na pasta "app", no lado direito do Explorador aparecem as indicações de mudanças feitas no código. Então aproveitem bastante esse Explorador para identificarem o que mudou.

O que rapidamente notamos no arquivo build.gradle é a mudança de versão do compilador do Compose para o kotlinCompilerExtensioVersion = '1.3.0'. Antes usávamos uma variável criada para reutilizar a versão em todos os módulos do Compose, e entenderemos o motivo dessa troca. Além dele, ainda no build.gradle, duas bibliotecas foram atualizadas:

//Código suprimido

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.activity:activity-compose:1.5.1'

//Código suprimido

Essas bibliotecas são atualizadas constantemente, e por isso adicionei essas, que estavam disponíveis, quando alterei o projeto. Provavelmente haverá outras atualizações quando forem fazer o curso e vocês podem atualizar.

Rolando para baixo o lado esquerdo, onde aparecem as alterações do código, encontramos as alterações feitas na MainActivity.kt. Nela temos a nossa HomeScreen(), a primeira tela que aparece quando abrimos o aplicativo no emulador, mostrando as categorias de produtos. Agora ela recebe o sampleSections nos parênteses.

//Código suprimido

HomeScreen(
    sampleSections
)

//Código suprimido

Entenderemos melhor o que é o sampleSections, mas adianto que é para definição das seções de produto que aparecem na tela. Seguindo nossa análise, o modelo de produto foi modificado. No modelo Product.kt, reparamos que agora o modelo não recebe mais um @DrawableRes, e sim uma String que pode ser nula. Entenderemos essa modificação depois, por enquanto entendam apenas que tivemos essa modificação bastante impactante.

Rolando a tela para o arquivo do SampleData.kt, observamos que há mais informações de amostra do que antes, tanto de doces, quanto de bebidas. Além disso, nós mantivemos a lista sampleProducts, com o hambúrguer, pizza e batata frita, mas agora adicionamos as amostra de doces e bebidas.

Na prática isso significa que todos os produtos que vemos na categoria "Promoções", na parte superior do aplicativo, tem tanto os produtos genéricos, como bebidas e doces. Essa adição é observada na linha de código sampleDrinks.toTypedArray(), *sampleCandies.toTypedArray(). Com ela as listas criadas anteriormente são transformadas em array e são adicionados apenas os valores, através do *.

No final no Product.kt, criamos um mapa representando as seções. Esse mapa tem uma chave que é uma String representam o título da seção, como "Promoções", e o valor, que é a lista de produtos com as amostras que queremos. Nossa HomeScreen, como veremos com mais atenção, também recebe esse mapa.

//Código suprimido

val sampleSections = mapOf(
    "Promoções" to sampleProducts,
    "Doces" to sampleCandies,
    "Bebidas" to sampleDrinks

Seguindo nossa análise, notamos que o ProductItem.kt passou por uma mudança na cor, que agora é uma lista do MaterialTheme.colors.primary e o MaterialTheme.colors.secondary, ou seja, utilizamos o próprio tema. Anteriormente usamos Purple500, Teal200 por ser nosso primeiro contato com as cores no Compose, mas agora, utilizando o tema, os ajustes serão conforme as configurações do tema.

Ainda no ProductItem.kt, observamos um //TODO ajustar imagem do produto, que será nossa primeira tarefa depois de revisarmos as mudanças de código. Atualmente as imagens estão apresentando o placeholder e trabalharemos para que sejam exibidas conforme a String que pode ser nula.

Prestando mais atenção ao nosso SampleDate.kt, reparemos que as imagens são representadas por uma URL, portanto será a partir dessa URL que carregaremos nossas imagens. É uma abordagem comum porque, quando temos muitos objetos, não faz sentido esperarmos que as imagens estejam disponíveis no app.

Nesses contextos, as imagens serão disponibilizadas a partir de um conteúdo que pode ser carregado via Internet. Esse é o padrão que costumamos utilizar e aprenderemos isso na próxima atividade.

Seguindo com nossa análise, reparamos que fizemos mais alterações no nosso ProductItem.kt. No @Preview, substituímos a imagem que era um drawable por um ProductItem().

//Código suprimido

@Preview(showBackground = true)
@Composable
private fun ProductItemPreview() {
    AluveryTheme {
        Surface {
            ProductItem(
                Product(
                    name = LoremIpsum(50).values.first(),
                    price = BigDecimal("14.99")
                )
            )
        }
    }
}

Seguindo para a ProductsSection.kt, tivemos uma modificação de tema. Agora adicionei o AluveryTheme para possibilitar uma verificação de dark ou light mode (modo escuro ou claro). Descendo, observamos que eu removi o arquivo TestComponents.kt do projeto, porque não iremos mais utilizá-lo.

Já no arquivo HomeScreen.kt, agora teremos as seções, com o sample sections, que é um Map<String, List<Product>>. Nossas seções também passaram a ser carregadas dinamicamente com uma interação for.

for (section in sections) {
    val title = section.key
    val products = section.value
    ProductsSection(
        title = title,
        products = products
    )
}

Com o for, extraímos o título e o valor de cada seção que obtivermos e montamos a seção de produtos enviando esses dados. Também adicionamos o AluveryTheme nesse arquivo, possibilitando a verificação de modo claro ou escuro.

As cores também foram modificadas no arquivo Color.kt:

val Indigo400 = Color(0xFF5C6BC0)
val Indigo500 = Color(0xFF3F51B5)
val Indigo400Light = Color(0xFF5C6BC0)

Essa modificação foi refletida no tema, e por isso estamos utilizando o tema no ProductItem. Então, no Theme.kt, temos alterações nas cores do tema claro e escuro. Vocês podem analisar os detalhes dessas trocas no commit.

private val DarkColorPalette = darkColors(
    primary = Indigo400,
    primaryVariant = Indigo400Light,
    secondary = Indigo500,
    onSecondary = Color.White
)

private val LightColorPalette = lightColors(
    primary = Indigo400,
    primaryVariant = Indigo400Light,
    secondary = Indigo500,
    onSecondary = Color.White

Abaixo do arquivo de tema, notamos que apagamos todas as imagens que adicionamos ao projeto. Depois, no arquivo build.gradle, reparamos que a versão do Compose mudou para a versão de lançamento, porque no curso passado usávamos a versão beta, que era a mais recente disponível. Além disso, como mudamos a versão do Android Studio, mudamos a versão dos plugins e do Kotlin.

Quanto às versões, eu quero destacar que, no Compose, temos a versão 1.2.1, mas no app/build.gradle, que é o arquivo do compilador, temos a versão 1.3.0. Podemos entender porque isso acontece analisando a página da documentação onde mostra as versões do Compose.

Notamos que existem esses módulos fundamentais para o Compose funcionar, mas o compilador tem uma versão diferente. Isso acontece porque a evolução desse compilador é independente dos outros módulos e, portanto, é atualizado de forma independente, algo que precisamos nos atentar.

Sendo assim, a versão disponível para o compilador nem sempre será a mesma para os outros módulos. Pode acontecer de precisarmos usar versões diferentes ou as que estejam disponíveis em release.

Esses são todos os detalhes e, por ser muita informação, eu recomendo que acessem o commit para observarem detalhadamente. Provavelmente notarão diferença em relação a versão mais recente do Android Studio.

Por exemplo, abrindo o Android Studio e pressionando "Ctrl + Shift + N", abrimos o campo de busca. Pesquisaremos e abrirmos o arquivo build.gradle(Aluvery). Notamos que a versão da IDE é a 'com.android.application' version '7.3.0-rc01', referente ao plugin da versão Dolphin. No commit ainda aparece a versão do Chipmunk.

Essa pode ser a diferença, mas tranquilizem-se que eu tentarei manter o GitHub atualizado para passar esse conteúdo para vocês. Era isso que precisávamos ver, em seguida passaremos para nossa próxima tarefa.

Carregando imagens via URL - Carregando imagem com o Coil

Agora que conhecemos todas as modificações que feitas no nosso projeto, seguiremos com a primeira tarefa: obter as imagens representadas por URL para exibi-las no composable de imagem. Aprenderemos agora como fazer isso. Em passos, a ordem é a seguinte:

  1. Acessar o conteúdo na Internet;
  2. Fazer o download do conteúdo, para que o tenhamos disponível dentro no nosso aplicativo;
  3. Converter o conteúdo para um tipo que nosso Image() consegue apresentar, ou seja, painter, bitmap ou image vector.

Assim conseguimos exibir o conteúdo. Notem que enumerando os passos, parece algo bem simples, mas esse tipo de código tem uma certa complexidade para funcionar perfeitamente. Isso significa que, ao invés de seguirmos esses passos manualmente, usaremos a biblioteca Coil.

A Coil é uma biblioteca bastante na comunidade Android, para fazer esses passos por nós, porque essa é uma abordagem comum, mas um pouco trabalhosa de se fazer manualmente. Essa biblioteca, inclusive, tem um módulo destinado ao Jetpack Compose, mas ela também é utilizada em sistema de views, caso se interessem.

Como nosso trabalho é com o Compose, utilizaremos o módulo de Compose da Coil. Para isso, precisamos adicionar essa biblioteca ao nosso projeto e, ao termos acesso a ela, teremos acesso ao composable AsyncImage, que indica que será uma imagem assíncrona.

Com isso, ao definirmos uma imagem, o AsyncImage fará a requisição e, enquanto estiver baixando, ele não exibirá o conteúdo esperado. Finalizado o processo, ele mostra a imagem. Portanto, o assíncrono significa que a tarefa será executada em paralelo à execução principal e, quando for finalizada, o resultado é exibido.

Dessa forma, o que precisamos agora é adicionar a dependência e usar esse composable. Começamos copiando o "io.coil-kt:coil-compose:2.2.0" da documentação e voltamos para o Android Studio.

Pressionando "Ctrl + Shift + N", pesquisaremos e acessaremos o build.gradle(:app). Dentro desse arquivo, em dependencies, adicionamos a dependência, atentando-se para que seja a versão 2.2.0. Caso tenha algo diferente da aula, pode estar relacionado à versão diferente que estão usando.

dependencies {
    implementation "io.coil-kt:coil-compose:2.2.0"
//Código suprimido

Por fim, clicaremos no "Sync Now", que está no canto superior direito da janela de código. Após a sincronização, teremos disponível todo o conteúdo do Coil.

Feito isso, primeiramente precisamos voltar no ProductItem.kt e, na linha 55, modificar o Image() por AsyncImage(). Notaremos que o código não irá compilar, porque ele não recebe painter, image vector ou bitmap, e sim um model, que pode receber null ou Any?, que são dois tipos de informação.

Voltando para a documentação da biblioteca, observamos que a AsyncImage() pode receber tanto uma String, que é a URL que queremos, ou a ImageRequest, que é uma referência que possibilita um acesso mais preciso ao modo como a requisição será feito. No exemplo da documentação, o ImageRequest traz dados de qual será a URL, se terá efeito para carregar e afins.

AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data("https://example.com/image.jpg")
        .crossfade(true)
        .build(),
    placeholder = painterResource(R.drawable.placeholder),
    contentDescription = stringResource(R.string.description),
    contentScale = ContentScale.Crop,
    modifier = Modifier.clip(CircleShape)
)

Portanto, se precisarem definir mais detalhes de exibição da imagem, usarão o ImageRequest, mas no caso deste curso, precisamos apenas enviar o endereço, ou seja, passaremos a String com a URL. Então voltaremos para IDE para adicionar o endereço. Como temos acesso ao produto e o produto tem acesso à URL, basta codar model = product.image na linha 58.

{
  AsyncImage(
      // TODO: ajustar imagem do produto
      model = product.image,
      contentDescription = null,
      Modifier
          .size(imageSize)
          .offset(y = imageSize / 2)
          .clip(shape = CircleShape)
          .align(Alignment.BottomCenter),
      contentScale = ContentScale.Crop
      )
}

Quando mudamos o código, recebemos a notificação que precisamos atualizar o preview, então usando o atalho "Ctrl + Shift + F5", percebemos que nosso preview não mostra mais o círculo com o placeholder como antes. Agora não há uma imagem dentro do projeto para fazer uma exibição, e a requisição não é feita enquanto o aplicativo não é executado.

Não se preocupem, porque existem uma técnica para verificar o preview e, após observamos o funcionamento do Coil, aprenderemos essa técnica. Feito isso, ao tentarmos executar o app, eu já adianto que teremos um problema, mas vou mostrar ele acontecendo para conseguirem identificá-lo.

Então vamos abrir o "Logcat", clicando no sexto botão da barra inferior do Android Studio. Algo interessante é que, a partir da versão Dolphin, o Logcat tem uma visualização diferente, e não apresenta apenas um texto branco.

Dica: A Jeniffer Bittencourt (Jeni), do Scuba Team Mobile, gravou um Alura+ sobre as Atualizações do Logcat na versão Dolphin do Android Studio!

Vamos limpar o Logcat, clicando no ícone de lixeira que fica no canto superior esquerdo do Logcat, e pressionar "Shift + F10" para executar o app. Com isso nossa aplicação é executada e quebra em seguida.

No Logcat somos notificados de uma exception indicando que ainda não temos a permissão de Internet, porque precisamos baixar o conteúdo da Internet. Como precisamos dessa permissão, precisamos realizar uma configuração no nosso aplicativo.

Para isso, buscaremos e abriremos o arquivo AndroidManifest.xml e, antes do <application, teremos acesso à algumas tags. Nesse caso usaremos a <uses-permission.

A partir dela, teremos a configuração de diversas permissões que podemos solicitar durante a instalação do nosso aplicativo, dentre elas, a permissão de Internet, codando <uses-permission android:name="android.permission.INTERNET" />. Com essa permissão, solucionamos o problema e podemos testar novamente.

Ao executarmos o código novamente e o aplicativo abre, mas as imagens não são carregadas imediatamente. Entretanto, após algum tempo as fotos dos produtos começam a aparecer. Isso acontece porque o download depende da Internet, então quanto melhor a conexão, mais rápido as imagens aparecem.

É muito importante verificarem, inclusive no material que vamos oferecer a vocês, se as imagens ainda estão disponíveis, porque se não estiverem, o conteúdo não será exibido no aplicativo. Com isso, notamos que não temos nenhum feedback, seja no preview ou quando a imagem não é carregada, o que pode aparecer estranho.

Quando as imagens dos meus cartões demoraram para carregar, observamos que o espaço ficou vazio, o que não é uma abordagem interessante. A pessoa que usar nosso app precisa saber que um espaço conterá uma imagem, que a imagem está carregando ou algo do gênero. O Coil oferece essas técnicas para nós, então aprenderemos como fazer isso.

Voltando para o ProductItem.kt(), e acessando o AsyncImage, teremos acesso a algumas técnicas, entre elas, o placeholder. Portanto, o AsyncImage placeholder como a imagem que aparecerá no preview e no lugar da imagem que não for carregada.

Nesse método placeholder, podemos usar o painterResource(id = R.drawable.placeholder). Por esse motivo a image do placeholder foi mantida no projeto, porque utilizamos esse conteúdo.

{
  AsyncImage(
      // TODO: ajustar imagem do produto
      model = product.image,
      contentDescription = null,
      Modifier
          .size(imageSize)
          .offset(y = imageSize / 2)
          .clip(shape = CircleShape)
          .align(Alignment.BottomCenter),
      contentScale = ContentScale.Crop,
      placeholder = painterResource(id = R.drawable.placeholder),
  )
}

Pressionando "Ctrl + Shift + F5", visualizamos essa mudança no preview, já que o placeholder agora está no lugar da imagem. Sendo assim, vamos executar aplicativo de novo para descobrir se ele funciona como esperado. Assim notamos que ele carregou inicialmente o placeholder, por padrão e depois as imagens.

Nessa segunda execução do aplicativo, minha Internet foi mais ágil, então não foi possível observar muito bem o placeholder, mas vocês podem voltar o vídeo e até diminuir a velocidade de reprodução para observar isso melhor. Sendo assim, enquanto ele não carregar a imagem, ele terá o placeholder disponível.

Dica: O placeholder pode ser a imagem que vocês quiserem, então caso queiram substituir a imagem cinza por outra imagem, como uma animação de carregamento, fiquem à vontade!

Era isso que precisávamos fazer, sendo assim, podemos apagar o //TODO. Agora nossas imagens são carregadas de maneira dinâmica!

Um último ponto que quero ressaltar é o que Coil é uma ferramenta muito poderosa para carregar imagens dinâmicas, então recomendo que deem uma olhada na documentação do Coil. Se tiver algo mais que queiram fazer, podem adicionar ao projeto e configurar, mas quero deixar claro que o objetivo do curso não será trabalhar com as possibilidades do Coil.

Por isso recomendo a vocês analisarem a documentação para descobrir se existe algo mais que queiram adicionar. Era isso que queria passar para vocês neste vídeo!

Sobre o curso Jetpack Compose: utilizando Lazy Layout e estados

O curso Jetpack Compose: utilizando Lazy Layout e estados possui 172 minutos de vídeos, em um total de 59 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