Cómo integrar Compose con tu IU existente

Organiza tus páginas con colecciones Guarda y categoriza el contenido según tus preferencias.

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 {
            // Use MdcTheme instead of MaterialTheme
            // Colors, typography, and shape have been read from the
            // View-based theme used in this Activity
            MdcTheme {
                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.

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

A partir de Compose 1.2.0, puedes controlar WindowInsets con modificadores para hacerlo dentro de tus diseños. También se admiten las animaciones IME.

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

        WindowCompat.setDecorFitsSystemWindows(window, false)

        setContent {
            MaterialTheme {
              MyScreen()
            }
        }
    }
}

@Composable
fun MyScreen() {
    Box {
        LazyColumn(
            modifier = Modifier
                .fillMaxSize() // fill the entire window
                .imePadding() // padding for the bottom for the IME
                .imeNestedScroll(), // scroll IME at the bottom
            content = { }
        )
        FloatingActionButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp) // normal 16dp of padding for FABs
                .navigationBarsPadding() // padding for navigation bar
                .imePadding(), // padding for when IME appears
            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

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

A fin de obtener más información para habilitar la interoperabilidad de desplazamiento anidada entre elementos de View desplazables y elementos componibles desplazables, anidados en ambas direcciones, lee el artículo sobre Interoperabilidad de desplazamiento anidada.

Compose en RecyclerView

Los elementos ranging channel en RecyclerView tienen un buen rendimiento desde la versión 1.3.0-alpha02 de RecyclerView. Asegúrate de usar al menos la versión 1.3.0-alpha02 de RecyclerView para ver esos beneficios.