Carregar e mostrar imagens da Internet

Mantenha tudo organizado com as coleções Salve e categorize o conteúdo com base nas suas preferências.

1. Antes de começar

Introdução

Nos codelabs anteriores, você aprendeu a receber dados de um serviço da Web usando um padrão de repositório e analisar a resposta em um objeto Kotlin. Neste codelab, você vai aproveitar esse conhecimento para carregar e mostrar fotos de um URL da Web. Também vamos lembrar como criar uma LazyVerticalGrid e usá-la para mostrar uma grade de imagens na página de visão geral.

Pré-requisitos

  • Saber como extrair o JSON de um serviço REST da Web e analisar esses dados em objetos Kotlin usando as bibliotecas Retrofit e Gson (links em inglês).
  • Conhecimento sobre um serviço da Web REST (link em inglês).
  • Conhecimento sobre os componentes da arquitetura do Android, como camadas de dados e repositórios.
  • Conhecimento sobre a injeção de dependência.
  • Conhecimento sobre ViewModel e ViewModelProvider.Factory.
  • Saber implementar corrotinas para o app.
  • Conhecimento sobre o padrão do repositório.

O que você vai aprender

  • Como usar a biblioteca Coil (link em inglês) para carregar e mostrar uma imagem de um URL da Web.
  • Como usar um LazyVerticalGrid para mostrar uma grade de imagens.
  • Como processar erros possíveis durante o download e a exibição das imagens.

O que você vai criar

  • Você vai modificar o app Mars Photos para acessar o URL dos dados de imagens de Marte e usar a Coil para carregar e mostrar essas imagens.
  • Adicionar uma animação e um ícone de erro de carregamento ao app.
  • Adicionar o status e o tratamento de erros ao app.

O que você precisa

  • Um computador com um navegador da Web moderno, como a versão mais recente do Chrome.
  • Código inicial do app Mars Photos com serviços REST da Web.

2. Visão geral do app

Neste codelab, você vai continuar trabalhando com o app Mars Photos de um codelab anterior. O app Mars Photos se conecta a um serviço da Web para extrair e mostrar o número de objetos Kotlin acessados usando Gson. Estes objetos Kotlin contêm os URLs das fotos reais da superfície de Marte capturadas pelos rovers da NASA.

7cd0c9b9557027cf.png

A versão do app que você criar neste codelab vai mostrar fotos de Marte em uma grade de imagens. As imagens fazem parte dos dados que o app extrai do serviço da Web. Seu app vai usar a biblioteca Coil (link em inglês) para carregar e mostrar as imagens, e uma LazyVerticalGrid para criar o layout de grade para elas. O app também vai processar corretamente os erros de rede ao mostrar uma mensagem de erro.

d6334e93f09038b.png

Acessar o código inicial

Para começar, faça o download do código inicial:

Outra opção é clonar o repositório do GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
$ cd basic-android-kotlin-compose-training-mars-photos
$ git checkout coil-starter

Procure o código no repositório do GitHub do Mars Photos (link em inglês).

3. Mostrar uma imagem transferida por download

Mostrar uma foto de um URL da Web pode parecer simples, mas requer muito trabalho técnico para que isso funcione bem. A imagem precisa ser transferida por download, armazenada internamente (em cache) e decodificada do formato compactado para uma imagem que o Android possa usar. É possível armazenar a imagem em um cache na memória, no cache baseado em armazenamento ou em ambos. Tudo isso precisa acontecer em segmentos de baixa prioridade em segundo plano para que a IU permaneça responsiva. Além disso, para conseguir a melhor performance de rede e CPU, convém buscar e decodificar mais de uma imagem ao mesmo tempo.

Felizmente, é possível usar uma biblioteca criada pela comunidade, a Coil (link em inglês), para fazer o download, armazenar em um buffer, decodificar e armazenar as imagens em cache. Sem a Coil, você teria muito trabalho a fazer.

Resumidamente, a Coil precisa de duas coisas:

  • O URL da imagem que você quer carregar e mostrar.
  • Um objeto AsyncImage para mostrar essa imagem.

