Guía de estudio 02 · Práctica 2

Arquitectura, navegación y gestión del estado en Android

Jetpack Compose, MVVM, ViewModel y Navigation

25 min de lectura Tema de la Práctica 2 Independiente del ejemplo de la práctica

Introducción

Una aplicación móvil suele tener varias pantallas que consultan y modifican información común. Un cambio realizado en un formulario puede necesitar reflejarse en un listado; una pantalla puede recrearse cuando cambia la configuración del dispositivo; y una operación puede requerir reglas de validación independientes de cómo se presentan los datos.

Para resolver estas situaciones conviene distinguir la presentación, la gestión del estado, las reglas del dominio y el acceso a los datos. En Android, Jetpack Compose permite construir interfaces declarativas; ViewModel ofrece un lugar para administrar el estado de pantalla; y Navigation Compose coordina el desplazamiento entre destinos.

Esta guía presenta los conceptos, las decisiones de diseño y los errores frecuentes asociados con esas herramientas. Los ejemplos son independientes entre sí y pueden adaptarse a aplicaciones de inventario, reservas, tareas, catálogos o formularios. El objetivo es comprender el comportamiento del código y justificar las decisiones, sin depender de una aplicación desarrollada previamente.

Resultados de aprendizaje

Al estudiar el material, se espera que puedas:

  • Distinguir las responsabilidades de UI, ViewModel, dominio y datos para organizar una aplicación.
  • Interpretar el estado observable y la recomposición para anticipar el comportamiento de una pantalla.
  • Seleccionar mecanismos de conservación del estado según su alcance y ciclo de vida.
  • Diseñar la comunicación entre composables, ViewModels y destinos de navegación para evitar dependencias innecesarias.
  • Diagnosticar errores de estado compartido, validación y navegación a partir de fragmentos de código.

Lo imprescindible para el examen

Interpretar UDF y MVVM; distinguir recomposición, ViewModel y estado guardado; explicar la navegación y la fuente única de verdad; razonar cuándo una intención de UI produce un estado o un efecto.

Se evalúa la capacidad de explicar, interpretar fragmentos y justificar decisiones. Las versiones puntuales de librerías, URLs, nombres de proyecto y comandos accesorios quedan como contexto de consulta.

Criterios transversales para interpretar esta guía

Principio transferible. Conviene distinguir la responsabilidad de una tecnología de la forma concreta en que se utilizó en la práctica. Un ejemplo de ViewModel, Retrofit, Hilt o WebSocket ilustra una solución posible; su configuración exacta no es una regla universal.

Decisión del proyecto. Los nombres, rutas, versiones, intervalos, tamaños y elecciones de librerías del caso original sirven para interpretar el código. Salvo indicación expresa, no se exige memorizarlos para el examen.

Alcance del código. Los fragmentos son recursos didácticos; pueden omitir imports, dependencias, permisos, detalles de manejo de errores o infraestructura. Comprender un fragmento no equivale a comprobar que compila como proyecto independiente.

Referencias cruzadas. La síntesis transversal de la colección reúne las distinciones entre estado y evento, persistencia, reintentos, idempotencia y límites de las pruebas.

1. Arquitectura y separación de responsabilidades

La arquitectura describe cómo se distribuyen las responsabilidades y cómo se relacionan las partes de una aplicación. Una organización frecuente en proyectos de aprendizaje distingue cuatro áreas:

Área Responsabilidad principal Ejemplo
UI Mostrar información, capturar interacciones y emitir eventos Dibujar una lista, mostrar un mensaje de error
ViewModel Preparar y exponer el estado de pantalla, atender eventos y coordinar operaciones Solicitar productos, actualizar el estado de carga
Dominio Representar conceptos y reglas del problema que pueden mantenerse independientes de Android Determinar si una cantidad solicitada es válida
Datos Obtener, almacenar y actualizar información Consultar un repositorio o una base de datos

Esta división es una forma de organizar el proyecto, no una obligación de crear cuatro carpetas en toda aplicación. La separación real se reconoce por las responsabilidades y dependencias del código.

