wandres.dev
EL VIEWMODEL · el hogar del estado

Qué resuelve el ViewModel: sobrevivir a la rotación

El ViewModel no es una capa de presentación ni un patrón arquitectónico: es la respuesta de ingeniería a un problema muy concreto de Android, el de que la Activity es un objeto desechable que el sistema destruye y reconstruye ante el gesto más trivial del usuario. Esta lección desmonta el problema —qué es un cambio de configuración, por qué el framework recrea la pantalla y qué se pierde en el camino—, expone el mecanismo real de retención con ViewModelStore, ViewModelStoreOwner y ViewModelProvider, y define con precisión el ciclo de vida del ViewModel: desde su creación perezosa hasta onCleared. Distingue además lo que sí resuelve de lo que no, para que no le pidas garantías que nunca prometió.

⏱ 16 min

Todo lo que has construido hasta aquí —el estado inmutable, el bucle intent-reduce-render, el container de Orbit— descansa sobre un supuesto silencioso: que existe algún lugar donde ese estado puede vivir más tiempo que la pantalla que lo muestra. En Android ese lugar no es obvio, porque la Activity es una de las entidades más frágiles del sistema: el propio framework la aniquila y la reconstruye ante el gesto más trivial del usuario. El ViewModel no nació como una capa de arquitectura limpia ni como el vértice de un diagrama; nació como la solución a esa fragilidad concreta. Entenderlo bien exige mirar tres cosas: qué se rompe exactamente, con qué mecanismo se repara y cuál es su ciclo de vida real, que no coincide con el de ninguna vista.

🎯 Al terminar esta lección sabrás
  • Entender por qué un cambio de configuración destruye la Activity y qué se pierde con ella.
  • Conocer el mecanismo real de retención: ViewModelStore, ViewModelStoreOwner y ViewModelProvider.
  • Situar el ciclo de vida del ViewModel entre su creación perezosa y onCleared.
  • Distinguir lo que el ViewModel sí resuelve de lo que nunca prometió resolver.

La Activity es desechable, tu estado no

En Android, la Activity y el Fragment no son objetos que tú gobiernas: son objetos que el sistema crea, destruye y vuelve a crear cuando le conviene. El detonante más famoso es la rotación, pero la lista es mucho más larga: pasar a modo oscuro, cambiar el idioma del dispositivo, entrar en pantalla dividida, redimensionar una ventana en un plegable o en escritorio, conectar un teclado físico, ajustar el tamaño de fuente del sistema. Todos son cambios de configuración, y la respuesta por defecto del framework ante cualquiera de ellos es idéntica: destruir la instancia viva y construir una nueva.

Esa política, que parece brutal, es en realidad coherente. En Android los recursos se resuelven por calificadores: hay un layout para vertical y otro para apaisado, unos values para español y otros para inglés, un drawable para tema claro y otro para oscuro. Cuando la configuración cambia, casi todo lo que la pantalla había resuelto —dimensiones, cadenas, imágenes, densidades— deja de ser válido. En lugar de intentar parchear cada referencia una por una, el sistema toma el camino simple y honesto: tira la pantalla entera y la vuelve a resolver desde cero con la configuración nueva.

Existe una vía de escape aparente y conviene desactivarla cuanto antes: declarar en el manifiesto que tu Activity maneja por su cuenta ciertos cambios de configuración. La instancia deja de destruirse, sí, pero a cambio te haces responsable de volver a resolver a mano cada recurso afectado, y esa lista crece con cada versión del sistema —densidades nuevas, plegables, ventanas redimensionables—. Además no soluciona el problema de fondo: no impide que el sistema mate tu proceso ni que la pantalla se reconstruya por cualquier otra razón. Es una optimización puntual para casos muy concretos, nunca una arquitectura.