Nesta tarefa, vamos aprender a usar a Coil para mostrar uma única imagem de Marte recebida do serviço da Web. Você vai mostrar a imagem da primeira foto de Marte na lista de fotos que o serviço da Web retorna. As imagens abaixo mostram as capturas de tela "antes e depois":

Adicionar uma dependência da Coil

  1. Abra o app da solução do Mars Photos (em inglês) do codelab Adicionar repositório e injeção de dependência manual.
  2. Execute o app para confirmar que ele mostra a contagem de fotos de Marte extraídas.
  3. Abra o arquivo build.gradle (Module: app).
  4. Na seção dependencies, adicione esta linha à biblioteca Coil:
// Coil
implementation "io.coil-kt:coil-compose:2.1.0"

Verifique e atualize a versão mais recente da biblioteca na página de documentação da Coil (link em inglês).

  1. Clique em Sync Now para recriar o projeto com a nova dependência.

Mostrar o URL da imagem

Nesta etapa, você vai extrair e mostrar o URL da primeira foto de Marte.

  1. Em MarsViewModel.kt, no método getMarsPhotos(), no bloco try, encontre a linha que define os dados extraídos do serviço da Web para listResult.
// No need to copy, code is already present
try {
   val listResult = marsPhotoRepository.getMarsPhotos()
   //...
}
  1. Atualize essa linha mudando listResult para result e atribuindo a primeira foto de Marte extraída à nova variável result. Atribua o primeiro objeto de foto no índice 0.
try {
   val result = marsPhotoRepository.getMarsPhotos()[0]
   //...
}
  1. Na próxima linha, atualize o parâmetro transmitido à chamada de função MarsUiState.Success() para a string no código abaixo. Use os dados da nova propriedade em vez de listResult. Mostre o URL da primeira imagem da foto result.
try {
   ...
   MarsUiState.Success("First Mars image URL: ${result.imgSrc}"
}

O bloco try completo agora tem esta aparência:

marsUiState = try {
               val result = marsPhotoRepository.getMarsPhotos()[0]
               MarsUiState.Success(
                   "   First Mars image URL : ${result.imgSrc}"
               )
           }
  1. Execute o app. O Text de composição agora mostra o URL da primeira foto de Marte. A próxima seção descreve como fazer com que o app mostre a imagem nesse URL.

9fd921581972fe05.png

Adicionar a função de composição AsyncImage

Nesta etapa, você vai adicionar uma função de composição AsyncImage para carregar e mostrar uma única foto de Marte. AsyncImage é uma função que executa uma solicitação de imagem de forma assíncrona e renderiza o resultado.

// Example code, no need to copy over
AsyncImage(
    model = "https://android.com/sample_image.jpg",
    contentDescription = null
)

O argumento model pode ser o valor ImageRequest.data ou o próprio ImageRequest. No exemplo anterior, você atribui o valor ImageRequest.data, ou seja, o URL da imagem, que é "https://android.com/sample_image.jpg". O código de exemplo abaixo mostra como atribuir a própria ImageRequest para model.

// Example code, no need to copy over

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

AsyncImage oferece suporte aos mesmos argumentos que o elemento de composição de imagem padrão. Além disso, ele oferece suporte à configuração de pintores placeholder/error/fallback e callbacks onLoading/onSuccess/onError. O código de exemplo anterior carrega a imagem com um corte circular e um crossfade, e define um marcador de posição.

contentDescription define o texto usado pelos serviços de acessibilidade para descrever o que essa imagem representa.

Adicione uma função de composição AsyncImage ao seu código para mostrar a primeira foto de Marte extraída.

  1. Em HomeScreen.kt, adicione uma nova função de composição com o nome MarsPhotoCard(), que usa MarsPhoto e Modifier.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
  1. Na função de composição MarsPhotoCard(), adicione a função AsyncImage() desta maneira:
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .build(),
        contentDescription = stringResource(R.string.mars_photo)
    )
}

No código anterior, você cria uma ImageRequest usando o URL da imagem (photo.imgSrc) e o transmite ao argumento model. Use contentDescription para definir o texto para leitores de acessibilidade.

  1. Adicione crossfade(true) à ImageRequest para ativar uma animação de crossfade quando a solicitação for concluída com êxito.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .crossfade(true)
            .build(),
        contentDescription = stringResource(R.string.mars_photo)
    )
}
  1. Atualize o elemento de composição HomeScreen para mostrar o elemento MarsPhotoCard em vez de ResultScreen quando a solicitação for concluída. O erro de correspondência de tipo vai ser corrigido na próxima etapa.
@Composable
fun HomeScreen(
    marsUiState: MarsUiState,
    retryAction: () -> Unit,
    modifier: Modifier = Modifier
) {
    when (marsUiState) {
        is MarsUiState.Loading -> LoadingScreen(modifier = modifier)
        is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier)
        else -> ErrorScreen(retryAction = retryAction, modifier = modifier)
    }
}
  1. No arquivo MarsViewModel.kt, atualize a interface MarsUiState para aceitar um objeto MarsPhoto em vez de uma String.
sealed interface MarsUiState {
    data class Success(val photos: MarsPhoto) : MarsUiState
    //...
}
  1. Atualize a função getMarsPhotos() para transmitir o primeiro objeto de foto de Marte ao método MarsUiState.Success(). Exclua a variável result.
marsUiState = try {
    MarsUiState.Success(marsPhotoRepository.getMarsPhotos()[0])
}
  1. Execute o app e confirme se ele mostra uma única imagem de Marte.

cc071ad4c0d6bef5.png

  1. A foto de Marte não está preenchendo toda a tela. Para preencher o espaço disponível na tela, em AsyncImage, defina contentScale como ContentScale.FillBounds.
import androidx.compose.ui.layout.ContentScale

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
   AsyncImage(
       model = ImageRequest.Builder(context = LocalContext.current)
           .data(photo.imgSrc)
           .crossfade(true)
           .build(),
       contentDescription = stringResource(R.string.mars_photo),
       contentScale = ContentScale.FillBounds
   )
}
  1. Execute o app e confirme se a imagem preenche a tela horizontal e verticalmente.

424e7750bbf88252.png

Adicionar imagens de erro e carregamento

Para melhorar a experiência do usuário no seu app, mostre uma imagem de marcador ao carregar a imagem. Você também pode mostrar uma imagem de erro se o carregamento falhar devido a um problema, como um arquivo de imagem corrompido ou ausente. Nesta seção, você adiciona imagens de erro e de marcador usando AsyncImage.

  1. Abra res/drawable/ic_broken_image.xml e clique na guia Design ou Split à direita. Para a imagem de erro, use o ícone de imagem corrompida disponível na biblioteca de ícones integrada. Esse drawable vetorial usa o atributo android:tint para colorir o ícone em cinza.

70e008c63a2a1139.png

  1. Abra res/drawable/loading_animation.xml. Esse drawable é uma animação que gira um drawable de imagem, loading_img.xml, ao redor do ponto central. Essa animação não vai ser mostrada na visualização.

92a448fa23b6d1df.png

  1. Retorne ao arquivo HomeScreen.kt. No elemento de composição MarsPhotoCard, atualize a chamada para AsyncImage() e adicione os atributos error e placeholder, como mostrado no código abaixo:
import androidx.compose.ui.res.painterResource

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        // ...
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        // ...
    )
}

Esse código define a imagem de carregamento de marcador a ser usada durante o carregamento (o drawable loading_animation). Também define a imagem a ser usada se o carregamento falhar (o drawable ic_broken_image).

O elemento de composição MarsPhotoCard completo agora tem esta aparência:

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
    AsyncImage(
        model = ImageRequest.Builder(context = LocalContext.current)
            .data(photo.imgSrc)
            .crossfade(true)
            .build(),
        error = painterResource(R.drawable.ic_broken_image),
        placeholder = painterResource(R.drawable.loading_img),
        contentDescription = stringResource(R.string.mars_photo),
        contentScale = ContentScale.FillBounds
    )
}
  1. Execute o app. Dependendo da velocidade da sua conexão de rede, você poderá notar o carregamento brevemente, à medida que a Coil faz o download e mostra a imagem. Porém, o ícone de imagem corrompida ainda não vai ser mostrado, mesmo que você desative a rede. Isso será corrigido na última tarefa do codelab.

