Saltar a contenido

Jetpack Compose: IU declarativa moderna en Android

Jetpack Compose es el kit de herramientas moderno para compilar interfaces de usuario nativas en Android.
Es la manera recomendada hoy en día para desarrollar apps desde cero o migrar proyectos existentes al nuevo paradigma.

De la IU imperativa a la declarativa

Históricamente, Android representaba la IU como un árbol de vistas (Views) definido en XML.
Cada widget (ej. Button, TextView, ImageView) mantenía un estado interno mutable y se actualizaba con métodos imperativos como:

val button: Button = findViewById(R.id.btnAceptar)
button.setText("Aceptar")
container.addView(newChildView)
imageView.setImageBitmap(bitmap)

Problemas del enfoque imperativo

  • Complejidad en el mantenimiento: múltiples llamadas a setText(), addChild(), etc. aumentan el riesgo de inconsistencias.
  • Estados ilegales: actualizaciones sobre vistas eliminadas o en conflicto.
  • Mayor carga cognitiva: el programador debe recordar siempre qué vista actualizar y en qué momento.

A medida que crece la app, también crece la probabilidad de errores y de comportamientos inesperados.

El modelo declarativo

En los últimos años, la industria migró hacia modelos de IU declarativa (ej. React, SwiftUI, Flutter).
Compose sigue esta línea: en lugar de modificar widgets, se describe la interfaz en función de un estado actual.

  • La IU se regenera conceptualmente desde cero cada vez que cambia el estado.
  • Compose decide de manera inteligente qué elementos volver a dibujar gracias al proceso de recomposición.
  • Esto evita la necesidad de modificar vistas manualmente y reduce el riesgo de errores.

Más información: Conceptos básicos de Compose

Composición y recomposición

En Compose, los widgets son funciones componibles (@Composable) en lugar de objetos con estado mutable.

@Composable
fun Greeting(name: String) {
    Text(text = "Hola, $name")
}
  • La función Greeting("Ginés") describe la UI en función del estado (name).
  • Si cambia el estado (name = "María"), Compose llama de nuevo a la función → la IU se redibuja automáticamente.

Proceso de recomposición

Proceso de recomposición

  1. El usuario interactúa (ej. onClick).
  2. Se notifica a la lógica de la app (ej. un ViewModel).
  3. Cambia el estado observable.
  4. Se ejecutan de nuevo las funciones @Composable con los nuevos datos.
  5. Solo las partes afectadas de la UI se redibujan → eficiencia en CPU, batería y memoria.

Documentación: Recomposición en Compose

Estado y arquitectura

En el enfoque declarativo:

  • El estado de la app se gestiona con patrones arquitectónicos recomendados (ej. MVVM).
  • El ViewModel proporciona los datos observables (LiveData, StateFlow).
  • Las funciones @Composable se limitan a transformar estado → UI.

Esto permite separar claramente: - Lógica de negocio → ViewModel.
- Representación visual → funciones componibles.

Guía oficial: Arquitectura de apps en Android

Interoperabilidad con Views

Interoperabilidad con Views

Compose está diseñado para convivir con el sistema de vistas tradicional:

  • Se puede incrustar Compose dentro de un ViewGroup existente.
  • También se pueden añadir Views dentro de Compose mediante AndroidView.

Esto facilita migraciones progresivas:
puedes modernizar solo ciertas pantallas sin reescribir toda la aplicación.

Referencia: Integrar Compose con Views

Ventajas clave de Jetpack Compose

  • Menos código y más legible.
  • Reducción de errores por estado inconsistente.
  • Mejor soporte para arquitecturas modernas (MVVM, unidireccional).
  • Actualizaciones reactivas y eficientes gracias a la recomposición.
  • Migración gradual gracias a su interoperabilidad.

Actividades

  • AC 205 (RA1 / CE1a / IC1 / 3p) Compara la construcción de interfaces en Android con enfoque imperativo (Views + XML) y declarativo (Jetpack Compose). Observa, documenta y reflexiona sobre diferencias de estructura, estado y actualización de la UI. Para ello:

    Imperativa (XML + Views)

    1. Estructura

      • Crea un proyecto “Empty Activity”.
      • En activity_main.xml, añade:
        <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="16dp">
        
            <TextView
                android:id="@+id/txtSaludo"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hola, mundo (Views)"
                android:textSize="20sp" />
        
            <Button
                android:id="@+id/btnCambiar"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Cambiar texto" />
        </LinearLayout>
        
    2. Interacción

      • En MainActivity.kt, localiza las vistas y actualiza el texto al pulsar:

        val txt = findViewById<TextView>(R.id.txtSaludo)
        val btn = findViewById<Button>(R.id.btnCambiar)
        
        btn.setOnClickListener {
            txt.text = "Texto actualizado (Views)"
        }
        
    3. Observa

      • ¿Cómo “encuentras” las vistas?
      • ¿Dónde se guarda el estado (el texto actual)?
      • ¿Qué pasa si giras el dispositivo (rotación)?

    Declarativa (Jetpack Compose)

    1. Estructura: Crea proyecto “Empty Activity (Compose)” o añade Compose al actual.

      • En MainActivity.kt:

        setContent {
            EjemploCompose()
        }
        
      • Crea la función componible:

        @Composable
        fun EjemploCompose() {
            var texto by remember { mutableStateOf("Hola, mundo (Compose)") }
        
            Column(modifier = Modifier.padding(16.dp)) {
                Text(text = texto, fontSize = 20.sp)
                Spacer(Modifier.height(8.dp))
                Button(onClick = { texto = "Texto actualizado (Compose)" }) {
                    Text("Cambiar texto")
                }
            }
        }
        
    2. Observa

      • ¿Dónde vive el estado (texto)?
      • ¿Cómo se actualiza la UI? (recomposición)
      • ¿Qué pasa con la rotación si no guardas el estado (pista: rememberSaveable)?

    Comparativa guiada (responde en 1–2 líneas por punto)

    1. Declaración de UI: XML + findViewById vs. funciones @Composable.
    2. Actualización: setText()/mutaciones vs. asignación de estado y recomposición.
    3. Estado: ¿en qué variable/objeto vive? ¿quién lo controla?
    4. Rotación: ¿qué se pierde y cómo lo conservarías en cada enfoque?
    5. Escalabilidad: ¿qué enfoque te parece más mantenible a medida que crece la app? ¿por qué?
  • AC 206 (RA1 / CE1a / IC1 / 3p) Explora la estructura mínima de un proyecto con Jetpack Compose. Para ello haz un documento que contenga los siguientes pasos:

    1. Crea un proyecto nuevo en Android Studio → Empty Activity (Compose).
    2. Abre MainActivity.kt y localiza:

      • La clase MainActivity.
      • La llamada a setContent { ... }.
      • La función @Composable por defecto (ej. Greeting()).
      • La función @Preview.
    3. Haz un esquema o mapa (a mano o digital) con estas relaciones:

      • ¿Qué arranca primero al ejecutar la app?
      • ¿Qué función muestra realmente la UI?
      • ¿Para qué sirve @Preview?
  • AC 207 (RA1 / CE1a / IC1 / 3p) Crea y prueba tu primera función @Composable. Realiza los siguientes pasos y documéntalo:

    1. Crea un proyecto, y codifica una nueva función @Composable llamada SaludoPersonal().
    2. Añade un @Preview para verla en el Design Preview de Android Studio.
    3. Modifica el texto 2–3 veces y comprueba cómo cambia en tiempo real.
    4. Explicación (3–4 líneas): ¿qué ventajas tiene trabajar con previews?