El coste de esa simplicidad lo pagas tú. Cada objeto que colgaba de la Activity muere con ella. Si el resultado de una llamada de red vivía en un campo de la Activity, la instancia nueva arranca vacía: vuelve a mostrar el indicador de carga, vuelve a pedir los mismos datos, vuelve a gastar batería y ancho de banda, y en el peor de los casos vuelve a ejecutar una operación que no era idempotente. El usuario, que solo giró el teléfono, ve cómo su pantalla se reinicia. Ese es el bug fundacional que el ViewModel vino a matar.

⚠️
Por que el Bundle no basta

El primer remedio histórico fue onSaveInstanceState con un Bundle. Funciona, pero es un canal estrecho y caro: todo lo que metes se serializa a un Parcel en el hilo principal y viaja por Binder hasta el proceso del sistema, donde hay un presupuesto compartido del orden de un megabyte para toda la transacción. Guardar ahí una lista de resultados o un objeto grande te acerca a una TransactionTooLargeException y penaliza cada transición. El Bundle sirve para llevar identificadores y migas de navegación, no para custodiar el grafo de objetos de tu pantalla.

El mecanismo real de retención

Conviene desmitificarlo: el ViewModel no sobrevive por magia del compilador ni por un truco del recolector de basura. Sobrevive porque hay una caja que el sistema traspasa a mano de la instancia vieja a la nueva. Esa caja es el ViewModelStore: un mapa de claves a instancias de ViewModel. Quien la posee es un ViewModelStoreOwner —tu Activity, tu Fragment, una entrada del grafo de navegación—, y quien la consulta es el ViewModelProvider, que ante una petición devuelve la instancia existente si la clave ya está ocupada y solo llama a la fábrica si no lo está.

// La delegación esconde tres pasos: buscar el owner, buscar la clave, crear si falta.
class PerfilActivity : AppCompatActivity() {
    private val viewModel: PerfilViewModel by viewModels()
}

// Equivalente explícito, sin azúcar sintáctico:
val viewModel = ViewModelProvider(this)[PerfilViewModel::class.java]

El paso decisivo ocurre durante el cambio de configuración: antes de morir, la Activity entrega su ViewModelStore al mecanismo de instancias retenidas del framework, y la Activity recién nacida lo recoge y lo adopta como propio. Por eso la segunda llamada a viewModels() no construye nada: encuentra la clave ocupada y devuelve exactamente el mismo objeto en memoria. No hay serialización, no hay copia, no hay coste: es la misma referencia atravesando la frontera entre dos instancias de pantalla.

Falta una pieza en el circuito: quién construye el objeto la primera vez. De eso se encarga una fábrica, y por defecto la fábrica solo sabe invocar el constructor vacío. En cuanto tu ViewModel necesita colaboradores —un repositorio, un caso de uso, el propio SavedStateHandle— hay que darle una fábrica que sepa proveerlos. Puedes escribirla a mano o delegarla en el grafo de inyección de dependencias, que es lo habitual en un proyecto real.

// Fábrica explícita: útil para entender qué hace la anotación por debajo
class PerfilFactory(private val repo: PerfilRepo) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(clase: Class<T>): T =
        PerfilViewModel(repo) as T
}

// En la práctica, el grafo de inyección genera esa fábrica por ti
@HiltViewModel
class PerfilViewModel @Inject constructor(private val repo: PerfilRepo) : ViewModel()

Ese reparto importa más de lo que parece: la fábrica se consulta una sola vez por ciclo de vida del dueño. Las dependencias que inyectas se resuelven en la creación y se conservan intactas a través de todas las rotaciones posteriores, porque no se vuelve a crear nada. Es otra forma de decir lo mismo que el ViewModelStore: lo que hay dentro del objeto retenido queda retenido con él.

De ahí se sigue una consecuencia que mucha gente descubre tarde: el alcance del ViewModel es el alcance de su dueño. No hay un solo tipo de ViewModel, hay tantos ámbitos como dueños posibles, y elegir el dueño equivocado produce o bien estado que no se comparte cuando debería, o bien estado que sobrevive cuando ya debería haber muerto.

