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. Ele tem como objetivo ajudar você a entender o seguinte:
- Quais APIs usar 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.
- Como definir o escopo da produção do estado da interface para considerar os recursos do sistema.
- 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:
Os eventos podem vir de:
- Usuários: à medida que interagem com a interface do app.
- Outras origens de mudança de estado: APIs que apresentam dados do app 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 IU
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. Elas podem ser:
- 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
openemDrawerStateno 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 terminaram de carregar de um
NewsRepositoryou outros eventos. - Uma mistura das 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 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, como corrotinas e fluxos, para realizar o trabalho fora da linha de execução de interface para manter a interface sem instabilidade. |
| Saída | Use APIs do detentor de dados observáveis, como o estado do Compose ou o StateFlow, para invalidar e renderizar novamente a interface quando o estado mudar. Os detentores de dados observáveis garantem que a interface sempre tenha um estado para mostrar na tela. |
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. No Jetpack Compose, o consumo de estado é fundamental para a interface, porque os elementos combináveis podem ser atualizados com base em mudanças de estado.
Entradas em pipelines de produção de estado
As entradas em um pipeline de produção de estado fornecem 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
Gerencie o estado com detentores de dados observáveis. Use a mutableStateOf API,
principalmente ao trabalhar com APIs de texto do Compose. Para um gerenciamento de estado mais complexo
ou ao integrar com outros componentes arquitetônicos, use a
MutableStateFlow API. 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 de
dados do usuário invoca o método síncrono Random.nextInt,
e o resultado é gravado no estado da interface.
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
}
}
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,
)
}
}
}
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 de suspensão saveTask salva uma tarefa
de forma assíncrona, o método update no MutableStateFlow propaga a
mudança de estado para o estado da interface.
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))
}
}
}
}
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))
}
}
}
}
}
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, faça o seguinte:
- Use o método
withContextpara executar corrotinas em um contexto simultâneo diferente. - Ao usar
MutableStateFlow, use oupdatemétodo normalmente. - Ao usar o estado do Compose, use o
Snapshot.withMutableSnapshotmétodo para garantir atualizações atômicas no estado no contexto simultâneo.
Por exemplo, suponha que, no snippet DiceRollViewModel abaixo, SlowRandom.nextInt seja uma função suspend de uso intensivo de computação que precisa ser chamada em uma corrotina vinculada à CPU.
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
}
}
}
}
}
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,
)
}
}
}
}
}
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 com a função combinar.
Confira um exemplo disso no "Now in Android" do código do
InterestsViewModel (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.WhileSubscribedse o pipeline precisar ficar ativo apenas quando a interface estiver visível ao coletar o fluxo de uma forma que reconhece o ciclo de vida. - Use
SharingStarted.Lazilyse 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, nivelamento e assim por diante 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 architecture-samples
repositório. O estado da interface depende de um fluxo para a tarefa atual (_task) e uma origem única (_isTaskDeleted) que é atualizada quando a tarefa é excluída. Essa flag é necessária para diferenciar quando uma tarefa não é encontrada no banco de dados devido a um ID incorreto e quando ela não é encontrada porque o usuário acabou de excluí-la:
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, taskAsync ->
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
}
}
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, taskAsync ->
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 }
}
}
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, como o Compose. O Jetpack Compose é o kit de ferramentas moderno recomendado para criar IUs nativas. Inclui as seguintes considerações:
- Estado de leitura de acordo com o ciclo de vida.
- Se 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 ao usar o Jetpack Compose:
| Entrada | Saída |
|---|---|
| APIs únicas | StateFlow ou State do Compose |
| APIs de fluxo | StateFlow |
| APIs únicas e de fluxo | 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. Flow APIs permitem fazer isso com o argumento started
no método stateIn. Nos casos em que isso não é aplicável, defina
uma função idempotente initialize para iniciar explicitamente o pipeline de produção de 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
}
}
}
Amostras
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:
Outros recursos
Para mais informações sobre o estado da interface, consulte estes recursos adicionais:
Documentação
Conteúdo de visualizações
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}