1. Antes de comenzar
Introducción
En codelabs anteriores, aprendiste a obtener datos de un servicio web usando un patrón de repositorio y a analizar la respuesta en un objeto Kotlin. En este codelab, aprenderás a cargar y mostrar fotos desde una URL web. También puedes revisar cómo compilar un objeto LazyVerticalGrid
y usarlo para mostrar una cuadrícula de imágenes en la página de descripción general.
Requisitos previos
- Conocimientos sobre cómo recuperar JSON de un servicio web de REST y analizar esos datos en objetos Kotlin mediante las bibliotecas Retrofit y Gson
- Conocimientos de un servicio web REST
- Familiarizarse con los componentes de la arquitectura de Android, como las capas de datos y los repositorios
- Conocimientos sobre la inserción de dependencias
- Conocimientos de
ViewModel
yViewModelProvider.Factory
- Conocimientos sobre la implementación de corrutinas para tu app
- Conocimientos sobre el patrón de repositorio
Qué aprenderás
- Cómo usar la biblioteca Coil para cargar y mostrar una imagen desde una URL web
- Cómo usar una
LazyVerticalGrid
para mostrar una cuadrícula de imágenes - Cómo manejar los posibles errores mientras se descargan y se muestran las imágenes
Qué compilarás
- Modificarás la app de Mars Photos para obtener la URL de la imagen de los datos de Marte y usarás Coil para cargar y mostrar esa imagen.
- Agregarás una animación de carga y un ícono de error a la app.
- Agregarás manejo de estado y errores a la app.
Qué necesitarás
- Una computadora con un navegador web moderno, como la versión más reciente de Chrome
- El código de inicio para la app de Mars Photos con servicios web de REST
2. Descripción general de la app
En este codelab, seguirás trabajando con la app de Mars Photos de un codelab anterior. La app de Mars Photos se conecta a un servicio web para recuperar y mostrar la cantidad de objetos de Kotlin recuperados con Gson. Estos objetos de Kotlin contienen las URLs de fotos reales de la superficie de Marte capturadas por los rovers de la NASA.
La versión de la app que compiles en este codelab mostrará fotos de Marte en una cuadrícula de imágenes. Las imágenes son parte de los datos que tu app recupera del servicio web. Tu app usará la biblioteca de Coil para cargar y mostrar las imágenes, y un LazyVerticalGrid
para crear el diseño de cuadrícula para las imágenes. Tu app también manejará los errores de red correctamente mostrando un mensaje de error.
Obtén el código de partida
Para comenzar, descarga el código de partida:
Como alternativa, puedes clonar el repositorio de GitHub para el código:
$ 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
Puedes explorar el código en el repositorio de GitHub de Mars Photos
.
3. Muestra una imagen descargada
Mostrar una foto de una URL web puede parecer sencillo, pero se necesita un poco de ingeniería para que funcione bien. La imagen se debe descargar, almacenar de forma interna (en caché) y decodificar de su formato comprimido a una imagen que Android pueda usar. Puedes almacenar la imagen en la caché de la memoria, en una caché basada en almacenamiento, o bien en ambas. Todo esto tiene que ocurrir en subprocesos en segundo plano de baja prioridad para que la IU siga siendo receptiva. Además, para obtener el mejor rendimiento de red y CPU, te recomendamos que recuperes y decodifiques más de una imagen a la vez.
Afortunadamente, puedes usar una biblioteca creada por la comunidad llamada Coil para descargar, almacenar en búfer, decodificar y almacenar en caché tus imágenes. Sin usar Coil, tendrías mucho más trabajo.
Básicamente, Coil necesita dos cosas:
- La URL de la imagen que quieres cargar y mostrar
- Un elemento de componibilidad
AsyncImage
para mostrar esa imagen
En esta tarea, aprenderás a utilizar Coil para mostrar una sola imagen del servicio web de Marte. Debes mostrar la imagen de la primera foto de Marte en la lista de fotos que muestra el servicio web. En las siguientes imágenes, se muestran las capturas de pantalla anterior y posterior:
Cómo agregar una dependencia de Coil
- Abre la app de Mars Photos solution del codelab Cómo agregar un repositorio y una DI manual.
- Ejecuta la app para confirmar que muestra el recuento de fotos de Marte recuperadas.
- Abre build.gradle.kts (Módulo :app).
- En la sección
dependencies
, agrega esta línea para la biblioteca de Coil:
// Coil
implementation("io.coil-kt:coil-compose:2.4.0")
Consulta la versión más reciente de la biblioteca y actualízala desde la página de documentación de Coil.
- Haz clic en Sync Now para volver a compilar el proyecto con la dependencia nueva.
Cómo mostrar la URL de la imagen
En este paso, recuperas y muestras la URL de la primera foto de Marte.
- En
ui/screens/MarsViewModel.kt
, dentro del métodogetMarsPhotos()
, dentro del bloquetry
, busca la línea que establece los datos recuperados del servicio web comolistResult
.
// No need to copy, code is already present
try {
val listResult = marsPhotosRepository.getMarsPhotos()
//...
}
- Para actualizar esta línea, cambia
listResult
aresult
y asigna la primera foto de Marte recuperada a la variable nuevaresult
. Asigna la primera foto en el índice0
.
try {
val result = marsPhotosRepository.getMarsPhotos()[0]
//...
}
- En la siguiente línea, actualiza el parámetro que se pasó a la llamada a función
MarsUiState.Success()
a la string en el siguiente código. Usa los datos de la propiedad nueva en lugar delistResult
. Muestra la primera URL de imagen de la fotoresult
.
try {
...
MarsUiState.Success("First Mars image URL: ${result.imgSrc}")
}
El bloque try
completo ahora tiene el siguiente aspecto:
marsUiState = try {
val result = marsPhotosRepository.getMarsPhotos()[0]
MarsUiState.Success(
" First Mars image URL : ${result.imgSrc}"
)
}
- Ejecuta la app. Ahora el elemento de componibilidad
Text
muestra la URL de la primera foto de Marte. En la siguiente sección, se describe cómo hacer que la app muestre la imagen en esta URL.
Agrega el elemento AsyncImage
componible
En este paso, agregarás una función de componibilidad AsyncImage
para cargar y mostrar una sola foto de Marte. AsyncImage
es un elemento de componibilidad que ejecuta una solicitud de imagen de forma asíncrona y procesa el resultado.
// Example code, no need to copy over
AsyncImage(
model = "https://android.com/sample_image.jpg",
contentDescription = null
)
El argumento model
puede ser el valor ImageRequest.data
o el propio ImageRequest
. En el ejemplo anterior, debes asignar el valor ImageRequest.data
, es decir, la URL de la imagen, que es "https://android.com/sample_image.jpg"
. En el siguiente código de ejemplo, se muestra cómo asignar la ImageRequest
al 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
admite los mismos argumentos que el elemento componible Image estándar. Además, admite la configuración de pintores placeholder
/error
/fallback
y devoluciones de llamada onLoading
/onSuccess
/onError
. El código de ejemplo anterior carga la imagen con un recorte circular y un encadenado, y establece un marcador de posición.
contentDescription
establece el texto que usan los servicios de accesibilidad para describir lo que representa esta imagen.
Agrega un elemento de componibilidad AsyncImage
a tu código para mostrar la primera foto de Marte recuperada.
- En
ui/screens/HomeScreen.kt
, agrega una nueva función que de componibilidad llamadaMarsPhotoCard()
, que tomaMarsPhoto
yModifier
.
@Composable
fun MarsPhotoCard(photo: MarsPhoto, modifier: Modifier = Modifier) {
}
- Dentro de la función de componibilidad
MarsPhotoCard()
, agrega la funciónAsyncImage()
de la siguiente manera:
import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.platform.LocalContext
@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),
modifier = Modifier.fillMaxWidth()
)
}
En el código anterior, compilaste un ImageRequest
con la URL de la imagen (photo.imgSrc
) y lo pasaste al argumento model
. Ahora usarás contentDescription
a fin de configurar el texto para los lectores de accesibilidad.
- Agrega
crossfade(true)
aImageRequest
para habilitar una animación de encadenado cuando la solicitud se complete correctamente.
@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),
modifier = Modifier.fillMaxWidth()
)
}
- Actualiza el elemento de componibilidad
HomeScreen
para mostrar el elemento de componibilidadMarsPhotoCard
, en lugar deResultScreen
, cuando la solicitud se complete correctamente. Corregirás el error de discrepancia de tipos en el paso siguiente.
@Composable
fun HomeScreen(
marsUiState: MarsUiState,
modifier: Modifier = Modifier
) {
when (marsUiState) {
is MarsUiState.Loading -> LoadingScreen(modifier = modifier.fillMaxSize())
is MarsUiState.Success -> MarsPhotoCard(photo = marsUiState.photos, modifier = modifier.fillMaxSize())
else -> ErrorScreen(modifier = modifier.fillMaxSize())
}
}
- En el archivo
MarsViewModel.kt
, actualiza la interfazMarsUiState
para que acepte un objetoMarsPhoto
en lugar de unString
.
sealed interface MarsUiState {
data class Success(val photos: MarsPhoto) : MarsUiState
//...
}
- Actualiza la función
getMarsPhotos()
para pasar el primer objeto de foto de Marte aMarsUiState.Success()
. Borra la variableresult
.
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos()[0])
}
- Ejecuta la app y confirma que muestre una sola imagen de Marte.
- La foto de Marte no ocupa toda la pantalla. Para llenar el espacio disponible en pantalla, en
HomeScreen.kt
dentro deAsyncImage
, establececontentScale
enContentScale.Crop
.
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.Crop,
modifier = modifier,
)
}
- Ejecuta la app y confirma que la imagen ocupe toda la pantalla tanto en horizontal como en vertical.
Agrega imágenes de carga y error
Puedes mejorar la experiencia del usuario en tu app mostrando una imagen de marcador de posición mientras se carga la imagen. También puedes mostrar una imagen de error si la carga falla debido a un problema, como un archivo de imagen faltante o dañado. En esta sección, agregarás imágenes de error y marcador de posición con AsyncImage
.
- Abre
res/drawable/ic_broken_image.xml
y haz clic en la pestaña Design o Split a la derecha. Para la imagen de error, usa el ícono de imagen rota que está disponible en la biblioteca de íconos integrada. Este elemento de diseño vectorial usa el atributoandroid:tint
para colorear el ícono gris.
- Abre
res/drawable/loading_img.xml
. Este elemento de diseño es una animación que rota un elemento de diseño de imagen,loading_img.xml
, alrededor del punto central (no ves la animación en la vista previa).
- Regresa al archivo
HomeScreen.kt
. En el elemento de componibilidadMarsPhotoCard
, actualiza la llamada aAsyncImage()
para agregar los atributoserror
yplaceholder
, como se muestra en el siguiente código:
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),
// ...
)
}
Este código configura el marcador de posición que se usará para cargar la imagen durante la carga (elemento de diseño loading_img
). También configura la imagen que se usará si falla la carga (elemento de diseño ic_broken_image
).
El elemento de componibilidad MarsPhotoCard
completo ahora luce como el siguiente código:
@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.Crop
)
}
- Ejecuta la app. Según la velocidad de la conexión de red, es posible que veas brevemente la imagen de carga mientras Coil descarga y muestra la imagen de la propiedad. Sin embargo, aún no verás el ícono de la imagen rota, incluso si desactivas la red; lo solucionarás en la última tarea del codelab.
4. Muestra una cuadrícula de imágenes con RecyclerView
Ahora tu app carga una foto de Marte de Internet, el primer elemento de la lista de MarsPhoto
. Usaste la URL de la imagen de esos datos de fotos de Marte para propagar un AsyncImage
. Sin embargo, el objetivo es que tu app muestre una cuadrícula de imágenes. En esta tarea, usarás un LazyVerticalGrid
con un administrador de diseño de cuadrícula para mostrar una cuadrícula de imágenes.
Cuadrículas diferidas
Los elementos de componibilidad LazyVerticalGrid y LazyHorizontalGrid admiten la visualización de elementos en una cuadrícula. Una cuadrícula vertical diferida muestra los elementos en un contenedor desplazable vertical, que abarca varias columnas, mientras que una cuadrícula horizontal diferida tiene el mismo comportamiento en el eje horizontal.
Desde una perspectiva de diseño, Grid Layout es la mejor opción para mostrar fotos de Marte como íconos o imágenes.
El parámetro columns
en LazyVerticalGrid
y el parámetro rows
en LazyHorizontalGrid
controlan el modo en que se forman las celdas en columnas o filas. En el siguiente ejemplo, se muestran elementos de una cuadrícula con GridCells.Adaptive
para que cada columna tenga al menos 128.dp
ancho:
// 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
te permite especificar un ancho para los elementos, y la cuadrícula se ajusta a la mayor cantidad de columnas posible. Después de calcular la cantidad de columnas, la cuadrícula distribuye el ancho restante de manera equitativa entre ellas. Esta forma de tamaño adaptable es especialmente útil para mostrar conjuntos de elementos en diferentes tamaños de pantalla.
En este codelab, para mostrar fotos de Marte, usarás el elemento LazyVerticalGrid
componible con GridCells.Adaptive
, y cada columna configurada con un ancho de 150.dp
.
Claves de elementos
Cuando el usuario se desplaza por la cuadrícula (una LazyRow
dentro de una LazyColumn
), cambia la posición del elemento de la lista. Sin embargo, en caso de cambio de orientación o si se agregan o quitan elementos, el usuario puede perder la posición de desplazamiento dentro de la fila. Las claves de elemento te ayudan a mantener la posición de desplazamiento en función de lo que indiquen.
Si proporcionas claves, ayudarás a Compose a controlar correctamente el reordenamiento. Por ejemplo, si tu elemento contiene un estado recordado, la configuración de claves permite a Compose mover este estado junto con el elemento cuando cambia su posición.
Agrega LazyVerticalGrid
Agrega un elemento componible para mostrar una lista de fotos de Marte en una cuadrícula vertical.
- En el archivo
HomeScreen.kt
, crea una nueva función de componibilidad llamadaPhotosGridScreen()
, que tome una lista deMarsPhoto
y unamodifier
como argumentos.
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
}
- Dentro del elemento de componibilidad
PhotosGridScreen
, agrega unLazyVerticalGrid
con los siguientes parámetros.
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,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(150.dp),
modifier = modifier.padding(horizontal = 4.dp),
contentPadding = contentPadding,
) {
}
}
- Para agregar una lista de elementos, dentro de la lambda
LazyVerticalGrid
, llama a la funciónitems()
pasando la lista deMarsPhoto
y una clave de elemento comophoto.id
.
import androidx.compose.foundation.lazy.grid.items
@Composable
fun PhotosGridScreen(
photos: List<MarsPhoto>,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
LazyVerticalGrid(
// ...
) {
items(items = photos, key = { photo -> photo.id }) {
}
}
}
- Para agregar el contenido que muestra un solo elemento de la lista, define la expresión lambda
items
. Llama aMarsPhotoCard
y pasa laphoto
.
items(items = photos, key = { photo -> photo.id }) {
photo -> MarsPhotoCard(photo)
}
- Actualiza el elemento de componibilidad
HomeScreen
para mostrar el elemento de componibilidadPhotosGridScreen
, en lugar delMarsPhotoCard
de componibilidad, al completar la solicitud de forma correcta.
when (marsUiState) {
// ...
is MarsUiState.Success -> PhotosGridScreen(marsUiState.photos, modifier)
// ...
}
- En el archivo
MarsViewModel.kt
, actualiza la interfazMarsUiState
para que acepte una lista de objetosMarsPhoto
, en lugar de una solaMarsPhoto
. El elemento de componibilidadPhotosGridScreen
acepta una lista de objetosMarsPhoto
.
sealed interface MarsUiState {
data class Success(val photos: List<MarsPhoto>) : MarsUiState
//...
}
- En el archivo
MarsViewModel.kt
, actualiza la funcióngetMarsPhotos()
para pasar una lista de objetos de fotos de Marte aMarsUiState.Success()
.
marsUiState = try {
MarsUiState.Success(marsPhotosRepository.getMarsPhotos())
}
- Ejecuta la app.
Ten en cuenta que no hay padding alrededor de cada foto, y la relación de aspecto es diferente para cada una de ellas. Puedes agregar un elemento de componibilidad Card
para solucionar estos problemas.
Agrega un elemento componible de tarjeta
- En el archivo
HomeScreen.kt
, en el elementoMarsPhotoCard
componible, agrega un elementoCard
con la elevación8.dp
alrededor de laAsyncImage
. Asigna el argumentomodifier
al elementoCard
componible.
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,
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
) {
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.Crop,
modifier = Modifier.fillMaxWidth()
)
}
}
- Para corregir la relación de aspecto, en
PhotosGridScreen()
, actualiza el modificador deMarsPhotoCard()
.
@Composable
fun PhotosGridScreen(photos: List<MarsPhoto>, modifier: Modifier = Modifier) {
LazyVerticalGrid(
//...
) {
items(items = photos, key = { photo -> photo.id }) { photo ->
MarsPhotoCard(
photo,
modifier = modifier
.padding(4.dp)
.fillMaxWidth()
.aspectRatio(1.5f)
)
}
}
}
- Actualiza la vista previa de la pantalla de resultados para obtener una vista previa de
PhotosGridScreen()
. Simula datos con URLs de imagen vacías.
@Preview(showBackground = true) @Composable fun PhotosGridScreenPreview() { MarsPhotosTheme { val mockData = List(10) { MarsPhoto("$it", "") } PhotosGridScreen(mockData) } }
Dado que los datos ficticios tienen URLs vacías, verás que se cargan imágenes en la vista previa de la cuadrícula de fotos.
- Ejecuta la app.
- Mientras se ejecuta la app, activa el modo de avión.
- Desplázate por las imágenes en el emulador. Las imágenes que aún no se cargaron se muestran como íconos de imágenes rotas. Este es el elemento de diseño de imagen que pasaste a la biblioteca de imágenes de Coil para mostrar en caso de que se produzca un error de red o no se pueda obtener la imagen.
¡Muy bien! Simulaste el error de conexión de red activando el modo de avión en el emulador o dispositivo.
5. Agrega la acción de volver a intentar
En esta sección, agregarás un botón para volver a intentar la acción y recuperarás las fotos cuando se haga clic en él.
- Agrega un botón a la pantalla de error. En el archivo
HomeScreen.kt
, actualiza el elemento componibleErrorScreen()
de modo que incluya un parámetro lambdaretryAction
y un botón.
@Composable
fun ErrorScreen(retryAction: () -> Unit, modifier: Modifier = Modifier) {
Column(
// ...
) {
Image(
// ...
)
Text(//...)
Button(onClick = retryAction) {
Text(stringResource(R.string.retry))
}
}
}
Cómo revisar la vista previa
- Actualiza el elemento
HomeScreen()
componible para pasar la lambda de reintento.
@Composable
fun HomeScreen(
marsUiState: MarsUiState, retryAction: () -> Unit, modifier: Modifier = Modifier
) {
when (marsUiState) {
//...
is MarsUiState.Error -> ErrorScreen(retryAction, modifier = modifier.fillMaxSize())
}
}
- En el archivo
ui/theme/MarsPhotosApp.kt
, actualiza la llamada a funciónHomeScreen()
para establecer el parámetro lambdaretryAction
enmarsViewModel::getMarsPhotos
. Esto recuperará las fotos de Marte del servidor.
HomeScreen(
marsUiState = marsViewModel.marsUiState,
retryAction = marsViewModel::getMarsPhotos
)
6. Actualiza la prueba de ViewModel
Ahora, MarsUiState
y MarsViewModel
admiten una lista de fotos, en lugar de una sola. En su estado actual, MarsViewModelTest
espera que la clase de datos MarsUiState.Success
contenga una propiedad de cadena. Por lo tanto, la prueba no se compila. Debes actualizar la prueba marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
para confirmar que MarsViewModel.marsUiState
es igual al estado Success
que contiene la lista de fotos.
- Abre el archivo
rules/MarsViewModelTest.kt
. - En la prueba
marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess()
, modifica la llamada a funciónassertEquals()
para comparar un estadoSuccess
(pasando la lista de fotos falsas al parámetro de fotos) con elmarsViewModel.marsUiState
.
@Test
fun marsViewModel_getMarsPhotos_verifyMarsUiStateSuccess() =
runTest {
val marsViewModel = MarsViewModel(
marsPhotosRepository = FakeNetworkMarsPhotosRepository()
)
assertEquals(
MarsUiState.Success(FakeDataSource.photosList),
marsViewModel.marsUiState
)
}
La prueba ahora se compila, se ejecuta y se aprueba.
7. Obtén el código de la solución
Para descargar el código del codelab terminado, puedes usar este comando de git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-mars-photos.git
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de la solución para este codelab, míralo en GitHub.
8. Conclusión
Felicitaciones por completar este codelab y compilar la app de Mars Photos. Es hora de que alardees tu app con fotos reales de Marte con tu familia y amistades.
No olvides compartir tu trabajo en redes sociales con el hashtag #AndroidBasics.
9. Más información
Documentación para desarrolladores de Android:
- Listas y cuadrículas | Jetpack Compose | Android Developers
- Cuadrículas diferidas | Jetpack Compose | Android Developers
- Descripción general de ViewModel
Otra opción: