Cómo migrar a Jetpack Compose

1. Introducción

Compose y el sistema de View pueden funcionar en conjunto.

En este codelab, migrarás partes de la pantalla de detalles de plantas de Sunflower a Compose. Creamos una copia del proyecto para que pruebes migrar una app realista a Compose.

Al final del codelab, podrás continuar con la migración y convertir el resto de las pantallas de Sunflower si lo deseas.

Para obtener más asistencia mientras realizas este codelab, consulta el siguiente código:

Qué aprenderás

En este codelab, aprenderás lo siguiente:

  • Cuáles son las diferentes rutas de migración que puedes seguir
  • Cómo migrar una app de forma incremental a Compose
  • Cómo agregar Compose a una pantalla existente compilada con Views
  • Cómo usar un objeto View desde Compose
  • Cómo crear un tema en Compose
  • Cómo probar una pantalla mixta escrita en Views y Compose

Requisitos previos

Qué necesitarás

2. Estrategia de migración

Jetpack Compose se diseñó con la interoperabilidad de View desde el principio. Para migrar a Compose, recomendamos una migración incremental en la que Compose y View coexistan en la base de código hasta que la app esté completamente en Compose.

Esta es la estrategia de migración recomendada:

  1. Compilar pantallas nuevas con Compose
  2. Identificar elementos reutilizables y comenzar a crear una biblioteca de componentes comunes de la IU a medida que compiles funciones
  3. Reemplazar las funciones existentes de a una pantalla por vez

Compilar pantallas nuevas con Compose

El uso de Compose para compilar funciones nuevas que abarcan una pantalla completa es la mejor manera de impulsar tu adopción de Compose. Con esta estrategia, puedes agregar funciones y aprovechar los beneficios de Compose sin dejar de satisfacer las necesidades comerciales de tu empresa.

Es posible que una función nueva abarque una pantalla completa, en cuyo caso, esta se incluiría en Compose. Si usas la navegación basada en Fragment, significa que tendrás que crear un nuevo Fragment y su contenido estará en Compose.

También puedes incorporar funciones nuevas en una pantalla existente. En este caso, los objetos View y Compose coexistirán en la misma pantalla. Por ejemplo, supongamos que la función que estás agregando es un nuevo tipo de vista en una RecyclerView. En ese caso, el nuevo tipo de vista estará en Compose y mantendrá los otros elementos iguales.

Cómo compilar una biblioteca de componentes comunes de la IU

A medida que compiles funciones con Compose, te darás cuenta rápidamente de que terminarás compilando una biblioteca de componentes. Te recomendamos que identifiques los componentes reutilizables que promueven la reutilización en la app, de modo que los componentes compartidos tengan una única fuente de información. Las funciones nuevas que compiles pueden depender de esta biblioteca.

Cómo reemplazar funciones existentes con Compose

Además de compilar funciones nuevas, te recomendamos que migres gradualmente las funciones existentes de tu app a Compose. Tú decides el enfoque que deseas adoptar, pero a continuación se mencionan algunas opciones recomendadas:

  1. Pantallas simples: se trata de pantallas simples en tu app con pocos elementos de IU y poco dinamismo, como una pantalla de bienvenida, una pantalla de confirmación o una pantalla de configuración. Estas pantallas se recomiendan para la migración a Compose, ya que se puede hacer con pocas líneas de código.
  2. Pantallas de Compose y View mixtas: las pantallas que ya contienen un poco de código de Compose son otra buena opción, ya que puedes continuar migrando elementos en esa pantalla paso a paso. Si tienes una pantalla con solo un subárbol en Compose, puedes seguir migrando otras partes del árbol hasta que toda la IU esté en Compose. Esto se denomina enfoque ascendente de la migración.

Enfoque ascendente de la migración de una IU con combinación de objetos View y Compose a una en Compose

El enfoque de este codelab

En este codelab, realizarás una migración incremental a Compose de la pantalla de detalles de plantas de Sunflower y harás que Compose y View trabajen en conjunto. Luego, si lo deseas, sabrás lo suficiente para continuar la migración.

3. Cómo prepararte

Obtén el código

Obtén el código del codelab de GitHub:

$ git clone https://github.com/android/codelab-android-compose

También tienes la opción de descargar el repositorio como archivo ZIP:

Cómo ejecutar la app de muestra

El código que acabas de descargar contiene código para todos los codelabs de Compose disponibles. Para completar este codelab, abre el proyecto MigrationCodelab en Android Studio.

En este codelab, migrarás la pantalla de detalles de plantas de Sunflower a Compose. Para abrir esta pantalla, presiona una de las plantas disponibles en la pantalla que muestra la lista de plantas.

bb6fcf50b2899894.png

Configuración del proyecto

El proyecto se compila en varias ramas de git:

  • La rama main es el punto de partida del codelab.
  • La rama end contiene la solución a este codelab.

Te recomendamos que comiences con el código de la rama main y sigas el codelab paso a paso a tu propio ritmo.

Durante el codelab, recibirás fragmentos de código que deberás agregar al proyecto. En algunos lugares, también deberás quitar el código que se mencionará explícitamente en los comentarios de los fragmentos de código.

Para obtener la rama end con git, cd en el directorio del proyecto MigrationCodelab seguido del comando a continuación:

$ git checkout end

También puedes descargar el código de la solución aquí:

Preguntas frecuentes

4. Compose en Sunflower

Compose ya se agregó al código que descargaste de la rama main. Sin embargo, veamos qué se requiere para que funcione.

Si abres el archivo build.gradle a nivel de la app, verás cómo importa las dependencias de Compose y permite que Android Studio funcione con Compose con la marca buildFeatures { compose true }.

app/build.gradle

android {
    //...
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        //...
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.3.2'
    }
}

dependencies {
    //...
    // Compose
    def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
    implementation(composeBom)
    androidTestImplementation(composeBom)

    implementation "androidx.compose.runtime:runtime"
    implementation "androidx.compose.ui:ui"
    implementation "androidx.compose.foundation:foundation"
    implementation "androidx.compose.foundation:foundation-layout"
    implementation "androidx.compose.material3:material3"
    implementation "androidx.compose.runtime:runtime-livedata"
    implementation "androidx.compose.ui:ui-tooling"
    //...
}

La versión de esas dependencias se define en el archivo build.gradle a nivel de proyecto.

5. ¡Hola, Compose!

En la pantalla de detalles de plantas, migraremos la descripción de la planta a Compose y dejaremos intacta la estructura general de la pantalla.

Compose necesita una Actividad o un Fragmento de host para renderizar la IU. En Sunflower, como todas las pantallas usan fragmentos, usarás ComposeView: una Vista de Android que puede alojar contenido de la IU de Compose mediante su método setContent.

Cómo quitar el código XML

Comencemos con la migración. Abre fragment_plant_detail.xml y haz lo siguiente:

  1. Cambia a la Vista de código.
  2. Quita el código ConstraintLayout y los 4 objetos TextView anidados dentro de la NestedScrollView (el codelab comparará el código XML y hará referencia a él cuando migres elementos individuales, por lo que será útil que tengas el código comentado).
  3. Agrega un ComposeView que alojará el código de Compose con compose_view como ID de vista.

fragment_plant_detail.xml

<androidx.core.widget.NestedScrollView
    android:id="@+id/plant_detail_scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="@dimen/fab_bottom_padding"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">

    <!-- Step 2) Comment out ConstraintLayout and its children ->
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="@dimen/margin_normal">

        <TextView
            android:id="@+id/plant_detail_name"
        ...
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    <!-- End Step 2) Comment out until here ->

    <!-- Step 3) Add a ComposeView to host Compose code ->
    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.core.widget.NestedScrollView>

Cómo agregar código de Compose

Ya tienes todo listo para comenzar a migrar la pantalla de detalles de plantas a Compose.

A lo largo del codelab, agregarás código de Compose al archivo PlantDetailDescription.kt en la carpeta plantdetail. Ábrelo y observa que ya tenemos un texto de marcador de posición "Hello Compose" disponible en el proyecto.

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription() {
    Surface {
        Text("Hello Compose")
    }  
}

Para mostrar esto en la pantalla, llama a este elemento componible desde el ComposeView que agregamos en el paso anterior. Abre PlantDetailFragment.kt.

Como la pantalla usa la vinculación de datos, puedes acceder directamente al composeView y llamar a setContent para mostrar el código de Compose en la pantalla. Llama al elemento componible PlantDetailDescription dentro de MaterialTheme, ya que Sunflower usa Material Design.

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    // ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            // ...
            composeView.setContent {
                // You're in Compose world!
                MaterialTheme {
                    PlantDetailDescription()
                }
            }
        }
        // ...
    }
}

Si ejecutas la app, verás que se muestra "Hello Compose" en la pantalla.

66f3525ecf6669e0.png

6. Cómo crear un elemento componible a partir de XML

Comencemos por migrar el nombre de la planta. Más precisamente, el TextView con el ID @+id/plant_detail_name que quitaste en fragment_plant_detail.xml. Este es el código XML:

<TextView
    android:id="@+id/plant_detail_name"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@{viewModel.plant.name}"
    android:textAppearance="?attr/textAppearanceHeadline5"
    ... />

Observa que tiene un estilo textAppearanceHeadline5, con un margen horizontal de 8.dp y centrado de forma horizontal en la pantalla. Sin embargo, el título que se mostrará se observa desde un LiveData expuesto por PlantDetailViewModel que proviene de la capa del repositorio.

Como observar un LiveData se revisará más adelante, asumamos que el nombre está disponible y se lo pasa como parámetro a un elemento nuevo PlantName que admite composición y que creamos en el archivo PlantDetailDescription.kt. Luego, se llamará a este elemento desde el elemento componible PlantDetailDescription.

PlantDetailDescription.kt

@Composable
private fun PlantName(name: String) {
    Text(
        text = name,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .wrapContentWidth(Alignment.CenterHorizontally)
    )
}

@Preview
@Composable
private fun PlantNamePreview() {
    MaterialTheme {
        PlantName("Apple")
    }
}

Con vista previa:

d09fe886b98bde91.png

Lugar:

  • El estilo de Text es MaterialTheme.typography.headlineSmall, que es similar a textAppearanceHeadline5 del código XML.
  • Los modificadores decoran el texto para que se vea como la versión XML:
  • El modificador fillMaxWidth se usa para que ocupe la cantidad máxima de ancho disponible. Este modificador corresponde al valor match_parent del atributo layout_width en el código XML.
  • El modificador padding se usa para que se aplique un valor de padding horizontal de margin_small. Esto corresponde a la declaración marginStart y marginEnd en XML. El valor margin_small también es el recurso de dimensión existente que se recupera con la función auxiliar dimensionResource.
  • El modificador wrapContentWidth se usa para alinear el texto y que esté centrado de forma horizontal. Esto es similar a tener una gravity de center_horizontal en XML.

7. ViewModels y LiveData

Ahora, conectemos el título a la pantalla. A tal fin, deberás cargar los datos mediante el PlantDetailViewModel. Para ello, Compose incluye integraciones para ViewModel y LiveData.

ViewModels

Como se usa una instancia de PlantDetailViewModel en el Fragment, podríamos pasarla como un parámetro a PlantDetailDescription y eso sería suficiente.

Abre el archivo PlantDetailDescription.kt y agrega el parámetro PlantDetailViewModel a PlantDetailDescription:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    //...
}

Ahora, pasa la instancia del ViewModel cuando llames a este elemento componible desde el fragmento:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        ...
        composeView.setContent {
            MaterialTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

LiveData

Con esto, ya tienes acceso al campo LiveData<Plant> de PlantDetailViewModel a fin de obtener el nombre de la planta.

Para observar LiveData desde un elemento componible, usa la función LiveData.observeAsState().

Como los valores emitidos por LiveData pueden ser null, deberás unir su uso en una verificación de null. Por ese motivo y para fines de reutilización, se recomienda dividir el consumo y la escucha de LiveData en diferentes elementos componibles. Por lo tanto, crea un nuevo elemento llamado PlantDetailContent que muestre la información de Plant.

Con estas actualizaciones, el archivo PlantDetailDescription.kt debería verse de la siguiente manera:

PlantDetailDescription.kt

@Composable
fun PlantDetailDescription(plantDetailViewModel: PlantDetailViewModel) {
    // Observes values coming from the VM's LiveData<Plant> field
    val plant by plantDetailViewModel.plant.observeAsState()

    // If plant is not null, display the content
    plant?.let {
        PlantDetailContent(it)
    }
}

@Composable
fun PlantDetailContent(plant: Plant) {
    PlantName(plant.name)
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

PlantNamePreview debe reflejar el cambio sin tener que actualizarlo directamente, ya que PlantDetailContent solo llama a PlantName:

3e47e682cf518c71.png

Ahora, conectaste el ViewModel para mostrar el nombre de una planta en Compose. En las siguientes secciones, compilarás el resto de los elementos componibles y los conectarás al ViewModel de manera similar.

8. Más migraciones de código XML

Ahora, es más fácil completar lo que falta en nuestra IU: la información de riego y la descripción de las plantas. Si sigues un enfoque similar al anterior, ya puedes migrar el resto de la pantalla.

El código XML de la información de riego que quitaste antes de fragment_plant_detail.xml consta de dos TextViews con los IDs plant_watering_header y plant_watering.

<TextView
    android:id="@+id/plant_watering_header"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginTop="@dimen/margin_normal"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    android:text="@string/watering_needs_prefix"
    android:textColor="?attr/colorAccent"
    android:textStyle="bold"
    ... />

<TextView
    android:id="@+id/plant_watering"
    ...
    android:layout_marginStart="@dimen/margin_small"
    android:layout_marginEnd="@dimen/margin_small"
    android:gravity="center_horizontal"
    app:wateringText="@{viewModel.plant.wateringInterval}"
    .../>

De manera similar a lo que hiciste antes, crea un nuevo elemento componible llamado PlantWatering y agrega elementos componibles Text para mostrar la información de riego en la pantalla:

PlantDetailDescription.kt

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun PlantWatering(wateringInterval: Int) {
    Column(Modifier.fillMaxWidth()) {
        // Same modifier used by both Texts
        val centerWithPaddingModifier = Modifier
            .padding(horizontal = dimensionResource(R.dimen.margin_small))
            .align(Alignment.CenterHorizontally)

        val normalPadding = dimensionResource(R.dimen.margin_normal)

        Text(
            text = stringResource(R.string.watering_needs_prefix),
            color = MaterialTheme.colorScheme.primaryContainer,
            fontWeight = FontWeight.Bold,
            modifier = centerWithPaddingModifier.padding(top = normalPadding)
        )

        val wateringIntervalText = pluralStringResource(
            R.plurals.watering_needs_suffix, wateringInterval, wateringInterval
        )
        Text(
            text = wateringIntervalText,
            modifier = centerWithPaddingModifier.padding(bottom = normalPadding)
        )
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    MaterialTheme {
        PlantWatering(7)
    }
}

Con vista previa:

6f6c17085801a518.png

Debes tener en cuenta lo siguiente:

  • A medida que los elementos Text que admiten composición comparten el padding horizontal y la decoración de alineación, puedes volver a usar el Modificador asignándolo a una variable local (es decir, centerWithPaddingModifier). Esto es posible dado que los modificadores son objetos normales de Kotlin.
  • El MaterialTheme de Compose no tiene una concordancia exacta con el colorAccent que se usa en plant_watering_header. Por ahora, usa MaterialTheme.colorScheme.primaryContainer, que mejorarás en la sección de temas de interoperabilidad.
  • En Compose 1. 2.1, para usar pluralStringResource, es necesario habilitar ExperimentalComposeUiApi. En una versión futura de Compose, es posible que ya no sea necesario.

Conectemos todas las piezas y llamemos a PlantWatering desde PlantDetailContent. El código XML ConstraintLayout que quitamos al principio tenía un margen de 16.dp que debemos incluir en nuestro código de Compose.

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="@dimen/margin_normal">

En PlantDetailContent, crea un objeto Column a fin de mostrar el nombre y la información de riego juntos, y tener eso como padding. Además, para que el color de fondo y los colores de texto sean apropiados, agrega un Surface que se encargue de eso.

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
        }
    }
}