En una arquitectura por capas tradicional puede observarse la secuencia UI → ViewModel → dominio → datos. En diseños donde se aplica inversión de dependencias, la implementación del repositorio vive en datos, mientras que su contrato puede definirse en una capa interior. Por tanto, el sentido de las dependencias depende de la arquitectura adoptada; lo relevante es evitar que las reglas centrales dependan innecesariamente de pantallas o componentes de Android.

1.1 Reglas de dominio y presentación

Supongamos que una aplicación permite solicitar productos y exige que la cantidad esté entre 1 y 20. La validación puede escribirse en Kotlin sin utilizar clases de Android:

sealed interface QuantityError {
    data object TooLow : QuantityError
    data object TooHigh : QuantityError
}

object QuantityValidator {
    fun validate(quantity: Int): QuantityError? = when {
        quantity < 1 -> QuantityError.TooLow
        quantity > 20 -> QuantityError.TooHigh
        else -> null
    }
}

La UI puede convertir el resultado en un mensaje comprensible:

fun QuantityError.toMessage(): String = when (this) {
    QuantityError.TooLow -> "La cantidad mínima es 1"
    QuantityError.TooHigh -> "La cantidad máxima es 20"
}

La distinción permite modificar los textos o traducir la interfaz sin cambiar la regla de validación. El lugar exacto de la traducción puede ser un recurso de cadenas, una función adaptadora o un componente de presentación, según el proyecto.

Criterio de análisis. Pregunta si una operación seguiría teniendo sentido aunque se reemplazara por completo la interfaz Android. Si es una regla del problema, probablemente corresponde al dominio; si define cómo mostrar algo, pertenece a presentación. Esta prueba orienta, pero no sustituye el análisis de responsabilidades.

1.2 Repositorios y fuente de verdad

Un repositorio proporciona una interfaz de acceso a los datos. La implementación puede utilizar una colección en memoria, Room, archivos o un servicio remoto. Cuando varios destinos consultan el mismo dato, es importante definir qué componente conserva su versión autorizada, conocida como fuente única de verdad.

Un ViewModel puede almacenar datos temporalmente para ejercicios pequeños. Sin embargo, si una aplicación necesita conservarlos al cerrarse o compartirlos entre distintas sesiones, se requiere una fuente persistente apropiada. Un ViewModel por sí solo no es almacenamiento permanente.

2. Estado y recomposición en Jetpack Compose

En Compose, la UI se describe mediante funciones @Composable. Cuando cambia un estado observable que un composable utiliza, Compose puede volver a ejecutar las partes necesarias de la composición para representar el nuevo valor.

@Composable
fun CounterScreen() {
    var count by remember { mutableIntStateOf(0) }

    Column {
        Text("Cantidad: $count")
        Button(onClick = { count++ }) {
            Text("Incrementar")
        }
    }
}

remember conserva count entre recomposiciones mientras su ubicación permanezca en la composición. mutableIntStateOf crea un estado observable: cuando se modifica y se lee desde la composición, Compose puede actualizar la UI correspondiente.

2.1 Estado hacia abajo, eventos hacia arriba

Para reutilizar un componente, resulta útil separar su representación de la administración del estado:

@Composable
fun CounterContent(
    count: Int,
    onIncrement: () -> Unit
) {
    Column {
        Text("Cantidad: $count")
        Button(onClick = onIncrement) {
            Text("Incrementar")
        }
    }
}

El componente recibe el valor actual y comunica la interacción mediante onIncrement. Otro componente decide cómo actualizar ese valor. Este patrón se conoce como state hoisting (elevar el estado).

Un componente así puede mostrarse en una vista previa sin construir la navegación ni un ViewModel:

@Preview(showBackground = true)
@Composable
fun CounterPreview() {
    CounterContent(count = 3, onIncrement = {})
}

La vista previa facilita inspeccionar visualmente la interfaz, pero no reemplaza las pruebas automatizadas de comportamiento.

2.2 Colecciones y observabilidad

Considera un estado que contiene una lista ordinaria:

