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.