Cómo integrar Compose con tu IU existente

Si tienes una app con una IU basada en objetos View, es posible que no quieras volver a escribir toda su IU de una sola vez. Esta página te ayudará a agregar elementos de Compose nuevos a tu IU existente.

Cómo migrar la IU compartida

Si migras gradualmente a Compose, es posible que necesites usar elementos compartidos de la IU en el sistema de Compose y de View. Por ejemplo, si tu app tiene un componente CallToActionButton personalizado, es posible que debas usarlo en pantallas basadas en Compose y en View.

En Compose, los elementos compartidos de la IU se convierten en elementos que admiten composición y que se pueden volver a usar en la app, independientemente de que el elemento al que se le aplica el estilo utilice XML o sea una vista personalizada. Por ejemplo, deberías crear un elemento CallToActionButton que admita composición para tu componente Button de llamada a la acción personalizada.

Para usar el elemento que admite composición en las pantallas basadas en View, debes crear un wrapper de vista personalizado que se extienda desde AbstractComposeView. En su elemento Content anulado que admite composición, coloca el elemento que creaste unido al tema de Compose como se muestra en el siguiente ejemplo:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Ten en cuenta que los parámetros que admiten composición se convierten en variables mutables dentro de la vista personalizada. Eso hace que la vista CallToActionViewButton personalizada aumente y se pueda usar (por ejemplo, con la vinculación de vistas), como una vista tradicional. Consulta el siguiente ejemplo:

class ExampleActivity : Activity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.something)
            onClick = { /* Do something */ }
        }
    }
}

Si el componente personalizado contiene un estado mutable, consulta la fuente de información de estado.

Temas

Según Material Design, el uso de la biblioteca Componentes de Material Design para Android (MDC) es la manera recomendada de diseñar apps para Android. Como se explica en la documentación de temas de Compose, Compose implementa esos conceptos con el elemento que admite composición MaterialTheme.

Cuando crees pantallas nuevas en Compose, asegúrate de aplicar un MaterialTheme antes de cualquier elemento que admita composición que emita IU desde la biblioteca de componentes materiales. Los componentes materiales (Button, Text, etc.) dependen de que se implemente un MaterialTheme, y su comportamiento no está definido sin él.

Todos los ejemplos de Jetpack Compose usan un tema de Compose personalizado creado sobre la base de MaterialTheme.

Varias fuentes de confianza

Es probable que una app existente tenga una gran cantidad de temas y estilos para las vistas. Si implementas Compose en una app existente, necesitarás migrar el tema a fin de usar MaterialTheme para cualquier pantalla de Compose. Eso significa que el tema de tu app tendrá 2 fuentes de confianza: un tema basado en la vista y otro basado en Compose. Si decides realizar cambios en tu estilo, deberás hacerlos en varios lugares.

Si tu plan es migrar por completo la app a Compose, eventualmente deberás crear una versión de Compose del tema existente. El problema es que, cuanto antes en el proceso de desarrollo crees tu tema de Compose, más mantenimiento tendrás que hacer durante el proceso.

Adaptador de temas de MDC Compose

Si utilizas la biblioteca de MDC en tu app para Android, la biblioteca del Adaptador de temas de MDC Compose te permitirá volver a usar fácilmente el color, la tipografía y la forma de tus temas basados en View existentes en tus elementos que admiten composición.

import com.google.android.material.composethemeadapter.MdcTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MdcTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Consulta la documentación sobre la biblioteca de MDC para obtener más información.

Adaptador de temas de AppCompat Compose

La biblioteca del Adaptador de temas de AppCompat Compose te permite volver a usar fácilmente temas de XML de AppCompat para la creación de temas en Jetpack Compose. Crea un MaterialTheme con los valores de color y tipografía del tema del contexto.

import com.google.accompanist.appcompattheme.AppCompatTheme

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            AppCompatTheme {
                // Colors, typography, and shape have been read from the
                // View-based theme used in this Activity
                ExampleComposable(/*...*/)
            }
        }
    }
}

Estilos de componentes predeterminados