var items by mutableStateOf(listOf("A", "B"))
    private set

fun addItem(item: String) {
    items = items + item
}

La asignación produce una nueva lista y notifica el cambio cuando corresponde a la política de igualdad del estado. En cambio, modificar internamente una MutableList ordinaria guardada en mutableStateOf no garantiza que Compose observe esa modificación.

Esto no significa que toda colección deba reemplazarse siempre. Compose también ofrece colecciones observables como mutableStateListOf, en las que operaciones como add son observables:

val items = mutableStateListOf("A", "B")
items.add("C")

La decisión depende del modelo de estado. En ViewModels se suele preferir exponer estados inmutables y controlar las modificaciones mediante métodos o eventos.

2.3 Estado derivado

Un valor derivado se obtiene de otros valores y, si es fácil de calcular, no requiere una segunda variable mutable:

data class OrderUiState(
    val quantity: Int = 1,
    val unitPrice: Double = 50.0
) {
    val total: Double get() = quantity * unitPrice
    val canConfirm: Boolean get() = quantity in 1..20
}

Si el total se almacenara también como estado independiente, una actualización incompleta podría dejarlo desfasado. Conviene calcularlo desde los datos de los que depende. Para cálculos costosos dentro de composables existen mecanismos de optimización, como derivedStateOf, pero no son necesarios para toda propiedad calculada.

3. Alcance y ciclo de vida del estado

Alcance indica quién puede acceder al estado. Tiempo de vida indica cuánto tiempo debe conservarse. Persistencia indica si puede recuperarse incluso después de terminar el proceso o cerrar la aplicación. Son decisiones diferentes.

Mecanismo Entre recomposiciones Tras rotación habitual de la Activity Tras muerte del proceso
remember Sí, mientras permanezca en la composición Generalmente no No
rememberSaveable Sí Sí, si el valor puede guardarse y restaurarse Puede restaurarse si existe estado guardado
ViewModel Sí Sí, en cambios de configuración habituales No conserva la instancia
ViewModel con SavedStateHandle Sí Sí Puede recuperar valores guardables en condiciones admitidas
Almacenamiento persistente (p. ej., Room) Independiente de la composición Sí Sí, hasta que los datos se eliminen

La restauración de rememberSaveable y SavedStateHandle depende del mecanismo de estado guardado de Android. No garantiza recuperación cuando el usuario descarta explícitamente la tarea, se eliminan los datos de la app o no existe un estado guardado válido.

3.1 Selección de mecanismo

  • Animación o expansión visual temporal: remember puede ser suficiente.
  • Texto breve de un formulario que deba recuperarse tras una recreación: rememberSaveable suele ser apropiado.
  • Estado y operaciones de una pantalla: ViewModel proporciona un alcance asociado a un ViewModelStoreOwner.
  • Datos que deben mantenerse después de cerrar la app: almacenamiento persistente.

ViewModel no sobrevive por sí mismo a la muerte del proceso; evita recrear su instancia durante cambios de configuración habituales. Tampoco se comparte automáticamente entre pantallas: su alcance depende del propietario con el que se obtiene.

4. MVVM y flujo de información

En el patrón Model-View-ViewModel, la vista presenta un estado y notifica eventos. El ViewModel procesa esos eventos, utiliza las operaciones necesarias y actualiza la información que consume la vista.

                estado
ViewModel ----------------------> UI
    ^                              |
    |           eventos            |
    +------------------------------+
    |
    +---- consulta y operaciones ----> Repositorio / dominio

4.1 Un ViewModel con estado encapsulado

El siguiente ejemplo administra elementos de una lista en memoria:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel

class TaskListViewModel : ViewModel() {
    var tasks by mutableStateOf<List<String>>(emptyList())
        private set

    fun addTask(name: String) {
        val normalized = name.trim()
        if (normalized.isBlank()) return
        tasks = tasks + normalized
    }
}

La UI puede leer tasks, pero no puede asignarla directamente. Para modificarla debe llamar a addTask. El modificador private set protege la escritura desde fuera de la clase y concentra el control de las modificaciones.