🤖

Ámbito de Activity

Con viewModels() en la Activity o activityViewModels() en un Fragment. Vive mientras viva la Activity: ideal para estado compartido entre varios fragmentos de una misma pantalla.

🧩

Ámbito de Fragment

Con viewModels() dentro del Fragment. Muere cuando el fragmento se destruye de verdad, no cuando solo se destruye su vista: dos cosas que se confunden a menudo.

🧭

Ámbito de grafo de navegación

Con una entrada del back stack como dueña. Permite que un flujo de varias pantallas comparta un ViewModel que se limpia solo al abandonar el flujo entero.

flowchart TD
A[Usuario gira el dispositivo] --> B[La Activity recibe onDestroy]
B --> C[El ViewModelStore se conserva]
B --> D[Vistas y binding se destruyen]
C --> E[La Activity nueva pide el mismo ViewModel]
D --> F[Se reconstruye la jerarquia de vistas]
E --> G[Mismo objeto, mismo estado, sin recargar]
F --> G
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111b
style G fill:#89b4fa,color:#11111b

El ciclo de vida real: hasta onCleared

Con el mecanismo en la mano, el ciclo de vida se vuelve preciso. Un ViewModel nace de forma perezosa la primera vez que alguien lo pide a su ViewModelProvider. A partir de ahí atraviesa impasible todos los cambios de configuración de su dueño: puede ver nacer y morir tres, diez o cincuenta instancias de Activity sin enterarse, porque nunca supo de su existencia. Y muere una sola vez: cuando su dueño desaparece definitivamente —la Activity termina de verdad, el fragmento se retira, la entrada del grafo sale del back stack— el framework limpia el ViewModelStore, lo que cancela su scope de corrutinas e invoca onCleared.

Ese onCleared es el único gancho de destrucción que tienes, y su contrato es estrecho: se llama en el hilo principal, sin garantías de que la vista siga existiendo, y sirve para soltar recursos que el ViewModel posea de forma explícita —un oyente registrado a mano, una conexión, un Closeable—. Todo lo que cuelgue de su scope de corrutinas ya se cancela solo, como verás en la lección siguiente.

class SensorViewModel(private val sensores: SensorHub) : ViewModel() {

    private val oyente = SensorListener { valor -> /* ... */ }

    init { sensores.registrar(oyente) }

    override fun onCleared() {
        sensores.quitar(oyente)   // lo que registraste a mano, lo retiras a mano
    }
}

Importa el orden en que ocurren las cosas: el framework cancela primero el scope de corrutinas y después invoca onCleared. Eso significa que dentro de ese método no puedes lanzar trabajo suspendido en el scope del propio ViewModel, porque ya está muerto y cualquier corrutina que intentes arrancar allí terminará de inmediato. Si necesitas una limpieza asíncrona, tiene que apoyarse en un ámbito de mayor duración que no dependa de la pantalla que acaba de desaparecer.

💡
Distingue morir de dejar de verse

Un ViewModel no se limpia porque su pantalla deje de estar visible. Si navegas hacia adelante y la pantalla queda en el back stack, su dueño sigue vivo y el ViewModel también: conserva el estado para cuando el usuario vuelva. Tampoco se limpia cuando la app pasa a segundo plano. La regla exacta es que se limpia cuando su dueño se destruye de forma definitiva, no cuando se oculta. Confundir invisibilidad con destrucción es la raíz de la mitad de las preguntas sobre por qué un ViewModel conserva datos viejos.

Hay un corolario que gobernará la última lección de este nivel y conviene anticiparlo aquí, porque se deduce de lo anterior sin esfuerzo. Si el ViewModel vive más que cualquier vista, entonces guardar dentro de él una referencia a una vista, a un Fragment, a un objeto de enlace de vistas o a un Context de Activity impide que el recolector libere esa jerarquía muerta. No es una mala práctica estilística: es la consecuencia aritmética de que un objeto largo sujete a uno corto.