Si actualiza la vista previa, verás lo siguiente:

56626a7118ce075c.png

9. Vistas en el código de Compose

Ahora, migraremos la descripción de las plantas. El código de fragment_plant_detail.xml tenía una TextView con app:renderHtml="@{viewModel.plant.description}" que le indicaba al XML qué texto mostrar en la pantalla. renderHtml es un adaptador de vinculación que puedes encontrar en el archivo PlantDetailBindingAdapters.kt. La implementación usa HtmlCompat.fromHtml para configurar el texto en TextView.

Sin embargo, Compose no admite clases Spanned ni muestra texto con formato HTML en este momento. Por lo tanto, necesitamos usar una TextView del sistema de View en el código de Compose a fin de evitar esta limitación.

Dado que Compose aún no puede renderizar código HTML, deberás crear una TextView de manera programática para hacer exactamente eso con la API de AndroidView.

AndroidView te permite construir un elemento View en su lambda factory. También proporciona una lambda update que se invoca cuando la View se aumenta y en las recomposiciones posteriores.

Para ello, creamos un nuevo elemento componible PlantDescription. Este elemento componible llama a AndroidView, que construye un elemento TextView en su lambda factory. En la lambda factory, inicializa un elemento TextView que muestra texto con formato HTML seguido de la configuración de movementMethod en una instancia de LinkMovementMethod. Por último, en la lambda update, configura el texto de TextView como htmlDescription.

PlantDetailDescription.kt

@Composable
private fun PlantDescription(description: String) {
    // Remembers the HTML formatted description. Re-executes on a new description
    val htmlDescription = remember(description) {
        HtmlCompat.fromHtml(description, HtmlCompat.FROM_HTML_MODE_COMPACT)
    }

    // Displays the TextView on the screen and updates with the HTML description when inflated
    // Updates to htmlDescription will make AndroidView recompose and update the text
    AndroidView(
        factory = { context ->
            TextView(context).apply {
                movementMethod = LinkMovementMethod.getInstance()
            }
        },
        update = {
            it.text = htmlDescription
        }
    )
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    MaterialTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Vista previa:

deea1d191e9087b4.png

Observa que htmlDescription recuerda la descripción HTML de un elemento description determinado que se pasa como parámetro. Si cambia el parámetro description, se volverá a ejecutar el código htmlDescription dentro de remember.

Como resultado, se recompondrá la devolución de llamada de actualización AndroidView si cambia htmlDescription. Cualquier estado que se lea dentro de la lambda update provocará una recomposición.

Agrega PlantDescription al elemento componible PlantDetailContent y cambia el código de la vista previa de modo que muestre una descripción HTML:

PlantDetailDescription.kt

@Composable
fun PlantDetailContent(plant: Plant) {
    Surface {
        Column(Modifier.padding(dimensionResource(R.dimen.margin_normal))) {
            PlantName(plant.name)
            PlantWatering(plant.wateringInterval)
            PlantDescription(plant.description)
        }
    }
}

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    MaterialTheme {
        PlantDetailContent(plant)
    }
}

Con vista previa:

7843a8d6c781c244.png

En este punto, ya migraste todo el contenido dentro del ConstraintLayout original a Compose. Puedes ejecutar la app para verificar que funcione según lo previsto.

c7021c18eb8b4d4e.gif

10. ViewCompositionStrategy

Compose elimina la composición cuando ComposeView se separa de una ventana. Esto es un inconveniente cuando se usa ComposeView en fragmentos por los siguientes 2 motivos:

  • La composición debe seguir el ciclo de vida de la vista del fragmento para los tipos de View de la IU de Compose para guardar el estado.
  • Cuando se realicen transiciones, el ComposeView subyacente estará en un estado separado. Sin embargo, los elementos de la IU de Compose seguirán siendo visibles durante estas transiciones.