En una aplicación más completa, el ViewModel podría delegar la inserción al repositorio, recibir el resultado y exponer estados de carga o error. Para información que cambia de manera asíncrona, una implementación con StateFlow también es habitual, aunque queda fuera del ejemplo básico de esta guía.

4.2 Agrupar el estado de una pantalla

En pantallas con varios campos puede utilizarse una data class:

data class FormUiState(
    val title: String = "",
    val quantity: Int = 1
) {
    val canSubmit: Boolean get() =
        title.isNotBlank() && quantity in 1..20
}

class FormViewModel : ViewModel() {
    var uiState by mutableStateOf(FormUiState())
        private set

    fun onTitleChange(value: String) {
        uiState = uiState.copy(title = value)
    }

    fun onQuantityChange(value: Int) {
        uiState = uiState.copy(quantity = value)
    }
}

copy() permite crear un nuevo estado conservando los demás campos. canSubmit se calcula a partir de esos campos, lo que evita guardar una copia adicional de la misma información.

4.3 UI desacoplada

@Composable
fun FormContent(
    state: FormUiState,
    onTitleChange: (String) -> Unit,
    onSubmit: () -> Unit
) {
    Column {
        OutlinedTextField(
            value = state.title,
            onValueChange = onTitleChange,
            label = { Text("Título") }
        )
        Button(onClick = onSubmit, enabled = state.canSubmit) {
            Text("Guardar")
        }
    }
}

Un composable que recibe datos y callbacks puede reutilizarse, probarse y visualizarse con menor acoplamiento. Habitualmente, un composable contenedor obtiene el ViewModel y conecta el estado y los eventos con el componente de presentación.

5. Navegación entre pantallas

Navigation Compose organiza los destinos mediante un NavController y un NavHost. El controlador administra la navegación y el back stack, la pila de entradas de destinos utilizada para regresar a pantallas anteriores.

5.1 Rutas y argumentos

En proyectos que utilizan rutas basadas en cadenas puede centralizarse su definición:

object Routes {
    const val LIST = "products"
    const val DETAIL = "products/{productId}"
    const val PRODUCT_ID = "productId"

    fun detail(productId: Int): String = "products/$productId"
}

La plantilla products/{productId} se declara al registrar el destino; la ruta concreta products/7 se utiliza al navegar.

NavHost(
    navController = navController,
    startDestination = Routes.LIST
) {
    composable(Routes.LIST) {
        ProductListContent(
            onProductClick = { id ->
                navController.navigate(Routes.detail(id))
            }
        )
    }

    composable(
        route = Routes.DETAIL,
        arguments = listOf(
            navArgument(Routes.PRODUCT_ID) {
                type = NavType.IntType
            }
        )
    ) { entry ->
        val id = entry.arguments
            ?.getInt(Routes.PRODUCT_ID)
            ?: return@composable
        ProductDetailContent(productId = id)
    }
}

Este fragmento muestra el patrón de navegación; presupone que navController y los composables de ejemplo están definidos en el proyecto.

Recomendación. Pasar un identificador o argumentos mínimos y consultar la información actualizada en el destino reduce la duplicación y evita transportar copias que puedan quedar desactualizadas. Navigation admite distintos tipos de argumentos y, según la API utilizada, rutas tipadas; por eso no debe afirmarse que está prohibido pasar cualquier objeto en todos los casos. La decisión debe considerar serialización, tamaño, estabilidad y fuente de verdad.

Los tipos de los argumentos deben coincidir con la forma de obtenerlos. Un error de correspondencia puede producir un valor incorrecto o un fallo; no existe una única consecuencia garantizada.

5.2 Responsabilidad de navegar

En un diseño desacoplado, la pantalla recibe un callback como onProductClick: (Int) -> Unit, mientras que un contenedor decide la ruta. Así, la pantalla no necesita conocer directamente el NavController. El ViewModel puede comunicar que una operación terminó correctamente; el componente responsable de la navegación decide cómo reaccionar y a qué destino ir.

5.3 Back stack y destinos superiores

Opción Función habitual
launchSingleTop = true Evita agregar otra entrada del mismo destino cuando ya ocupa la parte superior de la pila
popUpTo(...) Retira entradas hasta un destino determinado, según las opciones configuradas
saveState = true Permite guardar estado de destinos retirados cuando el mecanismo lo admite
restoreState = true Intenta restaurar estado previamente guardado para el destino

En una barra de navegación inferior, estas opciones pueden colaborar para evitar pilas innecesarias y conservar el estado de pestañas. No garantizan por sí mismas el desplazamiento de una lista en cualquier implementación: también importa cómo se haya conservado ese estado. Los destinos de detalle o edición suelen considerarse secundarios respecto de los destinos principales de la barra.

6. Estado compartido entre destinos

Supongamos una aplicación con un catálogo y una pantalla de favoritos. Al marcar un producto como favorito, ambas pantallas deben representar el mismo resultado.

Si cada destino obtiene un ViewModel diferente y guarda su propia lista, las pantallas pueden mostrar estados inconsistentes. Una solución consiste en consultar una fuente común de datos. Otra, válida para un alcance limitado, consiste en obtener la misma instancia de ViewModel desde un propietario compartido.

Alcance del ViewModel Propietario posible Comportamiento esperado
Pantalla específica Entrada del destino (NavBackStackEntry) Se elimina cuando el destino se retira definitivamente y se destruye su propietario
Flujo de varias pantallas Entrada de un grafo de navegación padre Puede compartirse entre destinos de ese grafo
Alcance amplio Activity Puede compartirse entre varias pantallas mientras viva ese propietario

Llamar a viewModel() dentro de un destino no es incorrecto: es adecuado para el estado privado de esa pantalla. Lo que causa problemas es esperar que dos ViewModels con propietarios distintos compartan automáticamente información.

En una pantalla de edición, por ejemplo, el texto todavía no guardado puede pertenecer a un ViewModel local al destino, mientras que los registros guardados se consultan desde un repositorio común. Al cancelar la edición, puede descartarse el borrador; al guardar, las demás pantallas deben consultar o recibir la actualización de los datos compartidos.

Conexión transversal: alcance y decisiones

Estado, eventos y efectos: la decisión arquitectónica

Un evento de interfaz (onClick) expresa una intención; el ViewModel puede transformarla en una operación y publicar un estado actual (StateFlow<UiState>). Una navegación puntual o un mensaje efímero requieren una política diferente: pueden modelarse como evento, o como estado consumible, según las garantías requeridas. StateFlow conserva el valor más reciente; SharedFlow depende de su configuración de replay, búfer y presencia de colectores. No conviene asumir que un evento se entregará siempre.

Ejemplo Naturaleza Pregunta de diseño
onGuardar() Intención ¿Qué operación solicita el usuario?
UiState.Guardando Estado ¿Qué debe dibujarse ahora o después de recrear la pantalla?
«Registro guardado» Resultado/evento ¿Debe reaparecer tras rotar? ¿Qué pasa si nadie escucha?
Flow<List<Registro>> de Room Fuente observable persistente ¿De dónde se reconstruye la verdad de los datos?

Véanse las guías 05 (datos persistentes), 08 (callbackFlow) y 12 (SharedFlow).

7. Errores frecuentes y diagnóstico

Síntoma Posible causa Revisión recomendada
Una cifra cambia internamente, pero no en pantalla El valor no está representado como estado observable o la modificación no es observada Verificar mutableStateOf, mutableStateListOf y las lecturas en UI
Se pierde un dato al girar el dispositivo Se almacenó en remember y se recreó la composición Evaluar rememberSaveable o ViewModel, según alcance
Se pierde el estado al salir y volver a una pantalla El propietario del estado fue eliminado Determinar si debe ser local, compartido o persistente
Dos destinos muestran datos distintos Manejan copias o ViewModels independientes Identificar la fuente de verdad y el alcance del ViewModel
Un formulario permite guardar datos inválidos La validación está ausente o solo se aplica al botón Validar en la operación que guarda, además de informar desde la UI
No se encuentra el destino de navegación La ruta usada no coincide con la registrada Revisar plantilla, generador de ruta y argumentos
Un argumento llega mal o genera un fallo Tipo, nombre o valor incompatible Comparar NavType, nombre del argumento y lectura
El botón «atrás» atraviesa pantallas repetidas Navegaciones repetidas agregaron entradas al back stack Revisar launchSingleTop y estructura de navegación

Caso de análisis

Una aplicación administra una lista de tareas. La pantalla A permite agregarlas y la pantalla B muestra un resumen. La lista se guarda en un remember dentro de A. Cuando el usuario entra a B y regresa, algunas tareas desaparecen. Tras girar el dispositivo, también se pierde la información.

Para diagnosticar el problema hay que responder tres preguntas: ¿quién es el dueño del estado?, ¿cuánto tiempo debe durar?, ¿en qué componente se encuentra la fuente de verdad? Elevar el estado a un padre común puede resolver el alcance entre pantallas mientras ese padre siga en composición. Un ViewModel con el propietario apropiado puede conservarlo ante cambios de configuración. Si las tareas deben sobrevivir al cierre de la aplicación, además se requiere persistencia.

8. Vocabulario esencial

Concepto Definición
Estado observable Valor cuyos cambios pueden ser observados para actualizar la interfaz
Recomposición Nueva ejecución selectiva de composables para reflejar cambios
State hoisting Mover un estado al propietario común adecuado y pasar valores y eventos
Estado derivado Valor calculado a partir de otros datos
Fuente única de verdad Lugar autorizado desde el que se obtiene el valor vigente de un dato
UiState Representación del estado que necesita la UI; suele modelarse con una clase inmutable
ViewModelStoreOwner Propietario que determina el alcance de las instancias de ViewModel
Back stack Pila de entradas de navegación
Destino Pantalla o contenido registrado en un grafo de navegación
Repositorio Abstracción para consultar o modificar datos

9. Preguntas de autoevaluación

Comprensión

  1. ¿Cómo distinguirías una regla del dominio de una decisión de presentación? Incluye un ejemplo diferente a los de esta guía.
  2. ¿Qué significa que una fuente de datos sea la «fuente única de verdad» y qué problema evita?
  3. Explica qué necesita ocurrir para que Compose observe un cambio en la información mostrada.
  4. ¿Qué resuelve state hoisting y por qué no garantiza conservar datos después de una recreación de Activity?
  5. Compara remember, rememberSaveable y ViewModel según alcance y ciclo de vida.
  6. ¿Por qué no conviene mantener total como variable mutable si puede calcularse siempre desde quantity y unitPrice?

Interpretación de código

  1. ¿Qué riesgo presenta el siguiente código? ¿Qué dos alternativas permitirían observar correctamente los cambios?

    var products by mutableStateOf(mutableListOf("A"))
    products.add("B")
  2. ¿Qué propiedad de encapsulación se obtiene con private set en var uiState by mutableStateOf(...)?

  3. En una ruta items/{itemId} declarada con NavType.IntType, ¿qué verificarías si el destino no obtiene correctamente el identificador?

  4. ¿Qué papel cumplen las funciones callback de una pantalla como onItemClick frente al uso directo de un NavController?

Aplicación y diagnóstico

  1. Un formulario pierde el texto al girar. ¿Qué opciones tienes si únicamente necesitas conservar el borrador y cuáles si además la pantalla coordina operaciones de guardado?
  2. Una aplicación tiene pantallas de catálogo y carrito, cada una con su ViewModel. ¿Por qué podrían mostrar cantidades distintas y cómo organizarías la fuente de verdad?
  3. Una aplicación agrega el mismo destino al back stack cada vez que el usuario pulsa una pestaña. ¿Qué comportamiento observará al regresar y qué opción de navegación puede ayudar?
  4. Una regla de negocio cambia de 20 a 10 unidades máximas. ¿Dónde debería centralizarse la modificación y qué otras partes tendrías que revisar?
  5. Una aplicación debe conservar pedidos aunque el usuario cierre la app. ¿Por qué ViewModel no basta y qué componente añadirías?

