Otros factores que considerar

Si bien la migración de vistas a Compose está puramente relacionada con la IU, existen muchos aspectos que se deben tener en cuenta para realizar una migración incremental y segura. En esta página, se incluyen algunas consideraciones para migrar tu app basada en objetos View a Compose.

Cómo migrar el tema de tu app

Material Design es el sistema de diseño recomendado para aplicar temas a las apps para Android.

Para las apps basadas en View, hay tres versiones de Material disponibles:

  • Material Design 1 con la biblioteca AppCompat (es decir, Theme.AppCompat.*)
  • Material Design 2 con la biblioteca MDC-Android (es decir, Theme.MaterialComponents.*)
  • Material Design 3 con la biblioteca MDC-Android (es decir, Theme.Material3.*)

Para las apps de Compose, hay dos versiones de Material disponibles:

  • Material Design 2 con la biblioteca de Compose Material (es decir, androidx.compose.material.MaterialTheme)
  • Material Design 3 con la biblioteca de Compose Material 3 (es decir, androidx.compose.material3.MaterialTheme)

Te recomendamos que uses la versión más reciente, (Material 3) si el sistema de diseño de tu app está en posición de hacerlo. Hay guías de migración disponibles para View y Compose:

Cuando crees pantallas nuevas en Compose, independientemente de la versión de Material Design que uses, asegúrate de aplicar un MaterialTheme antes de cualquier elemento componible que emita IU desde las bibliotecas de Compose Material. Los componentes de Material (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 compilado sobre MaterialTheme.

Consulta Cómo diseñar sistemas en Compose y Cómo migrar temas de XML a Compose para obtener más información.

Si usas el componente Navigation de tu app, consulta Cómo navegar con Compose: Interoperabilidad y Cómo migrar Navigation de Jetpack a Navigation Compose para obtener más información.

Cómo probar tu IU mixta de objetos View y Compose

Después de migrar partes de tu app a Compose, las pruebas son fundamentales para asegurarte de que no se haya dañado nada.

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

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Consulta Cómo probar tu diseño de Compose para obtener más información sobre las pruebas. Para conocer la interoperabilidad con los frameworks de prueba de la IU, consulta Interoperabilidad con Espresso y también Interoperabilidad con UiAutomator.

Integra Compose con tu arquitectura de app existente

Los patrones de arquitectura de flujo unidireccional de datos (UDF) funcionan perfectamente con Compose. Si la app usa otros tipos de patrones de arquitectura, como Model View Presenter (MVP), te recomendamos que migres esa parte de la IU a UDF antes de implementar Compose o mientras lo haces.

Cómo usar ViewModel en Compose

Cuando utilizas la biblioteca de ViewModel de componentes de la arquitectura, puedes acceder a un ViewModel desde cualquier elemento componible llamando a la función viewModel(), como se explica en Compose y otras bibliotecas.

Si implementas Compose, ten cuidado con usar el mismo tipo de ViewModel en distintos elementos componibles, ya que los elementos ViewModel tienen alcances de ciclo de vida de View. Si se utiliza la biblioteca de navegación, el alcance será la actividad del host, el fragmento o el gráfico de navegación.

Por ejemplo, si los elementos componibles están alojados en una actividad, viewModel() siempre mostrará la misma instancia, que recién se borrará cuando termine la actividad. En el siguiente ejemplo, se le dará la bienvenida al mismo usuario ("user1") dos veces porque la misma instancia de GreetingViewModel se volvió a usar en todos los elementos componibles dentro de la actividad de host. La primera instancia de ViewModel creada se vuelve a utilizar en otros elementos componibles.

class GreetingActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Como los gráficos de navegación también definen el alcance de los elementos ViewModel, los elementos componibles que sean un destino en un gráfico de navegación tendrán una instancia distinta del ViewModel. En este caso, el alcance de ViewModel se define según el ciclo de vida del destino, y se borrará cuando se quite el destino de la pila de actividades. En el siguiente ejemplo, cuando el usuario navega a la pantalla de perfil, se crea una nueva instancia de GreetingViewModel.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

El estado como fuente de confianza

Si implementas Compose en una parte de la IU, es posible que Compose y el código de sistema de View necesiten compartir datos. Cuando sea posible, te recomendamos que encapsules ese estado compartido en otra clase que cumpla las prácticas recomendadas de UDF que utilizan las dos plataformas; por ejemplo, en un ViewModel que exponga un flujo de los datos compartidos para emitir actualizaciones de datos.

Sin embargo, no siempre es posible si los datos que se compartirán son mutables o si están estrechamente vinculados a un elemento de la IU. En ese caso, un sistema debe ser la fuente de confianza. Además, ese sistema debe compartir todas las actualizaciones de datos con el otro sistema. Como regla general, la fuente de confianza debe ser propiedad del elemento que esté más cerca de la raíz en la jerarquía de IU.

Compose como fuente de confianza

Usa el elemento componible SideEffect para convertir el estado basado en Compose en código no basado en Compose. En este caso, la fuente de confianza se conserva en un elemento componible y que envía actualizaciones de estado.

Como ejemplo, tu biblioteca de estadísticas podría permitirte segmentar tu población de usuarios adjuntando metadatos personalizados (en este ejemplo, propiedades del usuario) a todos los eventos de estadísticas posteriores. Para comunicar el tipo de usuario actual a la biblioteca de estadísticas, usa SideEffect para actualizar su valor.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Para obtener más información, consulta Efectos secundarios en Compose.

El sistema de vistas como fuente de confianza

Si el sistema de vistas es propietario del estado y lo comparte con Compose, te recomendamos que unas el estado de objetos mutableStateOf para que sea seguro a nivel de los subprocesos para Compose. Si usas este enfoque, las funciones que admiten composición se simplifican debido a que ya no tienen la fuente de confianza. Sin embargo, el sistema de View necesita actualizar el estado mutable y las vistas que usan ese estado.

En el siguiente ejemplo, un CustomViewGroup contiene una TextView y una ComposeView con un elemento que admite composición TextField en su interior. La TextView necesita mostrar el contenido de lo que escribe el usuario en el TextField.

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

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

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 componibles 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 componible para tu componente Button de llamada a la acción personalizada.

Para usar el elemento componible en las pantallas basadas en vistas, debes crear un wrapper de vista personalizado que se extienda desde AbstractComposeView. En su elemento Content anulado componible, 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(
            containerColor = MaterialTheme.colorScheme.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("")
    var onClick by mutableStateOf({})

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

Ten en cuenta que los parámetros componibles se convierten en variables mutables dentro de la vista personalizada. Eso hace que la vista CallToActionViewButton personalizada aumente y se pueda usar, como una vista tradicional. A continuación, verás un ejemplo de esto con la vinculación de vistas:

class ViewBindingActivity : ComponentActivity() {

    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.greeting)
            onClick = { /* Do something */ }
        }
    }
}

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

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:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    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 puedas escribir elementos componibles reutilizables sin tener que saber exactamente la ubicación en la se usarán.

