Cómo usar la interoperabilidad con objetos View en Compose

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:

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.

a02177f6b6277edc.png afc4551fde8c3113.png 5dab7f58a3649c04.png

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:

Descargar ZIP

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
  1. En Android Studio, abre la carpeta basic-android-kotlin-compose-training-juice-tracker.
  2. 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:

Lista de opciones de colores

Barra de calificación con 4 de 5 estrellas seleccionadas

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.

  1. En el directorio bottomsheet, crea un archivo nuevo llamado ColorSpinnerRow.kt.
  2. Crea una clase nueva dentro del archivo llamada SpinnerAdapter.
  3. En el constructor de SpinnerAdapter, define un parámetro de devolución de llamada llamado onColorChange que tome un parámetro Int. SpinnerAdapter controla las funciones de devolución de llamada de Spinner.

bottomsheet/ColorSpinnerRow.kt

class SpinnerAdapter(val onColorChange: (Int) -> Unit){
}
  1. 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 {
}
  1. Implementa las funciones miembro AdapterView.OnItemSelectedListener: onItemSelected() y onNothingSelected().

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")
    }
}
  1. Modifica la función onItemSelected() para llamar a la función de devolución de llamada onColorChange(), 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")
    }
}
  1. Modifica la función onNothingSelected() para establecer el color en 0, 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.

  1. Crea un nuevo elemento componible llamado ColorSpinnerRow dentro del archivo ColorSpinnerRow.kt, pero fuera de la clase SpinnerAdapter.
  2. En la firma del método de ColorSpinnerRow(), agrega un parámetro Int para la posición de la lista de opciones, una función de devolución de llamada que tome un parámetro Int y un modificador.

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
}
  1. 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) }

}
  1. 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 aparece Spinner.

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.

  1. Para usar un objeto Spinner en Compose, crea un elemento AndroidView() componible en el cuerpo de lambda InputRow. El elemento AndroidView() 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 en factory
  • Un elemento modifier componible

3bb9f605719b173.png

  1. Para implementar AndroidView, pasa un modificador y llena el ancho máximo de la pantalla.
  2. Pasa una expresión lambda para el parámetro factory.
  3. La lambda factory toma un elemento Context como parámetro. Crea una clase Spinner 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.

  1. Configura el adaptador con un ArrayAdapter. El ArrayAdapter requiere un contexto, un diseño XML y un array. Pasa simple_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.

  1. Agrega una devolución de llamada update que pase una spinner. Usa la devolución de llamada proporcionada en update para llamar al método setSelection().

bottomsheet/ColorSpinnerRow.kt

...
@Composable
fun ColorSpinnerRow(
    colorSpinnerPosition: Int,
    onColorChange: (Int) -> Unit,
    modifier: Modifier = Modifier
) {
   ...
   InputRow(...) {
      //...
         },
         update = { spinner ->
             spinner.setSelection(colorSpinnerPosition)
             spinner.onItemSelectedListener = SpinnerAdapter(onColorChange)
         }
      )
   }
}
  1. Usa el SpinnerAdapter que creaste antes para configurar una devolución de llamada de onItemSelectedListener() en update.

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.

  1. 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)
}
  1. Implementa el elemento ColorSpinnerRow en el elemento SheetForm componible que se encuentra en el archivo EntryBottomSheet.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

  1. Crea un archivo nuevo en el directorio bottomsheet llamado RatingInputRow.kt.
  2. En el archivo RatingInputRow.kt, crea un nuevo elemento componible llamado RatingInputRow().
  3. En la firma del método, pasa un Int para la calificación, una devolución de llamada con un parámetro Int para controlar un cambio de selección y un modificador.

bottomsheet/RatingInputRow.kt

@Composable
fun RatingInputRow(rating:Int, onRatingChange: (Int) -> Unit, modifier: Modifier = Modifier){
}
  1. Al igual que con ColorSpinnerRow, agrega un InputRow al elemento componible que contiene un AndroidView, 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 = {}
        )
    }
}
  1. En el cuerpo de lambda factory, crea una instancia de la clase RatingBar, que proporciona el tipo de barra de calificación necesaria para este diseño. Configura stepSize como 1f 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.

  1. Usa la calificación que se pasa al elemento componible para configurar la calificación de la instancia RatingBar en el cuerpo de lambda update.
  2. Cuando configures una nueva calificación, usa la devolución de llamada RatingBar para llamar a la función de devolución de llamada onRatingChange() 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.

  1. Usa el elemento RatingInputRow() componible en EntryBottomSheet. 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

  1. En el paquete homescreen, crea un nuevo archivo llamado AdBanner.kt.
  2. En el archivo AdBanner.kt, crea un nuevo elemento componible llamado AdBanner().

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.

  1. Intenta compilar el banner por tu cuenta con la clase AdView. Asegúrate de establecer el tamaño del anuncio en AdSize.BANNER y el ID del bloque de anuncios en "ca-app-pub-3940256099942544/6300978111".
  2. Cuando la AdView aumente, carga un anuncio con AdRequest 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())
        }
    )
}
  1. Coloca el elemento AdBanner antes de JuiceTrackerList en JuiceTrackerApp. Se declara JuiceTrackerList 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.

Descargar ZIP

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!