La realidad: la mayoría son apps existentes
Muy pocas apps se escriben de cero con Compose. La mayoría son proyectos existentes con View System que necesitan migrar gradualmente. La buena noticia: Compose y el View System pueden convivir en la misma app indefinidamente.
Google recomienda migrar pantalla por pantalla, empezando por las más nuevas o las que más se están desarrollando. No hace falta ni es recomendable reescribir todo de una vez.
Lo que no cambiaEl ViewModel, Room, Retrofit, Hilt, Navigation — todo sigue igual durante la migración. Solo cambia la capa de View. Esto hace que la migración sea mucho menos riesgosa de lo que parece.
AndroidView — View legacy dentro de Compose
Cuando necesitás usar una View que todavía no tiene equivalente en Compose (un mapa, un player de video, un componente custom muy específico):
@Composable
fun MapaView(latLng: LatLng, modifier: Modifier = Modifier) {
AndroidView(
modifier = modifier.fillMaxWidth().height(300.dp),
factory = { context ->
// Se ejecuta UNA SOLA VEZ para crear la View
MapView(context).apply {
onCreate(null)
getMapAsync { googleMap ->
googleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng))
}
}
},
update = { mapView ->
// Se ejecuta cuando los parámetros cambian (recomposición)
mapView.getMapAsync { googleMap ->
googleMap.moveCamera(CameraUpdateFactory.newLatLng(latLng))
}
}
)
}
// Otro ejemplo — WebView dentro de Compose
@Composable
fun WebViewComposable(url: String) {
AndroidView(
factory = { context ->
WebView(context).apply {
settings.javaScriptEnabled = true
webViewClient = WebViewClient()
}
},
update = { webView ->
webView.loadUrl(url)
}
)
}
AndroidViewBinding — ViewBinding con Compose
Si ya tenés un layout XML complejo y no querés reescribirlo todavía, podés inflarlo con ViewBinding dentro de Compose:
// Necesitás la dependencia:
// implementation("androidx.compose.ui:ui-viewbinding")
@Composable
fun ComponenteLegacy(datos: List<Dato>) {
AndroidViewBinding(MiLayoutComplejoBinding::inflate) {
// 'this' es el binding generado
recyclerView.adapter = MiAdapterLegacy(datos)
tvTitulo.text = "Componente legacy"
btnAccion.setOnClickListener { /* ... */ }
}
}
ComposeView — Compose dentro de un Fragment existente
Para migrar pantalla por pantalla sin cambiar la estructura de Fragments/Activities, podés agregar Compose en un Fragment existente:
// Opción 1: el Fragment entero pasa a ser Compose
class ProductosFragment : Fragment() {
private val viewModel: ProductosViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
// Indica a Compose que respete el ciclo de vida del Fragment
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MiAppTheme {
ProductosScreen(viewModel = viewModel)
}
}
}
}
}
// Opción 2: mezclar Views y Compose en el mismo Fragment
// fragment_productos.xml:
// <LinearLayout>
// <TextView android:id="@+id/tvHeader" /> ← View legacy
// <androidx.compose.ui.platform.ComposeView
// android:id="@+id/composeView" /> ← isla de Compose
// </LinearLayout>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.tvHeader.text = "Productos"
binding.composeView.apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MiAppTheme {
ListaProductos(productos = viewModel.productos)
}
}
}
}
setViewCompositionStrategy es obligatorioSin esta configuración, Compose sigue vivo cuando la View del Fragment es destruida (back stack), causando leaks y comportamiento inesperado. Siempre usá DisposeOnViewTreeLifecycleDestroyed en Fragments.
ViewModel compartido durante la migración
Una de las ventajas de migrar gradualmente: el ViewModel puede ser compartido entre la parte legacy y la parte Compose de la misma pantalla:
// El ViewModel no sabe si la UI es Compose o View System
@HiltViewModel
class ProductosViewModel @Inject constructor(...) : ViewModel() {
val uiState: StateFlow<ProductosUiState> = ...
}
// Fragment con View System — lee el ViewModel con lifecycleScope
class ProductosFragment : Fragment() {
private val viewModel: ProductosViewModel by viewModels()
override fun onViewCreated(...) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state -> /* actualizar Views */ }
}
}
}
}
// Mismo Fragment migrado a Compose — lee el mismo ViewModel
class ProductosFragment : Fragment() {
private val viewModel: ProductosViewModel by viewModels()
override fun onCreateView(...): View = ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MiAppTheme {
// hiltViewModel() en Compose también puede recibir el VM existente
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
ProductosContent(uiState = uiState, onAction = viewModel::onAction)
}
}
}
}
¿Cuándo migrar qué?
Una guía práctica para priorizar la migración:
- Empezá por pantallas nuevas: todo lo nuevo directo en Compose. Sin excusas.
- Luego pantallas que se están reescribiendo: si de todos modos hay que tocarla, migrala.
- Componentes reutilizables: botones custom, cards, headers — migrarlos una sola vez y usarlos en toda la app.
- Layouts simples: pantallas con poca lógica de UI son fáciles de migrar y dan confianza.
- Últimos: pantallas complejas y estables: si funciona y nadie la toca, no hay urgencia.
No migrés solo para migrarLa migración tiene un costo. Si una pantalla funciona bien, está bien testeada y no se está desarrollando activamente, migrala cuando tengas una razón concreta. El objetivo es tener una mejor app, no una app en Compose.