@Composable
fun AScreen() {
    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. Si quieres obtener más información, consulta el documento para brindar compatibilidad con diferentes tamaños de pantalla.

Además, consulta el documento sobre creación de diseños adaptativos para obtener información sobre las técnicas que ofrece Compose para compilar IUs adaptables.

Desplazamiento anidado con View

Para 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 componibles en RecyclerView tienen un buen rendimiento desde la versión 1.3.0-alpha02 de RecyclerView. Asegúrate de tener al menos la versión 1.3.0-alpha02 de RecyclerView para ver esos beneficios.

Interoperabilidad de WindowInsets con Views

Es posible que debas anular las inserciones predeterminadas si tu pantalla tiene tanto elementos View como código de Compose en la misma jerarquía. En este caso, debes especificar de forma explícita en cuál se deben consumir las inserciones y cuál se deben ignorar.

Por ejemplo, si tu diseño más externo es un diseño de Android View, debes consumir las inserciones en el sistema de View e ignorarlas para Compose. Como alternativa, si tu diseño más externo es un elemento componible, debes consumir las inserciones en Compose y rellenar los elementos AndroidView componibles según corresponda.

De forma predeterminada, cada ComposeView consume todas las inserciones en el nivel de consumo WindowInsetsCompat. Para cambiar este comportamiento predeterminado, establece ComposeView.consumeWindowInsets en false.

Para obtener más información, lee la documentación de WindowInsets en Compose.