Para modificar este comportamiento, llama a setViewCompositionStrategy con el ViewCompositionStrategy adecuado de modo que siga el ciclo de vida de la vista del fragmento. En particular, te recomendamos que uses la estrategia DisposeOnViewTreeLifecycleDestroyed para eliminar la composición cuando se destruya el LifecycleOwner del fragmento.

Como PlantDetailFragment tiene transiciones de entrada y de salida (comprueba nav_garden.xml para obtener más información) y usaremos tipos de View de Compose más adelante, debemos asegurarnos de que ComposeView utilice la estrategia DisposeOnViewTreeLifecycleDestroyed. No obstante, configurar siempre esta estrategia cuando se use ComposeView en fragmentos es una práctica recomendada.

PlantDetailFragment.kt

import androidx.compose.ui.platform.ViewCompositionStrategy
...

class PlantDetailFragment : Fragment() {
    ...
    override fun onCreateView(...): View? {
        val binding = DataBindingUtil.inflate<FragmentPlantDetailBinding>(
            inflater, R.layout.fragment_plant_detail, container, false
        ).apply {
            ...
            composeView.apply {
                // Dispose the Composition when the view's LifecycleOwner
                // is destroyed
                setViewCompositionStrategy(
                    ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
                )
                setContent {
                    MaterialTheme {
                        PlantDetailDescription(plantDetailViewModel)
                    }
                }
            }
        }
        ...
    }
}

11. Temas de Material

Se migró el contenido de texto de los detalles de las plantas a Compose. Sin embargo, habrás notado que Compose no usa los colores de tema correctos. Usa el color púrpura en el nombre de la planta cuando debería usar el verde.

Deberás personalizar MaterialTheme para usar los colores de tema correctos. Para ello, define tu propio tema y proporciona sus colores.

Cómo personalizar MaterialTheme

Para crear tu propio tema, abre el archivo Theme.kt en el paquete theme. Theme.kt define un elemento componible llamado SunflowerTheme que acepta una lambda de contenido y la pasa a un MaterialTheme.

Todavía no hace nada interesante, lo personalizarás a continuación.

Theme.kt

import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable

@Composable
fun SunflowerTheme(
    content: @Composable () -> Unit
) {
    MaterialTheme(content = content)
}

MaterialTheme te permite personalizar sus colores, tipografía y formas. Por ahora, puedes personalizar los colores proporcionando los mismos colores en el tema de Sunflower View. El SunflowerTheme también puede aceptar un parámetro booleano llamado darkTheme, que se establecerá de forma predeterminada como true si el sistema está en modo oscuro; de lo contrario, estará como false. Con este parámetro, podemos pasar los valores de color correctos a MaterialTheme para que coincidan con el tema del sistema establecido actualmente.

Theme.kt

@Composable
fun SunflowerTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val lightColors  = lightColorScheme(
        primary = colorResource(id = R.color.sunflower_green_500),
        primaryContainer = colorResource(id = R.color.sunflower_green_700),
        secondary = colorResource(id = R.color.sunflower_yellow_500),
        background = colorResource(id = R.color.sunflower_green_500),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
    )
    val darkColors  = darkColorScheme(
        primary = colorResource(id = R.color.sunflower_green_100),
        primaryContainer = colorResource(id = R.color.sunflower_green_200),
        secondary = colorResource(id = R.color.sunflower_yellow_300),
        onPrimary = colorResource(id = R.color.sunflower_black),
        onSecondary = colorResource(id = R.color.sunflower_black),
        onBackground = colorResource(id = R.color.sunflower_black),
        surface = colorResource(id = R.color.sunflower_green_100_8pc_over_surface),
        onSurface = colorResource(id = R.color.sunflower_white),
    )
    val colors = if (darkTheme) darkColors else lightColors
    MaterialTheme(
        colorScheme = colors,
        content = content
    )
}

Para usar esto, reemplaza los usos de MaterialTheme para SunflowerTheme. Por ejemplo, en PlantDetailFragment:

PlantDetailFragment.kt

