As IUs modernas raramente são estáticas. O estado da interface muda quando o usuário interage com a interface ou quando o app precisa exibir novos dados.
Este documento descreve diretrizes para a produção e o gerenciamento do estado da interface. No final, você vai:
- saber quais APIs precisam ser usadas para produzir o estado da interface. Isso depende da natureza das origens de mudança de estado disponíveis nos detentores de estado, seguindo os princípios do fluxo de dados unidirecional;
- saber como definir o escopo da produção do estado da interface para considerar os recursos do sistema;
- saber como expor o estado da interface para consumo pela interface.
Basicamente, a produção de estado é a aplicação incremental dessas mudanças ao estado da interface. O estado sempre existe e muda como resultado de eventos. As diferenças entre eventos e estados estão resumidas na tabela abaixo:
Eventos | Estado |
---|---|
Transitório, imprevisível e existe por um período finito. | Sempre existe. |
As entradas da produção de estado. | A saída da produção de estado. |
O produto da interface ou de outras origens. | Consumido pela interface. |
Uma ótima mnemônica que resume o que foi dito acima é o estado é; os eventos acontecem. O diagrama abaixo ajuda a visualizar as mudanças de estado à medida que os eventos ocorrem em uma linha do tempo. Cada evento é processado pelo detentor de estado adequado e resulta em uma mudança de estado:
Confira quais podem ser a origem dos eventos:
- Usuários: à medida que interagem com a interface do app.
- Outras origens de mudança de estado: APIs que apresentam dados de apps da interface, do domínio ou de camadas de dados, como eventos de tempo limite da snackbar, casos de uso ou repositórios, respectivamente.
O pipeline de produção do estado da interface
A produção de estado nos apps Android pode ser considerada um pipeline de processamento com estas características:
- Entradas: as origens de mudança de estado. Alguns exemplos:
- Locais para a camada da interface: podem ser eventos do usuário, como a inserção de um
título para uma "tarefa" em um app gerenciador de tarefas ou APIs que fornecem
acesso à lógica da interface que gera mudanças no estado da interface. Por exemplo,
chamar o método
open
noDrawerState
no Jetpack Compose. - Externas à camada da interface: são origens do domínio ou das camadas
de dados que causam mudanças no estado da interface. Por exemplo, notícias que acabaram
de carregar em um
NewsRepository
ou outros eventos. - Uma mistura de todas as opções acima.
- Locais para a camada da interface: podem ser eventos do usuário, como a inserção de um
título para uma "tarefa" em um app gerenciador de tarefas ou APIs que fornecem
acesso à lógica da interface que gera mudanças no estado da interface. Por exemplo,
chamar o método
- Detentores de estado: tipos que aplicam a lógica de negócios e/ou a lógica da interface a origens de mudança de estado e processam eventos de usuário para produzir o estado da interface.
- Saída: o estado da interface que o app pode renderizar para fornecer aos usuários as informações de que eles precisam.
APIs de produção de estado
Há duas APIs principais usadas na produção do estado, dependendo do estágio em que você está no pipeline:
Estágio do pipeline | API |
---|---|
Entrada | Use APIs assíncronas para realizar o trabalho fora da linha de execução de interface para manter a interface sem instabilidade. Por exemplo, corrotinas ou fluxos em Kotlin e RxJava ou callbacks na linguagem de programação Java. |
Saída | Use APIs do detentor de dados observáveis para invalidar e renderizar novamente a interface quando o estado mudar. Por exemplo, StateFlow, Compose State ou LiveData. Os detentores de dados observáveis garantem que a interface sempre tenha um estado para mostrar na tela |
Das duas, a escolha da API assíncrona para entrada tem mais influência sobre a natureza do pipeline de produção de estado do que a escolha da API observável para saída. Isso ocorre porque as entradas ditam o tipo de processamento que pode ser aplicado ao pipeline.
Montagem do pipeline de produção de estado
As próximas seções abordam as técnicas de produção de estado mais adequadas para várias entradas e as APIs de saída correspondentes. Cada pipeline de produção de estado é uma combinação de entradas e saídas e precisa ter estas características:
- Conhecimento do ciclo de vida: quando a interface não está visível ou ativa, o pipeline de produção do estado não consome nenhum recurso, a menos que explicitamente necessário.
- Fácil de consumir: a interface precisa renderizar facilmente o estado produzido pela interface. As considerações da saída do pipeline de produção de estado variam em diferentes APIs de visualização, como o sistema de visualização ou o Jetpack Compose.
Entradas em pipelines de produção de estado
As entradas em um pipeline de produção de estado podem fornecer as origens de mudança de estado por meio de:
- Operações únicas que podem ser síncronas ou assíncronas. Por exemplo,
chamadas para funções
suspend
. - APIs de streaming. Por exemplo,
Flows
. - Todas as alternativas acima.
Nas seções a seguir, abordamos como montar um pipeline de produção de estado para cada uma das entradas acima.
APIs únicas como origens de mudança de estado
Use a API MutableStateFlow
como um contêiner de estado observável
e mutável. Em apps do Jetpack Compose, você também pode considerar
mutableStateOf
, principalmente ao trabalhar com
APIs de texto do Compose. As duas APIs oferecem métodos que permitem
atualizações atômicas seguras nos valores que hospedam, independentemente das atualizações serem
síncronas ou assíncronas.
Por exemplo, pense em atualizações de estado em um app simples para jogar dados. Cada jogada do
dado do usuário invoca o método síncrono
Random.nextInt()
, e o resultado é gravado no
estado da interface.
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 do 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
}
}
Como mudar o estado da interface em chamadas assíncronas
Para mudanças de estado que exigem um resultado assíncrono, inicie uma corrotina no
CoroutineScope
apropriado. Isso permite que o app descarte o trabalho quando o
CoroutineScope
é cancelado. O detentor do estado grava o resultado da
chamada do método de suspensão na API observável usada para expor o estado da interface.
Por exemplo, considere o AddEditTaskViewModel
no
Exemplo de arquitetura. Quando o método saveTask()
de suspensão
salva uma tarefa de forma assíncrona, o método update
no
MutableStateFlow propaga a mudança de estado para o estado da interface.
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 do 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))
}
}
}
}
Como mudar o estado da interface em linhas de execução em segundo plano
É preferível iniciar corrotinas no agente principal para a produção
do estado da interface. Ou seja, fora do bloco withContext
nos snippets de código abaixo. No entanto, se você precisar atualizar o estado da interface em um contexto de segundo plano
diferente, use as APIs a seguir:
- Use o método
withContext
para executar corrotinas em um contexto simultâneo diferente. - Ao usar
MutableStateFlow
, use o métodoupdate
normalmente. - Ao usar o estado do Compose, use o
Snapshot.withMutableSnapshot
para garantir atualizações atômicas no estado no contexto simultâneo.
Por exemplo, no snippet DiceRollViewModel
abaixo, suponha que
SlowRandom.nextInt()
seja uma função suspend
de uso intensivo de computação
que precisa ser chamada em uma corrotina vinculada à 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 do 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
}
}
}
}
}
APIs de fluxo como origens de mudança de estado
Para origens de mudança de estado que produzem vários valores ao longo do tempo em fluxos, agregar as saídas de todas as origens em um todo coeso é uma abordagem simples para a produção do estado.
Ao usar fluxos do Kotlin, é possível fazer isso usando combine. função. Confira um exemplo disso no InterestsViewModel do código do "Now in Android" (link em inglês):
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
)
}
O uso do operador stateIn
para criar StateFlows
proporciona à interface um controle mais refinado
sobre a atividade do pipeline de produção de estado, já que pode ser necessário
que ele esteja ativo apenas quando a interface estiver visível.
- Use
SharingStarted.WhileSubscribed()
se o pipeline só precisar ficar ativo quando a interface estiver visível ao coletar o fluxo de uma forma que reconhece o ciclo de vida. - Use
SharingStarted.Lazily
se o pipeline precisar ficar ativo desde que o usuário possa retornar à interface, ou seja, a interface esteja na backstack ou em outra guia fora da tela.
Quando a agregação de origens de estado baseadas em fluxo não se aplica, as APIs de fluxo, como fluxos do Kotlin, oferecem um conjunto avançado de transformações, por exemplo, fusão e nivelamento (links em inglês) para ajudar no processamento dos fluxos no estado da interface.
APIs únicas e de fluxo como origens de mudança de estado
Quando o pipeline de produção de estado depende de chamadas únicas e de fluxos como origens de mudança de estado, os fluxos são a restrição definidora. Portanto, converta as chamadas únicas em APIs de fluxo ou canalize a saída em fluxos e retome o processamento conforme descrito na seção de fluxos acima.
Com fluxos, isso geralmente significa criar uma ou mais instâncias de
MutableStateFlow
de apoio particulares para propagar mudanças de estado. Também é possível
criar fluxos de snapshot no estado do Compose.
Considere o TaskDetailViewModel
no
repositório architecture-samples (link em inglês) abaixo:
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 do 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 saída em pipelines de produção de estado
A escolha da API de saída para o estado da interface e a natureza da apresentação dependem muito da API usada pelo app para renderizar a interface. Em apps Android, você pode usar visualizações ou o Jetpack Compose. Inclui as seguintes considerações:
- Estado de leitura de acordo com o ciclo de vida.
- O estado precisa ser exposto em um ou vários campos no detentor de estado?
A tabela a seguir resume as APIs a serem usadas para o pipeline de produção do estado para qualquer entrada e consumidor:
Entrada | Consumidor | Saída |
---|---|---|
APIs únicas | Visualizações | StateFlow ou LiveData |
APIs únicas | Compose | StateFlow ou State do Compose |
APIs de fluxo | Visualizações | StateFlow ou LiveData |
APIs de fluxo | Compose | StateFlow |
APIs únicas e de fluxo | Visualizações | StateFlow ou LiveData |
APIs únicas e de fluxo | Compose | StateFlow |
Inicialização do pipeline de produção do estado
A inicialização de pipelines de produção do estado envolve definir as condições iniciais
para que o pipeline seja executado. Isso pode incluir o fornecimento de valores de entrada essenciais para a inicialização do pipeline. Por exemplo, um id
para a
visualização detalhada de uma matéria ou o início de um carregamento assíncrono.
Quando possível, inicialize o pipeline de produção de estado lentamente
para economizar recursos do sistema.
Na prática, isso significa aguardar até que haja um consumidor da
saída. As APIs Flow
permitem isso com o
Argumento started
em stateIn
. Nos casos em que isso não é aplicável,
definir um objeto idempotente
Função initialize()
para iniciar explicitamente o pipeline de produção do estado
conforme mostrado no snippet a seguir:
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
}
}
}
Exemplos
Os exemplos do Google a seguir demonstram como ocorre a produção de estado na camada de interface. Acesse-os para conferir a orientação na prática:
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Camada de interface
- Criar um app que prioriza o modo off-line
- Detentores de estado e estado da interface {:#mad-arch}