Arquitectura, navegación y gestión del estado en Android
Jetpack Compose, MVVM, ViewModel y Navigation
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:
rememberpuede ser suficiente. - Texto breve de un formulario que deba recuperarse tras una recreación:
rememberSaveablesuele ser apropiado. - Estado y operaciones de una pantalla:
ViewModelproporciona un alcance asociado a unViewModelStoreOwner. - 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
- ¿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.
- ¿Qué significa que una fuente de datos sea la «fuente única de verdad» y qué problema evita?
- Explica qué necesita ocurrir para que Compose observe un cambio en la información mostrada.
- ¿Qué resuelve state hoisting y por qué no garantiza conservar datos después de una recreación de Activity?
- Compara
remember,rememberSaveableyViewModelsegún alcance y ciclo de vida. - ¿Por qué no conviene mantener
totalcomo variable mutable si puede calcularse siempre desdequantityyunitPrice?
Interpretación de código
-
¿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") -
¿Qué propiedad de encapsulación se obtiene con
private setenvar uiState by mutableStateOf(...)? -
En una ruta
items/{itemId}declarada conNavType.IntType, ¿qué verificarías si el destino no obtiene correctamente el identificador? -
¿Qué papel cumplen las funciones callback de una pantalla como
onItemClickfrente al uso directo de unNavController?
Aplicación y diagnóstico
- 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?
- 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?
- 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?
- 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?
- Una aplicación debe conservar pedidos aunque el usuario cierre la app. ¿Por qué
ViewModelno basta y qué componente añadirías?
10. Respuestas posibles
- 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.
- Permite consultar una versión autorizada del dato y evita que pantallas con copias independientes discrepen sobre el valor actual.
- 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.
- Hoisting cambia el propietario y posibilita compartir el estado entre hijos. Si el propietario es un composable recreado, un
rememberordinario sigue sin restaurarse automáticamente. rememberconserva durante la permanencia en composición;rememberSaveableañade restauración de valores guardables bajo las condiciones de Android;ViewModelofrece un propietario y retención durante cambios habituales de configuración, pero no conserva su instancia tras muerte del proceso.- 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.
- 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 conmutableStateListOf. - 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.
- Revisaría que el nombre
itemIdcoincida, que la ruta concreta incluya un entero válido y que la lectura corresponda al tipo declarado. - 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.
- 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,SavedStateHandlepara guardar valores recuperables. - 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.
- Al retroceder recorrerá entradas repetidas.
launchSingleToppuede evitar una copia adicional cuando ese destino ya está en la parte superior;popUpToayuda a organizar los destinos según el diseño de la barra. - 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.
- 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:
- ¿Qué responsabilidades deben permanecer independientes de los composables?
- ¿Qué cambios del estado puede observar Compose y cuáles podrían pasar inadvertidos?
- ¿Cómo se decide entre
rememberSaveable,ViewModely persistencia? - ¿Qué relación existe entre el propietario de un ViewModel y el estado compartido?
- ¿Cómo diagnosticarías un error de navegación sin depender del nombre de las pantallas?