Las IUs modernas casi nunca son estáticas. El estado de la IU cambia cuando el usuario interactúa con ella o cuando la app necesita mostrar datos nuevos.
En este documento, se describen los lineamientos para la producción y administración del estado de la IU. Al final, podrás:
- Conocer las APIs que debes usar para producir el estado de la IU. Esto depende de la naturaleza de las fuentes de cambio de estado disponibles en tus contenedores de estado, en función de los principios del flujo unidireccional de datos.
- Conocer cómo debes definir el alcance de la producción del estado de la IU para que sea consciente de los recursos del sistema.
- Conocer cómo debes exponer el estado de la IU para que lo consuma la IU.
En esencia, la producción de estado es la aplicación incremental de estos cambios en el estado de la IU. El estado siempre existe y cambia como resultado de los eventos. Las diferencias entre los eventos y el estado se resumen en la siguiente tabla:
Eventos | Estado |
---|---|
Es transitoria, impredecible y existe durante un período limitado | Siempre existe |
Las entradas de producción del estado | Resultado de la producción del estado |
Es el producto de la IU o de otras fuentes | La consume la IU |
Un gran valor mnemotécnico que resume lo anterior es estado es; eventos ocurren. El siguiente diagrama ayuda a visualizar los cambios de estado a medida que ocurren los eventos en un cronograma. El contenedor de estado adecuado procesa cada evento y genera un cambio de estado:
Los eventos pueden provenir de las siguientes fuentes:
- Usuarios: Cuando interactúan con la IU de la app.
- Otras fuentes de cambio de estado: APIs que presentan datos de app desde la IU, el dominio o capas de datos, como eventos de tiempo de espera de la barra de notificaciones, casos de uso o repositorios, respectivamente.
La canalización de la producción del estado de la IU
La producción de estado en apps para Android se puede considerar como una canalización de procesamiento que comprende lo siguiente:
- Entradas: Las fuentes del cambio de estado. Pueden ser las siguientes:
- Locales en la capa de la IU: Pueden ser eventos de usuarios, como un usuario que ingresa un título para una tarea pendiente en una app de administración de tareas o APIs que proporcionan acceso a la lógica de la IU que generan cambios en el estado de la IU. Por ejemplo, una llamada al método
open
enDrawerState
en Jetpack Compose. - Externa a la capa de la IU: Estas son fuentes del dominio o capas de datos que causan cambios en el estado de la IU. Por ejemplo, noticias que terminaron de cargarse desde un
NewsRepository
o demás eventos. - Una combinación de todo lo anterior.
- Locales en la capa de la IU: Pueden ser eventos de usuarios, como un usuario que ingresa un título para una tarea pendiente en una app de administración de tareas o APIs que proporcionan acceso a la lógica de la IU que generan cambios en el estado de la IU. Por ejemplo, una llamada al método
- Contenedores de estado: Tipos que aplican la lógica empresarial o la lógica de la IU a las fuentes de cambio de estado y procesamiento de eventos de usuario para producir el estado de la IU.
- Salidas: El estado de la IU que la app puede renderizar para brindar a los usuarios la información que necesitan.
APIs de producción de estado
Hay dos APIs principales que se usan en la producción de estados según la etapa de la canalización en la que te encuentres:
Etapa de canalización | API |
---|---|
Entrada | Debes usar APIs asíncronas para realizar trabajos fuera del subproceso de IU a fin de mantener libre el bloqueo de la IU. Por ejemplo, corrutinas o flujos en Kotlin, y RxJava o devoluciones de llamada en el lenguaje de programación Java. |
Salida | Debes usar las APIs de retención de datos observables para invalidar y volver a representar la IU cuando cambia el estado. Por ejemplo, StateFlow, Compose State o LiveData. Los contenedores de datos observables garantizan que la IU siempre tenga un estado de IU para mostrar en la pantalla |
De las dos opciones, la elección de una API asíncrona para la entrada tiene una mayor influencia en la naturaleza de la canalización de producción que el estado que la elección de la API observable para la salida. Esto se debe a que las entradas dictan el tipo de procesamiento que se puede aplicar a la canalización.
Organización de canalizaciones de producción de estado
En las siguientes secciones, se abordan las técnicas de producción de estado más adecuadas para varias entradas y las APIs de salida que coinciden. Cada canalización de producción de estado es una combinación de entradas y salidas, y debe tener las siguientes características:
- Ser compatible con el ciclo de vida: En el caso de que la IU no esté visible o activa, la canalización de producción del estado no debería consumir ningún recurso, a menos que se solicite de manera explícita.
- Ser fácil de consumir: La IU debe poder renderizar con facilidad el estado de IU producido. Las consideraciones para la salida de la canalización de producción de estado variarán según las diferentes APIs de View, como el sistema de View o Jetpack Compose.
Entradas en canalizaciones de producción de estado
Las entradas de una canalización de producción de estado pueden proporcionar sus fuentes de cambio de estado mediante lo siguiente:
- Operaciones únicas que pueden ser síncronas o asíncronas, por ejemplo, llamadas a funciones
suspend
- APIs de transmisión, por ejemplo,
Flows
- Todas las opciones anteriores
En las siguientes secciones, se explica cómo puedes organizar una canalización de producción de estado para cada una de las entradas anteriores.
APIs de operaciones únicas como fuentes de cambio de estado
Usa la API de MutableStateFlow
como contenedor de estado observable y mutable. En las apps de Jetpack Compose, también puedes considerar mutableStateOf
, en especial cuando trabajas con APIs de texto de Compose. Ambas APIs ofrecen métodos que permiten actualizaciones atómicas seguras en los valores que alojan, sin importar si las actualizaciones son síncronas o asíncronas.
Por ejemplo, considera las actualizaciones de estado en una app de dados simple. Cada lanzamiento del dado del usuario invoca el método Random.nextInt()
síncrono y el resultado se escribe en el estado de la IU.
StateFlow
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Estado de Compose
@Stable
interface DiceUiState {
val firstDieValue: Int?
val secondDieValue: Int?
val numberOfRolls: Int?
}
private class MutableDiceUiState: DiceUiState {
override var firstDieValue: Int? by mutableStateOf(null)
override var secondDieValue: Int? by mutableStateOf(null)
override var numberOfRolls: Int by mutableStateOf(0)
}
class DiceRollViewModel : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
_uiState.firstDieValue = Random.nextInt(from = 1, until = 7)
_uiState.secondDieValue = Random.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
Mutación del estado de la IU a partir de llamadas asíncronas
En el caso de los cambios de estado que requieren un resultado asíncrono, inicia una corrutina en el CoroutineScope
correspondiente. Esto permite que la app descarte el trabajo cuando se cancela CoroutineScope
. Luego, el contenedor de estado escribe el resultado de la llamada de método de suspensión en la API observable que se usó para exponer el estado de la IU.
Por ejemplo, considera AddEditTaskViewModel
en la muestra de arquitectura. Cuando el método saveTask()
de suspensión guarda una tarea de forma asíncrona, el método update
en MutableStateFlow propaga el cambio de estado al estado de la IU.
StateFlow
data class AddEditTaskUiState(
val title: String = "",
val description: String = "",
val isTaskCompleted: Boolean = false,
val isLoading: Boolean = false,
val userMessage: String? = null,
val isTaskSaved: Boolean = false
)
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableStateFlow(AddEditTaskUiState())
val uiState: StateFlow<AddEditTaskUiState> = _uiState.asStateFlow()
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.update {
it.copy(isTaskSaved = true)
}
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.update {
it.copy(userMessage = getErrorMessage(exception))
}
}
}
}
}
Estado de Compose
@Stable
interface AddEditTaskUiState {
val title: String
val description: String
val isTaskCompleted: Boolean
val isLoading: Boolean
val userMessage: String?
val isTaskSaved: Boolean
}
private class MutableAddEditTaskUiState : AddEditTaskUiState() {
override var title: String by mutableStateOf("")
override var description: String by mutableStateOf("")
override var isTaskCompleted: Boolean by mutableStateOf(false)
override var isLoading: Boolean by mutableStateOf(false)
override var userMessage: String? by mutableStateOf<String?>(null)
override var isTaskSaved: Boolean by mutableStateOf(false)
}
class AddEditTaskViewModel(...) : ViewModel() {
private val _uiState = MutableAddEditTaskUiState()
val uiState: AddEditTaskUiState = _uiState
private fun createNewTask() {
viewModelScope.launch {
val newTask = Task(uiState.value.title, uiState.value.description)
try {
tasksRepository.saveTask(newTask)
// Write data into the UI state.
_uiState.isTaskSaved = true
}
catch(cancellationException: CancellationException) {
throw cancellationException
}
catch(exception: Exception) {
_uiState.userMessage = getErrorMessage(exception))
}
}
}
}
Mutación del estado de la IU a partir de subprocesos en segundo plano
Se prefiere iniciar las corrutinas en el despachador principal para la producción de estado de la IU. Es decir, fuera del bloque withContext
en los siguientes fragmentos de código. Sin embargo, si necesitas actualizar el estado de la IU en un contexto en segundo plano diferente, puedes hacerlo mediante las siguientes APIs:
- Usa el método
withContext
para ejecutar corrutinas en un contexto simultáneo diferente. - Cuando uses
MutableStateFlow
, usa el métodoupdate
como de costumbre. - Cuando uses el estado de Compose, usa
Snapshot.withMutableSnapshot
para garantizar las actualizaciones atómicas del estado en el contexto simultáneo.
Por ejemplo, supongamos que, en el fragmento DiceRollViewModel
a continuación, SlowRandom.nextInt()
es una función suspend
de procesamiento intensivo a la que se debe llamar desde una corrutina vinculada a la CPU.
StateFlow
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = SlowRandom.nextInt(from = 1, until = 7),
secondDieValue = SlowRandom.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
}
}
Estado de Compose
class DiceRollViewModel(
private val defaultDispatcher: CoroutineScope = Dispatchers.Default
) : ViewModel() {
private val _uiState = MutableDiceUiState()
val uiState: DiceUiState = _uiState
// Called from the UI
fun rollDice() {
viewModelScope.launch() {
// Other Coroutines that may be called from the current context
…
withContext(defaultDispatcher) {
Snapshot.withMutableSnapshot {
_uiState.firstDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.secondDieValue = SlowRandom.nextInt(from = 1, until = 7)
_uiState.numberOfRolls = _uiState.numberOfRolls + 1
}
}
}
}
}
Transmite APIs como fuentes de cambio de estado
En el caso de las fuentes de cambio de estado que producen varios valores a lo largo del tiempo en las transmisiones, agregar los resultados de todas las fuentes a un todo coherente es un enfoque sencillo para la producción de estado.
Cuando usas flujos de Kotlin, puedes lograrlo con Combine . Puedes ver un ejemplo de esto en "Ahora en Android", en la sección InterestsViewModel:
class InterestsViewModel(
authorsRepository: AuthorsRepository,
topicsRepository: TopicsRepository
) : ViewModel() {
val uiState = combine(
authorsRepository.getAuthorsStream(),
topicsRepository.getTopicsStream(),
) { availableAuthors, availableTopics ->
InterestsUiState.Interests(
authors = availableAuthors,
topics = availableTopics
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = InterestsUiState.Loading
)
}
El uso del operador stateIn
para crear StateFlows
proporciona a la IU un control más preciso sobre la actividad de la canalización de producción de estado, ya que puede ser necesario que esté activo solo cuando la IU es visible.
- Usa
SharingStarted.WhileSubscribed()
si la canalización solo debe estar activa cuando la IU sea visible mientras se recopila el flujo de manera optimizada para los ciclos de vida. - Usa
SharingStarted.Lazily
si la canalización debe estar activa mientras el usuario pueda volver a la IU, es decir, si está en la pila de actividades o en otra pestaña fuera de la pantalla.
En los casos en que no se aplica la agregación de fuentes de estado basadas en transmisiones, las APIs de transmisión, como los flujos de Kotlin, ofrecen un amplio conjunto de transformaciones, como combinación y compactación, para procesar las transmisiones en el estado de la IU.
APIs de transmisión y de operación única como fuentes de cambio de estado
En los casos en que la canalización de producción de estado depende de llamadas únicas y transmisiones como fuentes de cambio de estado, las transmisiones son la restricción definida. Por lo tanto, convierte las llamadas únicas en las APIs de transmisión o canaliza sus resultados en transmisiones y reanuda el procesamiento, como se describe en la sección de transmisiones anterior.
Por lo general, con los flujos, esto significa crear una o más instancias privadas de MutableStateFlow
de copia de seguridad para propagar los cambios de estado. También puedes crear flujos de instantáneas desde el estado de Compose.
Considera el TaskDetailViewModel
del repositorio architecture-samples que aparece a continuación:
StateFlow
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _isTaskDeleted = MutableStateFlow(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
_isTaskDeleted,
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted.update { true }
}
}
Estado de Compose
class TaskDetailViewModel @Inject constructor(
private val tasksRepository: TasksRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private var _isTaskDeleted by mutableStateOf(false)
private val _task = tasksRepository.getTaskStream(taskId)
val uiState: StateFlow<TaskDetailUiState> = combine(
snapshotFlow { _isTaskDeleted },
_task
) { isTaskDeleted, task ->
TaskDetailUiState(
task = taskAsync.data,
isTaskDeleted = isTaskDeleted
)
}
// Convert the result to the appropriate observable API for the UI
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = TaskDetailUiState()
)
fun deleteTask() = viewModelScope.launch {
tasksRepository.deleteTask(taskId)
_isTaskDeleted = true
}
}
Tipos de salida en canalizaciones de producción de estado
La elección de la API de salida para el estado de la IU y la naturaleza de su presentación dependen en gran medida de la API que tu app usa para procesar la IU. En las apps para Android, puedes elegir usar Views o Jetpack Compose. Se debe considerar lo siguiente:
- La lectura del estado de forma consciente del ciclo de vida
- Si el estado debe exponerse en uno o varios campos del contenedor de estado
En la siguiente tabla, se resume qué API usar para la canalización de producción del estado para cualquier entrada y consumidor:
Entrada | Consumidor | Salida |
---|---|---|
APIs de operación única | Vistas | StateFlow o LiveData |
APIs de operación única | Compose | StateFlow o Compose State |
APIs de transmisión | Vistas | StateFlow o LiveData |
APIs de transmisión | Compose | StateFlow |
APIs de transmisión y operación única | Vistas | StateFlow o LiveData |
APIs de transmisión y operación única | Compose | StateFlow |
Inicialización de la canalización de producción del estado
La inicialización de las canalizaciones de producción del estado implica configurar las condiciones iniciales para que se ejecute la canalización. Esto puede implicar proporcionar valores de entrada iniciales que son críticos para el inicio de la canalización (por ejemplo, un id
para la vista de detalles de un artículo de noticias) o para iniciar una carga asíncrona.
Debes inicializar la canalización de producción de estado de forma diferida cuando sea posible para conservar los recursos del sistema.
Esto suele significar que debes esperar hasta que haya un consumidor del resultado. Las APIs de Flow
lo permiten con la
El argumento started
en stateIn
. En los casos en que esto no sea aplicable,
definir un modelo idempotente
Función initialize()
para iniciar de forma explícita la canalización de producción del estado
como se muestra en el siguiente fragmento:
class MyViewModel : ViewModel() {
private var initializeCalled = false
// This function is idempotent provided it is only called from the UI thread.
@MainThread
fun initialize() {
if(initializeCalled) return
initializeCalled = true
viewModelScope.launch {
// seed the state production pipeline
}
}
}
Ejemplos
En los siguientes ejemplos de Google, se demuestra la producción de estado en la capa de la IU. Explóralos para ver esta guía en práctica:
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Capa de la IU
- Cómo compilar una app que prioriza el uso sin conexión
- Contenedores de estado y estado de la IU {:#mad-arch}