El problema que resuelven
Antes de ViewModel y LiveData, el código de UI (Fragment/Activity) mezclaba tres responsabilidades distintas: mostrar datos, reaccionar a eventos del usuario, y obtener/transformar datos. El resultado era clases enormes, difíciles de testear y propensas a crashes por el ciclo de vida.
La solución es la separación de responsabilidades:
- ViewModel: tiene los datos y la lógica. Sobrevive a recreaciones de la Activity/Fragment.
- LiveData: un contenedor observable que notifica a la UI cuando los datos cambian, y que es consciente del ciclo de vida (no llama a Fragments detenidos).
- Fragment/Activity: solo observa datos y reacciona. No sabe de dónde vienen.
ViewModel
// build.gradle (app)
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0")
class ProductosViewModel : ViewModel() {
// Estado privado (mutable)
private val _productos = MutableLiveData<List<Producto>>()
// Estado público (solo lectura para la UI)
val productos: LiveData<List<Producto>> = _productos
private val _isLoading = MutableLiveData(false)
val isLoading: LiveData<Boolean> = _isLoading
init {
cargarProductos()
}
private fun cargarProductos() {
_isLoading.value = true
// Aquí irías al repositorio, a la base de datos, etc.
val lista = listOf(Producto(1, "Tablet"), Producto(2, "Phone"))
_productos.value = lista
_isLoading.value = false
}
fun eliminarProducto(id: Int) {
_productos.value = _productos.value?.filter { it.id != id }
}
}
Patrón backing propertyEl patrón _productos / productos (privado mutable / público inmutable) es el estándar. Exponer MutableLiveData directamente al Fragment le daría a la UI la capacidad de modificar datos, violando la separación de responsabilidades.
LiveData y el ciclo de vida
LiveData es lifecycle-aware: cuando observás un LiveData desde un Fragment usando viewLifecycleOwner, las notificaciones se detienen automáticamente cuando el Fragment va a onStop y se reanudan en onStart. Nunca vas a recibir una actualización cuando tu Fragment no está visible.
Esto elimina una clase entera de bugs: el clásico crash de intentar actualizar la UI de un Fragment que ya fue destruido.
MutableLiveData — value vs postValue
// .value — solo desde el hilo principal (main thread)
_productos.value = nuevaLista
// .postValue — desde cualquier hilo (background thread)
// útil cuando el resultado viene de una corrutina en IO dispatcher
_productos.postValue(nuevaLista)
Observar LiveData en el Fragment
class ProductosFragment : Fragment(R.layout.fragment_productos) {
// viewModels() crea el ViewModel ligado al Fragment
private val viewModel: ProductosViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// SIEMPRE usar viewLifecycleOwner, nunca 'this'
viewModel.productos.observe(viewLifecycleOwner) { lista ->
adapter.submitList(lista)
}
viewModel.isLoading.observe(viewLifecycleOwner) { loading ->
binding.progressBar.isVisible = loading
}
binding.btnEliminar.setOnClickListener {
viewModel.eliminarProducto(selectedId)
}
}
}
viewLifecycleOwner, no thisEn un Fragment, siempre pasá viewLifecycleOwner como owner al observar LiveData. Si usás this (el Fragment como LifecycleOwner), podés recibir notificaciones cuando la View ya fue destruida pero el Fragment sigue en el back stack. Esto causa crashes.
ViewModel compartido entre Fragments
Cuando dos Fragments necesitan compartir datos (ej: un Fragment lista y uno detalle en la misma Activity), usás un ViewModel ligado a la Activity:
// Fragment A — produce datos
class ListaFragment : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
fun onItemSelected(item: Item) {
sharedViewModel.seleccionarItem(item)
}
}
// Fragment B — consume datos
class DetalleFragment : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.itemSeleccionado.observe(viewLifecycleOwner) { item ->
binding.tvNombre.text = item.nombre
}
}
}
ViewModelFactory — inyectar dependencias
Por defecto, el ViewModel debe tener un constructor sin parámetros. Si necesitás pasarle dependencias (un repositorio, un ID), usás una Factory:
class ProductosViewModel(private val repo: ProductoRepository) : ViewModel() {
// ...
}
class ProductosViewModelFactory(
private val repo: ProductoRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProductosViewModel(repo) as T
}
}
// En el Fragment:
private val viewModel: ProductosViewModel by viewModels {
ProductosViewModelFactory(ProductoRepository())
}
Hilt simplifica estoCon el framework de inyección de dependencias Hilt, no necesitás escribir Factories manualmente. Hilt las genera automáticamente. Vale la pena aprenderlo cuando tengas claro el patrón base.