Las bibliotecas de MDC y del Adaptador de temas de AppCompat Compose no leen ningún estilo de widget predeterminado definido por temas. Eso se debe a que Compose no tiene el concepto de elementos que admiten composición predeterminados.

Obtén más información sobre los estilos de componentes y los sistemas de diseño personalizados en la documentación de temas.

Superposiciones de temas en Compose

Cuando migres pantallas basadas en View a Compose, presta atención a los usos del atributo android:theme. Es probable que necesites un nuevo MaterialTheme en esa parte del árbol de IU de Compose.

Obtén más información al respecto en la guía sobre temas.

Animaciones de WindowInsets e IME

Puedes controlar WindowInsets si usas la biblioteca de inserciones de acompañamiento, que brinda elementos que admiten composición y modificadores para controlar las inserciones en tus diseños, además de compatibilidad con las animaciones IME.

class ExampleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
                ProvideWindowInsets {
                    MyScreen()
                }
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding(), // Move it out from under the nav bar
            onClick = { }
        ) {
            Icon( /* ... */)
        }
    }
}

Animación que muestra un elemento de IU que se desplaza desde arriba hacia abajo a fin de dejar lugar para un teclado.

Figura 2: Animaciones IME que usan la biblioteca de inserciones de acompañamiento

Si deseas obtener más información, consulta la documentación de la biblioteca de inserciones de acompañamiento.

Prioriza la división del estado sobre la presentación

Tradicionalmente, un View es un elemento con estado. Un View administra los campos que describen qué mostrar, además de cómo mostrarlo. Cuando conviertas un elemento View en Compose, separa los datos que se procesarán para lograr un flujo de datos unidireccional, tal y como se explica con más detalle en el documento de elevación de estado.

Por ejemplo, un elemento View tiene una propiedad visibility que describe si es visible, invisible o no está presente. Esta es una propiedad inherente de View. Si bien otros fragmentos de código pueden cambiar la visibilidad de un elemento View, solo View sabe su visibilidad actual. La lógica para garantizar que un View sea visible puede ser propensa a errores y, a menudo, está vinculada al propio elemento View.

Por el contrario, Compose facilita la visualización de elementos componibles completamente diferentes cuando se usa la lógica condicional en Kotlin que se muestra a continuación:

if (showCautionIcon) {
    CautionIcon(/* ... */)
}

Por defecto, CautionIcon no necesita ni le interesa saber por qué se muestra, y no hay un concepto de visibility dado que este puede estar presente en la composición como no.

Si separas de forma clara la administración del estado y la lógica de presentación, puedes cambiar con mayor libertad la manera en la que muestras contenido como una conversión de estado en una IU. Poder elevar el estado cuando sea necesario también hace que los elementos componibles sean más reutilizables, ya que la propiedad del estado es más flexible.

Promueve componentes encapsulados y reutilizables

Los elementos View a menudo saben dónde se encuentran: dentro de una Activity, un Dialog, un Fragment o algún lugar dentro de otra jerarquía View. Debido a que suelen aumentarse a partir de archivos de diseño estáticos, la estructura general de un elemento View suele ser muy rígida. Como resultado, se produce un acoplamiento más alto y hace que sea más difícil cambiar o reutilizar un elemento View.

Por ejemplo, un View personalizado puede suponer que tiene un elemento View secundario de un tipo determinado, con un ID determinado, y cambiar sus propiedades directamente como respuesta a alguna acción. Esto acopla altamente los elementos View, lo que aumenta las posibilidades de que el View personalizado falle si no puede encontrar el elemento secundario, y es probable que el elemento secundario no se pueda volver a usar si el elemento View superior no se personaliza.

Cuando se usan elementos componibles, las probabilidades de que esto ocurra son menores. Los elementos superiores pueden especificar con facilidad el estado y las devoluciones de llamada, de modo que se puedan escribir elementos componibles reutilizables sin tener que saber exactamente la ubicación en la se usarán.

var isEnabled by rememberSaveable { mutableStateOf(false) }

Column {
    ImageWithEnabledOverlay(isEnabled)
    ControlPanelWithToggle(
        isEnabled = isEnabled,
        onEnabledChanged = { isEnabled = it }
    )
}

