1. Antes de comenzar
Introducción
En este punto del curso, ya sabes compilar apps con Compose y también tienes conocimientos para hacerlo con XML, objetos View, vinculaciones de vistas y fragmentos. Una vez que hayas compilado apps con objetos View, apreciarás las ventajas de compilar apps con una IU declarativa, como Compose. Sin embargo, puede haber algunos casos en los que usar vistas sea más conveniente que usar Compose. En este codelab, aprenderás a usar la interoperabilidad con objetos View para agregar estos componentes a una app de Compose moderna.
Al momento de escribir este codelab, los componentes de la IU que crearás todavía no están disponibles en Compose, por lo que esta es la oportunidad perfecta para usar la interoperabilidad con objetos View.
Requisitos previos:
- Haber completado el curso Aspectos básicos de Android en Compose a través del codelab Cómo compilar una app para Android con vistas.
Qué necesitarás
- Una computadora con acceso a Internet y Android Studio instalado
- Un dispositivo o emulador
- El código de partida de la app de Juice Tracker
Qué compilarás
En este codelab, integrarás tres objetos View en la IU de Compose para completar la IU de la app de Juice Tracker, una lista, una RatingBar y un AdView. Para compilar estos componentes, usarás la interoperabilidad con objetos View. Gracias a la interoperabilidad con objetos View, puedes agregar estos elementos a tu app uniéndolos a un elemento componible.
Explicación del código
En este codelab, trabajarás con la misma app de JuiceTracker de los codelabs Cómo compilar una app para Android con objetos View y Cómo agregar Compose a una app basada en objetos View. La diferencia con esta versión es que el código de partida lo proporciona Compose en su totalidad. Actualmente, la app no tiene las entradas de color y calificación en la hoja de diálogo de entrada ni el banner de anuncio que aparece en la parte superior de la pantalla de la lista.
El directorio bottomsheet
contiene todos los componentes de la IU relacionados con el diálogo de entrada. Este paquete debe contener los componentes de IU para cuando se creen las entradas de color y calificación.
homescreen
contiene los componentes de IU que aloja la pantalla principal, incluida la lista de JuiceTracker. En algún punto, este paquete deberá contener el banner de anuncio, cuando se cree este último.
Los componentes principales de la IU, como la hoja inferior y la lista de extracción, se alojan en el archivo JuiceTrackerApp.kt
.
2. Obtén el código de inicio
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-juice-tracker.git $ cd basic-android-kotlin-compose-training-juice-tracker $ git checkout compose-starter
- En Android Studio, abre la carpeta
basic-android-kotlin-compose-training-juice-tracker
. - Abre el código de la app de Juice Tracker en Android Studio.
3. Configuración de Gradle
Agrega la dependencia de anuncios de Servicios de Play al archivo build.gradle.kts
de la app.
app/build.gradle.kts
android {
...
dependencies {
...
implementation("com.google.android.gms:play-services-ads:22.2.0")
}
}
4. Configuración
Para habilitar el banner del anuncio y realizar pruebas, agrega el siguiente valor al manifiesto de Android antes de la etiqueta activity
:
AndroidManifest.xml
...
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713" />
...
5. Completa el cuadro de diálogo de entrada
En esta sección, crearás la lista de opciones de colores y la barra de calificación para completar el cuadro de diálogo de entrada. La lista de opciones de colores es el componente que permite elegir un color, y la barra de calificación permite seleccionar una calificación del jugo. Observa el siguiente diseño:
Crea la lista de opciones de colores
Para implementar una lista de opciones en Compose, se debe usar la clase Spinner
. A diferencia de un elemento componible, Spinner
es un componente View, por lo que debe implementarse con una interoperabilidad.
- En el directorio
bottomsheet
, crea un archivo nuevo llamadoColorSpinnerRow.kt
. - Crea una clase nueva dentro del archivo llamada
SpinnerAdapter
. - En el constructor de
SpinnerAdapter
, define un parámetro de devolución de llamada llamadoonColorChange
que tome un parámetroInt
.SpinnerAdapter
controla las funciones de devolución de llamada deSpinner
.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
- Implementa la interfaz de
AdapterView.OnItemSelectedListener
.
Esta interfaz te permite definir el comportamiento de clic de la lista de opciones. Más adelante, configurarás este adaptador en un elemento componible.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
}
- Implementa las funciones miembro
AdapterView.OnItemSelectedListener
:onItemSelected()
yonNothingSelected()
.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
TODO("Not yet implemented")
}
override fun onNothingSelected(parent: AdapterView<*>?) {
TODO("Not yet implemented")
}
}
- Modifica la función
onItemSelected()
para llamar a la función de devolución de llamadaonColorChange()
, de modo que, cuando selecciones un color, la app actualice el valor seleccionado en la IU.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onColorChange(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
TODO("Not yet implemented")
}
}
- Modifica la función
onNothingSelected()
para establecer el color en0
, de modo que, si no seleccionas nada, el color predeterminado sea el primero, el rojo.
bottomsheet/ColorSpinnerRow.kt
class SpinnerAdapter(val onColorChange: (Int) -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onColorChange(position)
}
override fun onNothingSelected(parent: AdapterView<*>?) {
onColorChange(0)
}
}
El SpinnerAdapter
, que define el comportamiento de la lista de opciones con funciones de devolución de llamada, ya está compilado. Ahora, debes compilar el contenido de la lista de opciones y propagarlo con datos.
- Crea un nuevo elemento componible llamado
ColorSpinnerRow
dentro del archivoColorSpinnerRow.kt
, pero fuera de la claseSpinnerAdapter
. - En la firma del método de
ColorSpinnerRow()
, agrega un parámetroInt
para la posición de la lista de opciones, una función de devolución de llamada que tome un parámetroInt
y un modificador.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
}
- Dentro de la función, crea un array de recursos de cadenas para la selección del color de jugo con la enumeración
JuiceColor
. Este array funciona como el contenido del ícono giratorio que se propagará.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val juiceColorArray =
JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
}
- Agrega un elemento
InputRow()
componible y pasa el recurso de cadenas para la selección del color de la etiqueta de entrada y un modificador, que define la fila de entrada donde apareceSpinner
.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val juiceColorArray =
JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
}
}
A continuación, crearás el Spinner
. Como Spinner
es una clase de vista, se debe usar la API de interoperabilidad con objetos View de Compose para unirla en un elemento componible. Esto se logra con el elemento componible AndroidView
.
- Para usar un objeto
Spinner
en Compose, crea un elementoAndroidView()
componible en el cuerpo de lambdaInputRow
. El elementoAndroidView()
componible crea un elemento o jerarquía de View en un elemento componible.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
val juiceColorArray =
JuiceColor.values().map { juiceColor -> stringResource(juiceColor.label) }
InputRow(inputLabel = stringResource(R.string.color), modifier = modifier) {
AndroidView()
}
}
El elemento AndroidView
componible toma tres parámetros:
- La lambda
factory
, que es una función que crea la vista - La devolución de llamada
update
, a la que se llama cuando se aumenta la vista creada enfactory
- Un elemento
modifier
componible
- Para implementar
AndroidView
, pasa un modificador y llena el ancho máximo de la pantalla. - Pasa una expresión lambda para el parámetro
factory
. - La lambda
factory
toma un elementoContext
como parámetro. Crea una claseSpinner
y pasa el contexto.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
...
InputRow(...) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
Spinner(context)
}
)
}
}
Al igual que un RecyclerView.Adapter
proporciona datos a un elemento RecyclerView
, un ArrayAdapter
proporciona datos a un elemento Spinner
. El elemento Spinner
requiere un adaptador para contener el array de colores.
- Configura el adaptador con un
ArrayAdapter
. ElArrayAdapter
requiere un contexto, un diseño XML y un array. Pasasimple_spinner_dropdown_item
para el diseño, que se proporciona de forma predeterminada con Android.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
...
InputRow(...) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
Spinner(context).apply {
adapter =
ArrayAdapter(
context,
android.R.layout.simple_spinner_dropdown_item,
juiceColorArray
)
}
}
)
}
}
La devolución de llamada factory
muestra una instancia de la vista creada en ella. update
es una devolución de llamada que toma un parámetro del mismo tipo que el que muestra en factory
. Este parámetro es una instancia del objeto View aumentada por factory
. En este caso, como se creó un Spinner
en factory, se puede acceder a la instancia de ese Spinner
en el cuerpo de lambda update
.
- Agrega una devolución de llamada
update
que pase unaspinner
. Usa la devolución de llamada proporcionada enupdate
para llamar al métodosetSelection()
.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
...
InputRow(...) {
//...
},
update = { spinner ->
spinner.setSelection(colorSpinnerPosition)
spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
}
)
}
}
- Usa el
SpinnerAdapter
que creaste antes para configurar una devolución de llamada deonItemSelectedListener()
enupdate
.
bottomsheet/ColorSpinnerRow.kt
...
@Composable
fun ColorSpinnerRow(
colorSpinnerPosition: Int,
onColorChange: (Int) -> Unit,
modifier: Modifier = Modifier
) {
...
InputRow(...) {
AndroidView(
// ...
},
update = { spinner ->
spinner.setSelection(colorSpinnerPosition)
spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
}
)
}
}
El código del componente de la lista de opciones de colores ya está completo.
- Agrega la siguiente función de utilidad para obtener el índice enum de
JuiceColor
. Usarás esto en el siguiente paso.
private fun findColorIndex(color: String): Int {
val juiceColor = JuiceColor.valueOf(color)
return JuiceColor.values().indexOf(juiceColor)
}
- Implementa el elemento
ColorSpinnerRow
en el elementoSheetForm
componible que se encuentra en el archivoEntryBottomSheet.kt
. Coloca la lista de opciones de color después del texto "Description" y antes de los botones.
bottomsheet/EntryBottomSheet.kt
...
@Composable
fun SheetForm(
juice: Juice,
onUpdateJuice: (Juice) -> Unit,
onCancel: () -> Unit,
onSubmit: () -> Unit,
modifier: Modifier = Modifier,
) {
...
TextInputRow(
inputLabel = stringResource(R.string.juice_description),
fieldValue = juice.description,
onValueChange = { description -> onUpdateJuice(juice.copy(description = description)) },
modifier = Modifier.fillMaxWidth()
)
ColorSpinnerRow(
colorSpinnerPosition = findColorIndex(juice.color),
onColorChange = { color ->
onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
}
)
ButtonRow(
modifier = Modifier
.align(Alignment.End)
.padding(bottom = dimensionResource(R.dimen.padding_medium)),
onCancel = onCancel,
onSubmit = onSubmit,
submitButtonEnabled = juice.name.isNotEmpty()
)
}
}
Crea la entrada de calificación
- Crea un archivo nuevo en el directorio
bottomsheet
llamadoRatingInputRow.kt
. - En el archivo
RatingInputRow.kt
, crea un nuevo elemento componible llamadoRatingInputRow()
. - En la firma del método, pasa un
Int
para la calificación, una devolución de llamada con un parámetroInt
para controlar un cambio de selección y un modificador.
bottomsheet/RatingInputRow.kt
@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
- Al igual que con
ColorSpinnerRow
, agrega unInputRow
al elemento componible que contiene unAndroidView
, como se muestra en el siguiente código de ejemplo.
bottomsheet/RatingInputRow.kt
@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
AndroidView(
factory = {},
update = {}
)
}
}
- En el cuerpo de lambda
factory
, crea una instancia de la claseRatingBar
, que proporciona el tipo de barra de calificación necesaria para este diseño. ConfigurastepSize
como1f
para que la calificación sea siempre un número entero.
bottomsheet/RatingInputRow.kt
@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
AndroidView(
factory = { context ->
RatingBar(context).apply {
stepSize = 1f
}
},
update = {}
)
}
}
Cuando se aumenta el objeto View, se establece la calificación. Recuerda que factory
regresa la instancia de RatingBar
a la devolución de llamada de actualización.
- Usa la calificación que se pasa al elemento componible para configurar la calificación de la instancia
RatingBar
en el cuerpo de lambdaupdate
. - Cuando configures una nueva calificación, usa la devolución de llamada
RatingBar
para llamar a la función de devolución de llamadaonRatingChange()
y actualizar la calificación en la IU.
bottomsheet/RatingInputRow.kt
@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
InputRow(inputLabel = stringResource(R.string.rating), modifier = modifier) {
AndroidView(
factory = { context ->
RatingBar(context).apply {
stepSize = 1f
}
},
update = { ratingBar ->
ratingBar.rating = rating.toFloat()
ratingBar.setOnRatingBarChangeListener { _, _, _ ->
onRatingChange(ratingBar.rating.toInt())
}
}
)
}
}
Ya completaste el elemento componible de entrada de calificación.
- Usa el elemento
RatingInputRow()
componible enEntryBottomSheet
. Colócalo debajo de la lista de opciones de colores y sobre los botones.
bottomsheet/EntryBottomSheet.kt
@Composable
fun SheetForm(
juice: Juice,
onUpdateJuice: (Juice) -> Unit,
onCancel: () -> Unit,
onSubmit: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
...
ColorSpinnerRow(
colorSpinnerPosition = findColorIndex(juice.color),
onColorChange = { color ->
onUpdateJuice(juice.copy(color = JuiceColor.values()[color].name))
}
)
RatingInputRow(
rating = juice.rating,
onRatingChange = { rating -> onUpdateJuice(juice.copy(rating = rating)) }
)
ButtonRow(
modifier = Modifier.align(Alignment.CenterHorizontally),
onCancel = onCancel,
onSubmit = onSubmit,
submitButtonEnabled = juice.name.isNotEmpty()
)
}
}
Crea el banner de anuncio
- En el paquete
homescreen
, crea un nuevo archivo llamadoAdBanner.kt
. - En el archivo
AdBanner.kt
, crea un nuevo elemento componible llamadoAdBanner()
.
A diferencia de los elementos componibles que creaste antes, el AdBanner
no requiere una entrada. Por lo tanto, no es necesario unirlo a un elemento componible InputRow
. Sin embargo, sí requiere una AndroidView
.
- Intenta compilar el banner por tu cuenta con la clase
AdView
. Asegúrate de establecer el tamaño del anuncio enAdSize.BANNER
y el ID del bloque de anuncios en"ca-app-pub-3940256099942544/6300978111"
. - Cuando la
AdView
aumente, carga un anuncio conAdRequest Builder
.
homescreen/AdBanner.kt
@Composable
fun AdBanner(modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier,
factory = { context ->
AdView(context).apply {
setAdSize(AdSize.BANNER)
// Use test ad unit ID
adUnitId = "ca-app-pub-3940256099942544/6300978111"
}
},
update = { adView ->
adView.loadAd(AdRequest.Builder().build())
}
)
}
- Coloca el elemento
AdBanner
antes deJuiceTrackerList
enJuiceTrackerApp
. Se declaraJuiceTrackerList
en la línea 83.
ui/JuiceTrackerApp.kt
...
AdBanner(
Modifier
.fillMaxWidth()
.padding(
top = dimensionResource(R.dimen.padding_medium),
bottom = dimensionResource(R.dimen.padding_small)
)
)
JuiceTrackerList(
juices = trackerState,
onDelete = { juice -> juiceTrackerViewModel.deleteJuice(juice) },
onUpdate = { juice ->
juiceTrackerViewModel.updateCurrentJuice(juice)
scope.launch {
bottomSheetScaffoldState.bottomSheetState.expand()
}
},
)
6. Obtén el código de la solución
Para descargar el código del codelab terminado, puedes usar estos comandos de git:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-juice-tracker.git $ cd basic-android-kotlin-compose-training-juice-tracker $ git checkout compose-with-views
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, puedes hacerlo en GitHub.
7. Más información
8. Eso es todo
Si bien este curso termina aquí, este es solo el comienzo de tu recorrido hacia el desarrollo de apps para Android.
En este curso, aprendiste a compilar apps con Jetpack Compose, el moderno kit de herramientas de IU que permite compilar apps nativas de Android. Aquí, compilaste apps con listas, una o varias pantallas, y navegaste entre ellas. Aprendiste a crear apps interactivas, hiciste que tu app responda a entradas de usuario y actualizaste la IU. Aplicaste Material Design y usaste colores, formas y tipografía para diseñar tu app. También usaste Jetpack y otras bibliotecas de terceros para programar tareas, recuperar datos de servidores remotos, conservar datos localmente y mucho más.
Al final de este curso, no solo comprendes cómo crear apps atractivas y responsivas con Jetpack Compose, sino que también cuentas con las habilidades y los conocimientos necesarios para crear apps de Android eficientes, de fácil mantención y con atractivo visual. Esta base de conocimientos te ayudará a continuar aprendiendo y desarrollando tus habilidades para trabajar con Compose y desarrollar apps para Android modernas.
Queremos dar las gracias a todas las personas que participaron de este curso y lo completaron. Te recomendamos que sigas aprendiendo y ampliando tus habilidades con otros recursos, como la Documentación para desarrolladores de Android, curso de Jetpack Compose para desarrolladores de Android, Arquitectura moderna de apps para Android, Blog para desarrolladores de Android, otros codelabs y proyectos de muestra.
Por último, no olvides compartir tus creaciones en redes sociales con el hashtag #AndroidBasics para que nosotros y el resto de la comunidad de desarrolladores de Android también podamos seguir tu recorrido de aprendizaje.
¡Feliz compilación!