LiveData vs StateFlow — ¿cuándo usar cada uno?

LiveData sigue siendo válida y funciona bien. Pero tiene una limitación: está atada a Android (necesita un LifecycleOwner) y no puede usarse fácilmente en capas que no deberían conocer el framework (repositorios, usecases).

StateFlow es parte de Kotlin Coroutines: es puro Kotlin, más potente, composable con otros flows, y se está convirtiendo en el estándar en proyectos modernos.

  • Usá LiveData: si el proyecto ya la usa, si estás aprendiendo o si el equipo la conoce mejor.
  • Usá StateFlow: en proyectos nuevos, si ya usás coroutines, o si querés alinearte con las guías actuales de Google.

StateFlow

StateFlow es un flow que siempre tiene un valor actual y emite actualizaciones. Es el equivalente de MutableLiveData pero basado en coroutines:

// Antes (LiveData):
private val _contador = MutableLiveData(0)
val contador: LiveData<Int> = _contador

// Ahora (StateFlow):
private val _contador = MutableStateFlow(0)
val contador: StateFlow<Int> = _contador.asStateFlow()

Para actualizar el valor desde el ViewModel:

// Desde el hilo principal:
_contador.value = _contador.value + 1

// Dentro de una corrutina (con update para thread-safety):
_contador.update { it + 1 }

Patrón UiState sellado

En lugar de tener múltiples LiveData/StateFlows separados para loading, error y datos, el patrón moderno agrupa todo el estado de la pantalla en una sealed class:

// El estado posible de la pantalla de productos
sealed class ProductosUiState {
    object Loading : ProductosUiState()
    data class Success(val productos: List<Producto>) : ProductosUiState()
    data class Error(val mensaje: String) : ProductosUiState()
}

// En el ViewModel: un solo StateFlow para todo el estado
private val _uiState = MutableStateFlow<ProductosUiState>(ProductosUiState.Loading)
val uiState: StateFlow<ProductosUiState> = _uiState.asStateFlow()

¿Por qué sealed class?El compilador sabe todos los subtipos posibles, así que el when es exhaustivo. Si agregás un nuevo estado y olvidás manejarlo en la UI, el compilador te avisa. No hay posibilidad de estados inconsistentes (loading + error al mismo tiempo).

ViewModel completo con StateFlow y coroutines

class ProductosViewModel(
    private val repository: ProductoRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow<ProductosUiState>(ProductosUiState.Loading)
    val uiState: StateFlow<ProductosUiState> = _uiState.asStateFlow()

    init {
        cargarProductos()
    }

    fun cargarProductos() {
        viewModelScope.launch {
            _uiState.value = ProductosUiState.Loading
            try {
                val productos = repository.obtenerProductos() // suspending
                _uiState.value = ProductosUiState.Success(productos)
            } catch (e: Exception) {
                _uiState.value = ProductosUiState.Error(e.message ?: "Error desconocido")
            }
        }
    }
}

viewModelScope es un CoroutineScope ligado al ciclo de vida del ViewModel: cuando el ViewModel se destruye, cancela todas sus corrutinas automáticamente.

Collectar StateFlow en el Fragment

Para observar un Flow desde la UI de forma lifecycle-safe, usás repeatOnLifecycle:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.uiState.collect { state ->
                when (state) {
                    is ProductosUiState.Loading -> {
                        binding.progressBar.isVisible = true
                        binding.recyclerView.isVisible = false
                        binding.tvError.isVisible = false
                    }
                    is ProductosUiState.Success -> {
                        binding.progressBar.isVisible = false
                        binding.recyclerView.isVisible = true
                        adapter.submitList(state.productos)
                    }
                    is ProductosUiState.Error -> {
                        binding.progressBar.isVisible = false
                        binding.tvError.isVisible = true
                        binding.tvError.text = state.mensaje
                    }
                }
            }
        }
    }
}

repeatOnLifecycle es obligatorioSin repeatOnLifecycle, el flow sigue colectando aunque el Fragment esté en background (onStop), procesando actualizaciones innecesarias y potencialmente crasheando. repeatOnLifecycle(STARTED) pausa la colección cuando el Fragment va a onStop y la reanuda en onStart.

SharedFlow para eventos únicos

StateFlow siempre replay el último valor al nuevo colector. Para eventos que no deben repetirse (navegar a otra pantalla, mostrar un Snackbar), usás SharedFlow:

// En el ViewModel:
private val _eventos = MutableSharedFlow<UiEvento>()
val eventos: SharedFlow<UiEvento> = _eventos.asSharedFlow()

sealed class UiEvento {
    data class NavVegarADetalle(val id: Int) : UiEvento()
    data class MostrarError(val mensaje: String) : UiEvento()
}

fun guardarProducto(producto: Producto) {
    viewModelScope.launch {
        repository.guardar(producto)
        _eventos.emit(UiEvento.NavVegarADetalle(producto.id))
    }
}

// En el Fragment:
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.eventos.collect { evento ->
            when (evento) {
                is UiEvento.NavVegarADetalle ->
                    findNavController().navigate(
                        HomeFragmentDirections.actionHomeToDetalle(evento.id)
                    )
                is UiEvento.MostrarError ->
                    Snackbar.make(binding.root, evento.mensaje, Snackbar.LENGTH_LONG).show()
            }
        }
    }
}