class PlantDetailFragment : Fragment() {
    ...
    composeView.apply {
        ...
        setContent {
            SunflowerTheme {
                PlantDetailDescription(plantDetailViewModel)
            }
        }
    }
}

Y todos los elementos de vista previa que admiten composición en el archivo PlantDetailDescription.kt:

PlantDetailDescription.kt

@Preview
@Composable
private fun PlantDetailContentPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

@Preview
@Composable
private fun PlantNamePreview() {
    SunflowerTheme {
        PlantName("Apple")
    }
}

@Preview
@Composable
private fun PlantWateringPreview() {
    SunflowerTheme {
        PlantWatering(7)
    }
}

@Preview
@Composable
private fun PlantDescriptionPreview() {
    SunflowerTheme {
        PlantDescription("HTML<br><br>description")
    }
}

Como puedes ver en la vista previa, los colores ahora deberían coincidir con los del tema Sunflower.

886d7eaea611f4eb.png

También puedes obtener una vista previa de la IU en tema oscuro creando una función nueva y pasando Configuration.UI_MODE_NIGHT_YES al uiMode de la vista previa:

import android.content.res.Configuration
...

@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PlantDetailContentDarkPreview() {
    val plant = Plant("id", "Apple", "HTML<br><br>description", 3, 30, "")
    SunflowerTheme {
        PlantDetailContent(plant)
    }
}

Con vista previa:

cfe11c109ff19eeb.png

Si ejecutas la app, esta se comportará exactamente igual que antes de la migración tanto en el tema claro como en el oscuro:

438d2dd9f8acac39.gif

12. Prueba

Después de migrar partes de la pantalla de detalles de las plantas a Compose, las pruebas son fundamentales para asegurarte de que no hayas dañado nada.

En Sunflower, el objeto PlantDetailFragmentTest ubicado en la carpeta androidTest prueba algunas funciones de la app. Abre el archivo y observa el código actual:

  • testPlantName verifica el nombre de la planta en la pantalla.
  • testShareTextIntent verifica que se active el intent correcto después de presionar el botón para compartir.

Cuando una actividad o un fragmento utilizan Compose, en lugar de ActivityScenarioRule, debes usar createAndroidComposeRule, que integra ActivityScenarioRule con un objeto ComposeTestRule que te permite probar el código de Compose.

En PlantDetailFragmentTest, reemplaza la regla ActivityScenarioRule de uso por createAndroidComposeRule. Cuando se necesite la regla de actividad para configurar la prueba, usa el atributo activityRule de createAndroidComposeRule de la siguiente manera:

@RunWith(AndroidJUnit4::class)
class PlantDetailFragmentTest {

    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<GardenActivity>()
   
    ...

    @Before
    fun jumpToPlantDetailFragment() {
        populateDatabase()

        composeTestRule.activityRule.scenario.onActivity { gardenActivity ->
            activity = gardenActivity

            val bundle = Bundle().apply { putString("plantId", "malus-pumila") }
            findNavController(activity, R.id.nav_host).navigate(R.id.plant_detail_fragment, bundle)
        }
    }

    ...
}

Si ejecutas las pruebas, testPlantName fallará. testPlantName verifica que haya una TextView en la pantalla. Sin embargo, migraste esa parte de la IU a Compose. Por lo tanto, en su lugar, debes usar aserciones de Compose:

@Test
fun testPlantName() {
    composeTestRule.onNodeWithText("Apple").assertIsDisplayed()
}

Si ejecutas las pruebas, verás que todas son exitosas.

dd59138fac1740e4.png

13. Felicitaciones

¡Felicitaciones! Completaste este codelab con éxito.

La rama compose del proyecto original de GitHub de Sunflower migra por completo la pantalla de detalles de las plantas a Compose. Además de lo que hiciste en este codelab, también simula el comportamiento del CollapsingToolbarLayout. Esto incluye lo siguiente:

  • Carga de imágenes con Compose
  • Animaciones
  • Mejor control de las dimensiones
  • Y mucho más

¿Qué sigue?

Consulta los otros codelabs sobre la ruta de aprendizaje de Compose:

Lecturas adicionales