El modelo imperativo — cómo pensabas antes

En el View System, construís la UI en XML y luego la mutás desde Kotlin. Le decís al sistema cómo cambiar la pantalla paso a paso:

// Modelo imperativo: describís pasos, mutás objetos
binding.tvNombre.text = usuario.nombre
binding.ivAvatar.isVisible = usuario.tieneFoto
binding.ivAvatar.load(usuario.fotoUrl)
binding.btnAdmin.isVisible = usuario.esAdmin
binding.tvRol.text = if (usuario.esAdmin) "Administrador" else "Usuario"

Esto funciona, pero escala mal. Si el usuario puede cambiar (login, edición de perfil, cambio de rol), tenés que acordarte de actualizar cada propiedad en cada lugar donde el estado puede cambiar. Se te puede olvidar alguno. La UI queda inconsistente.

El modelo declarativo — cómo pensás en Compose

En Compose, describís qué debería mostrarse dado un estado. No mutás nada — cada vez que el estado cambia, la UI se redescribe desde cero a partir de ese estado:

// Modelo declarativo: describís el resultado, no los pasos
@Composable
fun PerfilUsuario(usuario: Usuario) {
    Column {
        Text(text = usuario.nombre)
        if (usuario.tieneFoto) {
            AsyncImage(model = usuario.fotoUrl, contentDescription = null)
        }
        if (usuario.esAdmin) {
            Text(text = "Administrador")
            Button(onClick = { /* ... */ }) { Text("Panel admin") }
        }
    }
}

No hay isVisible, no hay setText(), no hay mutaciones. Si usuario cambia, Compose vuelve a ejecutar esta función y la UI refleja el nuevo estado automáticamente. La UI es siempre una función del estado: UI = f(estado).

El cambio mental más importanteDejá de pensar en "actualizar la UI". Empezá a pensar en "dado este estado, ¿cómo se ve la pantalla?". Tu trabajo es describir esa función. Compose se encarga de hacer las actualizaciones mínimas necesarias.

Recomposición — el motor de Compose

Cuando el estado que lee un composable cambia, Compose vuelve a ejecutarlo. Esto se llama recomposición. Compose es inteligente: no redibuja toda la pantalla, solo los composables que leen el estado que cambió.

@Composable
fun Pantalla() {
    var contador by remember { mutableStateOf(0) }  // estado

    Column {
        // Este composable se recompone cuando contador cambia
        Text("Contador: $contador")

        // Este composable NUNCA se recompone — no lee ningún estado
        Text("Título estático")

        Button(onClick = { contador++ }) {
            Text("Incrementar")
        }
    }
}

Tres consecuencias importantes de la recomposición:

  • Los composables deben ser puros: dado el mismo input, producen la misma UI. Sin efectos secundarios dentro de la función.
  • Pueden ejecutarse en cualquier orden: no asumas que los composables se ejecutan de arriba a abajo o en un orden específico.
  • Pueden ejecutarse frecuentemente: no hagas cálculos caros directamente en un composable. Usá remember o derivá el valor en el ViewModel.

Efectos secundarios no van acáLlamar a una API, escribir en la base de datos, navegar — nada de eso va dentro del cuerpo de un composable. Para eso existen los effect handlers (LaunchedEffect, SideEffect), que veremos en la lección 05.

Setup del proyecto

// build.gradle (app) — Compose necesita estas configuraciones
android {
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.11"
    }
}

dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2024.05.00")
    implementation(composeBom)

    // BOM gestiona las versiones — no necesitás especificarlas
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose:1.9.0")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")

    debugImplementation("androidx.compose.ui:ui-tooling")
}

Compose BOMEl Bill of Materials asegura que todas las librerías de Compose sean versiones compatibles entre sí. Actualizás una sola línea (compose-bom:2024.05.00) y todas las dependencias se sincronizan.

El primer composable real

Una Activity con Compose solo necesita llamar a setContent { }:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // A partir de acá, todo es Compose
            MiAppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    PantallaInicio()
                }
            }
        }
    }
}

@Composable
fun PantallaInicio() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Hola Compose",
            style = MaterialTheme.typography.headlineMedium
        )
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { /* navegar */ }) {
            Text("Comenzar")
        }
    }
}

@Preview — el superpoder de Compose

Una de las ventajas más prácticas de Compose: podés ver cómo se ve un composable sin ejecutar la app. Solo anotá la función con @Preview:

@Preview(showBackground = true)
@Composable
fun PantallaInicioPreview() {
    MiAppTheme {
        PantallaInicio()
    }
}

// Múltiples previews para distintos estados
@Preview(showBackground = true, name = "Light")
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES, name = "Dark")
@Preview(showBackground = true, device = "spec:width=411dp,height=891dp", name = "Pixel 5")
@Composable
fun PantallaInicioMultiPreview() {
    MiAppTheme {
        PantallaInicio()
    }
}

Diseñá con PreviewsEl flujo recomendado es: escribir el composable → ver el preview → ajustar → repetir. Es mucho más rápido que compilar y deployar a un emulador para cada cambio visual.