En el ejemplo anterior, todas las tres partes están más encapsuladas y menos acopladas:

  • ImageWithEnabledOverlay solo necesita saber cuál es el estado actual de isEnabled. No necesita saber si ControlPanelWithToggle existe o, incluso, cómo se puede controlar.

  • ControlPanelWithToggle no sabe que ImageWithEnabledOverlay existe. Puede haber cero, una o más formas en que se muestra isEnabled y ControlPanelWithToggle no tendría que cambiar.

  • Al elemento superior, no le importa la profundidad de las anidaciones de ImageWithEnabledOverlay o ControlPanelWithToggle. Esos elementos secundarios podrían fomentar cambios, intercambiar contenido o transmitirlo a otros elementos secundarios.

Este patrón se conoce como la inversión de control. Si lo deseas, puedes obtener más información sobre este tema en la documentación de CompositionLocal.

Cómo controlar cambios de tamaños de pantalla

Una de las formas principales de crear diseños responsivos de View es tener diferentes recursos para diferentes tamaños de ventanas. Si bien los recursos calificados continúan siendo una opción para las decisiones de diseño a nivel de la pantalla, Compose facilita el cambio completo de los diseños en el código con una lógica condicional normal. Como se muestra a continuación, con el uso de herramientas como BoxWithConstraints, se pueden tomar decisiones en función del espacio disponible para elementos individuales, lo cual no es posible con recursos calificados:

@Composable
fun MyComposable() {
    BoxWithConstraints {
        if (minWidth < 480.dp) {
            /* Show grid with 4 columns */
        } else if (minWidth < 720.dp) {
            /* Show grid with 8 columns */
        } else {
            /* Show grid with 12 columns */
        }
    }
}

Lee el documento sobre creación de diseños adaptativos a fin de obtener más información sobre las técnicas que ofrece Compose para compilar IU adaptables.

Desplazamiento anidado con View

Lamentablemente, el desplazamiento anidado entre el sistema de View y Jetpack Compose todavía no está disponible. Puedes verificar el progreso en este error con la herramienta de seguimiento de errores.

Compose en RecyclerView

Jetpack Compose usa DisposeOnDetachedFromWindow como el valor predeterminado ViewCompositionStrategy. Eso significa que la composición se descarta cada vez que se separa la vista de la ventana.

Cuando se usa un objeto ComposeView como parte de un contenedor RecyclerView de View, la estrategia predeterminada no es eficiente, ya que las instancias de composición subyacentes permanecerán en la memoria hasta que se desvincule RecyclerView de la ventana. Una práctica recomendada es descartar la composición subyacente cuando RecyclerView ya no necesita ComposeView.

La función disposeComposition permite descartar la composición subyacente de un ComposeView de forma manual. Puedes llamar a esta función cuando se recicla la vista de la siguiente manera:

import androidx.compose.ui.platform.ComposeView

class MyComposeAdapter : RecyclerView.Adapter<MyComposeViewHolder>() {

    override fun onCreateViewHolder(
        parent: ViewGroup,
        viewType: Int,
    ): MyComposeViewHolder {
        return MyComposeViewHolder(ComposeView(parent.context))
    }

    override fun onViewRecycled(holder: MyComposeViewHolder) {
        // Dispose of the underlying Composition of the ComposeView
        // when RecyclerView has recycled this ViewHolder
        holder.composeView.disposeComposition()
    }

    /* Other methods */
}

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {
    /* ... */
}

Como se explica en la sección ViewCompositionStrategy de ComposeView de la guía sobre API de interoperabilidad, es necesario usar la estrategia DisposeOnViewTreeLifecycleDestroyed para que el contenedor de vistas de Compose funcione en todos los casos.

import androidx.compose.ui.platform.ViewCompositionStrategy

class MyComposeViewHolder(
    val composeView: ComposeView
) : RecyclerView.ViewHolder(composeView) {

    init {
        composeView.setViewCompositionStrategy(
            ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
        )
    }

    fun bind(input: String) {
        composeView.setContent {
            MdcTheme {
                Text(input)
            }
        }
    }
}

Para ver cómo funciona ComposeView en RecyclerView, consulta la rama compose_recyclerview de la app de Sunflower.