6dcecd205a0741a.gif

4. Mostrar uma grade de imagens com uma LazyVerticalGrid

Agora, seu app carrega uma foto de Marte recebida da Internet, o primeiro item da lista de MarsPhoto. Você usou o URL da imagem desses dados de fotos de Marte para preencher uma AsyncImage. No entanto, o objetivo é que o app mostre uma grade de imagens. Nesta tarefa, você usa uma LazyVerticalGrid com um gerenciador de layout de grade para mostrar uma grade de imagens.

Grades lentas

Os elementos de composição LazyVerticalGrid e LazyHorizontalGrid oferecem suporte para a exibição de itens em uma grade. Uma grade vertical lenta mostra os itens em um contêiner de rolagem vertical, dividido em várias colunas, enquanto uma grade horizontal lenta tem o mesmo comportamento no eixo horizontal.

27680e208333ed5.png

Do ponto de vista do design, o layout de grade é melhor para mostrar fotos de Marte como ícones ou imagens.

O parâmetro columns na LazyVerticalGrid e o parâmetro rows na LazyHorizontalGrid controlam como as células são formadas em colunas ou linhas. O exemplo abaixo mostra itens em uma grade, usando GridCells.Adaptive para definir cada coluna com pelo menos 128.dp de largura:

// Sample code - No need to copy over

@Composable
fun PhotoGrid(photos: List<Photo>) {
    LazyVerticalGrid(
        columns = GridCells.Adaptive(minSize = 150.dp)
    ) {
        items(photos) { photo ->
            PhotoItem(photo)
        }
    }
}

LazyVerticalGrid permite especificar uma largura para os itens. A grade se ajusta ao máximo de colunas possível. Depois de calcular o número de colunas, a grade distribui qualquer largura restante igualmente entre as colunas. Essa maneira adaptável de dimensionamento é útil principalmente para mostrar conjuntos de itens em diferentes tamanhos de tela.

Neste codelab, para mostrar fotos de Marte, você usa o elemento de composição LazyVerticalGrid com GridCells.Adaptive, com cada coluna definida como 150.dp de largura.

Chaves de itens

Quando o usuário rola a grade (uma LazyRow em uma LazyColumn), a posição do item da lista muda. No entanto, devido a uma mudança de orientação ou se os itens forem adicionados ou removidos, o usuário pode perder a posição de rolagem na linha. As chaves de itens mantêm a posição de rolagem com base na chave.

Ao fornecer chaves, você ajuda o Compose a processar as reordenações corretamente. Por exemplo, se o item tiver um estado memorizado, a definição das chaves vai permitir que o Compose mova esse estado junto ao item quando a posição mudar.

Adicionar uma LazyVerticalGrid

Adicione uma função de composição para mostrar uma lista de fotos de Marte em uma grade vertical.

  1. No arquivo HomeScreen.kt, crie uma nova função de composição com o nome PhotosGridScreen(), que usa uma lista de MarsPhoto e um modifier como argumentos.
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
}
  1. Dentro da função de composição PhotosGridScreen, adicione um LazyVerticalGrid com os parâmetros abaixo.
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.ui.unit.dp

@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
   LazyVerticalGrid(
       columns = GridCells.Adaptive(150.dp),
       modifier = modifier.fillMaxWidth(),
       contentPadding = PaddingValues(4.dp)
   ) {
     }
}
  1. Para adicionar uma lista de itens, dentro da lambda LazyVerticalGrid, chame a função items(), transmitindo a lista de MarsPhoto e uma chave de item como photo.id.
import androidx.compose.foundation.lazy.grid.items

@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
   LazyVerticalGrid(
       // ...
   ) {
       items(items = photos, key = { photo -> photo.id }) {
       }
   }
}
  1. Para adicionar o conteúdo mostrado por um único item da lista, defina a expressão lambda items. Chame MarsPhotoCard, transmitindo a photo.
