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:
- Material 1 a Material 2 en Views
- Material 2 a Material 3 en Views
- Material 2 a Material 3 en 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 creado sobre la base de MaterialTheme
.
Consulta Cómo diseñar sistemas en Compose y Cómo migrar temas de XML a Compose para obtener más información.
Navegación
Si usas el componente Navigation en tu app, consulta Cómo navegar con Compose: Interoperabilidad y Cómo migrar Jetpack Navigation 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 la interoperabilidad con frameworks de pruebas de IU, consulta la interoperabilidad con Espresso y la 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 deisEnabled
. No necesita saber siControlPanelWithToggle
existe o, incluso, cómo se puede controlar.ControlPanelWithToggle
no sabe queImageWithEnabledOverlay
existe. Puede haber cero, una o más formas en que se muestraisEnabled
yControlPanelWithToggle
no tendría que cambiar.Al elemento superior, no le importa la profundidad de las anidaciones de
ImageWithEnabledOverlay
oControlPanelWithToggle
. 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. Consulta Cómo usar clases de tamaño de ventana para obtener más información.
Además, consulta Cómo brindar compatibilidad con diferentes tamaños de pantalla 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 los rellenos predeterminados cuando tu pantalla tenga elementos View y código de Compose en la misma jerarquía. En este caso, debes ser explícito en cuál debe consumir los insertos y cuál debe ignorarlos.
Por ejemplo, si tu diseño más externo es un diseño de View de Android, debes consumir los insertos en el sistema de View y, luego, ignorarlos para Compose.
Como alternativa, si tu diseño más externo es un elemento componible, debes consumir los inserciones en Compose y rellenar los elementos componibles AndroidView
según corresponda.
De forma predeterminada, cada ComposeView
consume todos los inserciones en el nivel de consumo WindowInsetsCompat
. Para cambiar este comportamiento predeterminado, establece ComposeView.consumeWindowInsets
en false
.
Para obtener más información, consulta la documentación de WindowInsets
en Compose.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Cómo mostrar emojis
- Material Design 2 en Compose
- Inserciones de ventana en Compose