Las fronteras: lo que nunca prometió

Igual de importante es marcar el perímetro de lo que no resuelve, porque casi todas las decepciones con el ViewModel vienen de pedirle garantías que jamás ofreció. Un objeto retenido en memoria depende de que la memoria siga existiendo, y esa condición no está en tus manos.

💀

No sobrevive al proceso

Si el sistema mata la app en segundo plano para recuperar memoria, el ViewModelStore desaparece con ella. Ese hueco lo cubre SavedStateHandle, en la tercera lección.

💾

No es persistencia

Nada de lo que guarde estará ahí mañana ni tras reinstalar. Los datos que deben durar entre sesiones viven en base de datos o en almacenamiento en disco.

🧵

No es un gestor de tareas

El trabajo que debe completarse aunque el usuario abandone la pantalla —subir una foto, sincronizar— no le pertenece: necesita un ámbito de mayor duración.

Estas tres exclusiones no son defectos: son la contrapartida exacta de su virtud. El ViewModel es barato, rápido y sin coste de serialización precisamente porque vive en memoria y no negocia con nadie. Cada una de las cosas que no hace la hace mejor otro mecanismo con otro precio: el Bundle paga serialización a cambio de cruzar la muerte del proceso, la base de datos paga entrada y salida a cambio de durar para siempre. Un diseño sano combina los tres en lugar de estirar uno hasta romperlo.

El ViewModel no es una capa, es un tiempo de vida

Aquí está la idea que reordena todo lo demás: el ViewModel no se define por lo que contiene, sino por cuánto dura. Los diagramas de arquitectura lo dibujan como una capa entre la vista y el dominio, y esa imagen, siendo cómoda, es engañosa, porque sugiere que su esencia es su posición. Su esencia es su longevidad. Android te ofrece exactamente tres duraciones y el arte consiste en saber qué corresponde a cada una. La vista dura lo que dura una configuración: milisegundos entre dos rotaciones, y por eso jamás debe custodiar nada que no sepas regenerar en un instante. El proceso dura lo que el sistema tolere: minutos u horas, y ahí viven las cachés y los grafos de inyección. El disco dura hasta que el usuario desinstale. El ViewModel ocupa una cuarta franja, deliberadamente extraña, que ninguna de las otras cubría: dura lo que dura una intención del usuario. Mientras alguien esté en el flujo de editar su perfil, el objeto existe; cuando abandona ese flujo, muere. Ni antes ni después. Cuando internalizas eso, la pregunta de si algo va en el ViewModel deja de ser estética y se vuelve empírica: ¿este dato debe seguir vivo mientras el usuario siga en esta intención, y desaparecer sin dejar rastro cuando la abandone? Si la respuesta es sí, ahí va. Si debe sobrevivir a que el sistema mate el proceso, no basta el ViewModel. Y si puedes recalcularlo trivialmente al pintar, no merece ocupar esa franja. Todas las discusiones sobre qué es responsabilidad del ViewModel se disuelven cuando dejas de preguntarte dónde está en el diagrama y empiezas a preguntarte cuánto tiene que durar.

⚔️ Mide la frontera de la retencion
  1. Enumera cinco cambios de configuración distintos de la rotación que destruyan una Activity, y explica por qué el framework prefiere recrearla a parchearla.
  2. Describe con tus palabras el recorrido del ViewModelStore durante una rotación: quién lo guarda, quién lo recoge y por qué no hay serialización de por medio.
  3. Para una pantalla de detalle abierta desde una lista, decide si su ViewModel debe tener ámbito de fragmento, de actividad o de grafo de navegación, y justifica el impacto de elegir mal.
  4. Explica por qué onCleared no se invoca al pasar la app a segundo plano ni al navegar hacia adelante, y qué evento sí lo dispara.
  5. Clasifica cinco datos de una pantalla real en las cuatro duraciones —vista, ViewModel, proceso y disco— y defiende cada colocación.