10. Respuestas posibles

  1. Una regla del dominio representa una restricción o cálculo del problema; la presentación determina cómo se comunica o visualiza. Por ejemplo, «una reserva no puede tener fecha anterior a hoy» es una regla, mientras que el texto del error pertenece a presentación.
  2. Permite consultar una versión autorizada del dato y evita que pantallas con copias independientes discrepen sobre el valor actual.
  3. El composable debe leer un estado observable apropiado; cuando el estado cambia, Compose puede invalidar y recomponer el contenido que depende de él. No toda mutación de un objeto ordinario produce una notificación.
  4. Hoisting cambia el propietario y posibilita compartir el estado entre hijos. Si el propietario es un composable recreado, un remember ordinario sigue sin restaurarse automáticamente.
  5. remember conserva durante la permanencia en composición; rememberSaveable añade restauración de valores guardables bajo las condiciones de Android; ViewModel ofrece un propietario y retención durante cambios habituales de configuración, pero no conserva su instancia tras muerte del proceso.
  6. El valor calculado puede quedar inconsistente si se modifica uno de sus componentes y se olvida actualizarlo. Calcularlo desde sus dependencias reduce la duplicación del estado.
  7. Se modifica una lista mutable ordinaria sin asignar un nuevo valor al estado. Es preferible usar una lista inmutable y reemplazarla con products = products + "B", o una colección observable creada con mutableStateListOf.
  8. Las clases externas pueden consultar el estado, pero las modificaciones deben hacerse a través de los métodos permitidos por el ViewModel. Esto permite centralizar verificaciones y transformaciones.
  9. Revisaría que el nombre itemId coincida, que la ruta concreta incluya un entero válido y que la lectura corresponda al tipo declarado.
  10. Permiten que la UI notifique la intención sin conocer la navegación. El contenedor decide cómo y a dónde navegar, lo que facilita reutilización, previews y pruebas.
  11. Para un borrador simple y guardable puede usarse rememberSaveable. Si también existe estado de pantalla y operaciones coordinadas, puede utilizarse un ViewModel y, si procede, SavedStateHandle para guardar valores recuperables.
  12. Cada ViewModel podría conservar una copia independiente. Organizaría una fuente de datos compartida consultada por ambos y, si el caso lo requiere, un ViewModel con propietario común o un repositorio observable.
  13. Al retroceder recorrerá entradas repetidas. launchSingleTop puede evitar una copia adicional cuando ese destino ya está en la parte superior; popUpTo ayuda a organizar los destinos según el diseño de la barra.
  14. Centralizaría la regla en el validador de dominio. Revisaría mensajes visibles, pruebas y cualquier interfaz que muestre el límite; separar capas reduce cambios, pero no garantiza que otras partes permanezcan intactas.
  15. Un ViewModel vive en memoria y puede desaparecer al terminar el proceso. Se necesita persistencia, como una base de datos Room o un mecanismo equivalente, y un repositorio para acceder a ella.

Cierre

Una arquitectura comprensible permite identificar qué componente administra cada dato, quién puede modificarlo y durante cuánto tiempo debe conservarse. Para analizar un problema en Compose conviene distinguir la observabilidad del estado, el alcance de su propietario, el ciclo de vida y la fuente de datos autorizada. En navegación, esa misma separación ayuda a evitar copias innecesarias, dependencias directas entre pantallas y comportamientos inesperados del back stack.

Antes de la evaluación, verifica que puedes responder sin consultar el código:

  1. ¿Qué responsabilidades deben permanecer independientes de los composables?
  2. ¿Qué cambios del estado puede observar Compose y cuáles podrían pasar inadvertidos?
  3. ¿Cómo se decide entre rememberSaveable, ViewModel y persistencia?
  4. ¿Qué relación existe entre el propietario de un ViewModel y el estado compartido?
  5. ¿Cómo diagnosticarías un error de navegación sin depender del nombre de las pantallas?