items(items = photos, key = { photo -> photo.id }) {
   photo -> MarsPhotoCard(photo)
}
  1. Atualize o elemento de composição HomeScreen para mostrar a função PhotosGridScreen em vez do MarsPhotoCard para concluir a solicitação.
when (marsUiState) {
       // ...
       is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
       else -> // ...
}
  1. No arquivo MarsViewModel.kt, atualize a interface MarsUiState para aceitar uma lista de objetos MarsPhoto em vez de uma única MarsPhoto. A função de composição PhotosGridScreen aceita uma lista de objetos MarsPhoto.
sealed interface MarsUiState {
    data class Success(val photos: List<MarsPhoto>) : MarsUiState
    //...
}
  1. No arquivo MarsViewModel.kt, atualize a função getMarsPhotos() para transmitir uma lista de objetos de fotos de Marte ao método MarsUiState.Success().
marsUiState = try {
    MarsUiState.Success(marsPhotoRepository.getMarsPhotos())
}
  1. Execute o app.

8f3ad442203f5e6b.png

Não há padding ao redor de cada foto e a proporção é diferente para fotos diferentes. Você pode adicionar um elemento de composição Card para corrigir esses problemas.

Adicionar um card de composição

  1. No arquivo HomeScreen.kt, no elemento de composição MarsPhotoCard, adicione um Card ao redor da AsyncImage.
import androidx.compose.material.Card
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.padding

@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
   Card(
       modifier = modifier
           .padding(4.dp)
           .fillMaxWidth()
           .aspectRatio(1f),
       elevation = 8.dp,
   ) {
       AsyncImage(
           // ...
       )
   }
}
  1. Atualize a visualização da tela de resultados para visualizar PhotosGridScreen(). Simulação de dados com URLs de imagem vazios.
@Preview(showBackground = true)
@Composable
fun PhotosGridScreenPreview() {
   MarsPhotosTheme {
       val mockData = List(10) { MarsPhoto("$it", "") }
       PhotosGridScreen(mockData)
   }
}

Como os dados simulados têm URLs vazios, você vai notar o carregamento de imagens na visualização da grade de fotos.

2ff7b8c52e46c861.png

  1. Execute o app.

  1. Enquanto o app estiver em execução, ative o modo avião.
  2. Role as imagens no emulador. As imagens que ainda não foram carregadas aparecem como ícones de imagem corrompida. Este é o drawable de imagem que você transmitiu para a biblioteca de imagens Coil mostrar no caso de qualquer erro de rede ou imagem.

aa690ef699c7a6d5.png

Bom trabalho! Você simulou um erro de conexão de rede ativando o modo avião no emulador ou dispositivo.

5. Atualizar o teste do ViewModel

O MarsUiState e o MarsViewModel agora mostram uma lista de fotos em vez de uma única foto. No estado atual, o MarsViewModelTest espera que a classe de dados MarsUiState.Success contenha uma propriedade de string. Portanto, o teste não é compilado. É necessário atualizar o teste marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() para declarar que o MarsViewModel.marsUiState é igual ao estado Success que contém a lista de fotos.

  1. Abra o arquivo MarsViewModelTest.kt.
  2. No teste marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess(), modifique a chamada de função assertEquals() para comparar um estado Success (transmitindo a lista de fotos falsas ao parâmetro fotos) para o marsViewModel.marsUiState.
@Test
    fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
        runTest {
            val marsViewModel = MarsViewModel(
                marsPhotosRepository = FakeNetworkMarsPhotosRepository()
            )
            assertEquals(
                MarsUiState.Success(FakeDataSource.photosList),
                marsViewModel.marsUiState
            )
        }

O teste agora é compilado, executado e aprovado.

6. Acessar o código da solução

Para fazer o download do código do codelab concluído, use este comando git:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git

Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Confira o código da solução deste codelab no GitHub (link em inglês).

7. Conclusão

Parabéns por concluir este codelab e criar o app Mars Photos! É hora de mostrar seu app com fotos reais de Marte aos seus familiares e amigos.

Não se esqueça de compartilhar seu trabalho nas redes sociais com a hashtag #AndroidBasics.

8. Saiba mais

Documentação do desenvolvedor Android:

Outro:

  • Coil (link em inglês)