Em um app do Compose, a elevação do estado da IU depende da exigência da lógica da IU ou da de negócios. Este documento descreve esses dois cenários principais.
Prática recomendada
Você precisa elevar o estado da IU para o menor ancestral comum entre todos os elementos combináveis que fazem leitura e gravação. Mantenha o estado mais próximo de onde ele é consumido. No proprietário do estado, exponha aos consumidores o estado imutável e os eventos para modificar o estado.
O menor ancestral comum também pode estar fora da composição. Por exemplo,
ao elevar o estado em um ViewModel
porque há uma lógica de negócios envolvida.
Esta página explica em detalhes essa prática recomendada e as ressalvas a serem consideradas.
Tipos de estado e lógica da IU
Confira abaixo as definições dos tipos de estado e lógica de IU usados ao longo deste documento.
Estado da IU
O estado da IU é a propriedade que descreve a IU. Há dois tipos de estados da IU:
- O estado da IU da tela é o que você precisa mostrar na tela. Por exemplo, uma
classe
NewsUiState
pode conter as matérias de notícias e outras informações necessárias para renderizar a IU. Esse estado geralmente está conectado a outras camadas da hierarquia, já que ele contém dados do app. - O estado do elemento da IU se refere a propriedades intrínsecas aos elementos da IU que
influenciam a renderização. Um elemento da IU pode ser mostrado ou ocultado e
pode ter um determinado tipo, tamanho ou cor da fonte. No Android, a visualização
gerencia esse estado por conta própria, já que ela tem um estado inerente e expõe métodos de
modificação ou consulta. Um exemplo disso são os métodos
get
eset
da classeTextView
para o texto. No Jetpack Compose, o estado é externo ao elemento de composição, e você pode até elevar o estado para fora da proximidade do elemento e o enviar até a função de composição que fez a chamada ou um detentor de estado. Um exemplo disso é oScaffoldState
do elemento de composiçãoScaffold
.
Lógica
A lógica em um aplicativo pode ser de negócios ou de IU:
- A lógica de negócios é a implementação de requisitos de produtos para dados do app. Por exemplo, adicionar uma matéria aos favoritos em um app de notícias quando o usuário toca no botão. Essa lógica para salvar um favorito em um arquivo ou banco de dados geralmente é colocada nas camadas de domínio ou de dados. O detentor do estado geralmente delega essa lógica a essas camadas chamando os métodos que elas expõem.
- A lógica da IU está relacionada a como mostrar o estado da IU na tela. Por exemplo, acessar a dica correta da barra de pesquisa quando o usuário seleciona uma categoria, rolar para um determinado item em uma lista ou a lógica de navegação para uma tela específica quando o usuário clica em um botão.
Lógica da IU
Quando a lógica da IU precisa ler ou gravar o estado, é necessário definir o escopo do estado da IU, seguindo o ciclo de vida dela. Para isso, você precisa elevar o estado no nível correto em uma função combinável. Como alternativa, é possível fazer isso em uma classe simples de detentor de estado, também definida como escopo do ciclo de vida da IU.
Confira abaixo uma descrição das duas soluções e uma explicação sobre quando usar cada uma.
Elementos combináveis como proprietários do estado
Ter uma lógica da IU e um estado de elemento da IU entre os elementos combináveis é uma boa abordagem se ambos forem simples. É possível deixar o estado interno para um elemento combinável ou elevar conforme necessário.
Não é necessária elevação de estado
A elevação de estado nem sempre é obrigatória. O estado pode ser mantido em um elemento combinável quando nenhum outro elemento precisar controlá-lo. Neste snippet, há um elemento combinável que se expande e é recolhido ao ser tocado:
@Composable
fun ChatBubble(
message: Message
) {
var showDetails by rememberSaveable { mutableStateOf(false) } // Define the UI element expanded state
ClickableText(
text = message.content,
onClick = { showDetails = !showDetails } // Apply simple UI logic
)
if (showDetails) {
Text(message.timestamp)
}
}
A variável showDetails
é o estado interno desse elemento da IU. Ela só
é lida e modificada nesse combinável, e a lógica aplicada a ela é muito simples.
Portanto, elevar o estado nesse caso não traria muitos benefícios, então ele
poderia ser deixado interno. Isso faz com que esse combinável seja o proprietário e a única
fonte de verdade do estado expandido.
Elevação em elementos combináveis
Se você precisar compartilhar o estado do elemento da IU com outros elementos combináveis e aplicar a lógica dela a diferentes locais, eleve o nível na hierarquia da IU. Isso também torna os combináveis mais reutilizáveis e fáceis de testar.
O exemplo abaixo é um app de chat que implementa duas funcionalidades:
- O botão
JumpToBottom
rola a lista de mensagens para a parte de baixo. O botão executa a lógica da IU no estado da lista. - A lista
MessagesList
rola para a parte de baixo depois que o usuário envia novas mensagens. O elemento UserInput executa a lógica da IU no estado da lista.

A hierarquia de elementos combináveis é esta:

O estado LazyColumn
é elevado para a tela de conversa para que o app
possa realizar a lógica da IU e ler o estado de todos os elementos combináveis que precisam dele:

Por fim, os elementos combináveis são:

O código é este:
@Composable
private fun ConversationScreen(...) {
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState() // State hoisted to the ConversationScreen
MessagesList(messages, lazyListState) // Reuse same state in MessageList
UserInput(
onMessageSent = { // Apply UI logic to lazyListState
scope.launch {
lazyListState.scrollToItem(0)
}
},
)
}
@Composable
private fun MessagesList(
messages: List<Message>,
lazyListState: LazyListState = rememberLazyListState() // LazyListState has a default value
) {
LazyColumn(
state = lazyListState // Pass hoisted state to LazyColumn
) {
items(messages, key = { message -> message.id }) { item ->
Message(...)
}
}
JumpToBottom(onClicked = {
scope.launch {
lazyListState.scrollToItem(0) // UI logic being applied to lazyListState
}
})
}
O LazyListState
é elevado o mais alto possível para a lógica da IU que precisa ser
aplicada. Como é inicializado em uma função combinável, ele é armazenado na
composição, seguindo o ciclo de vida.
Observe que o lazyListState
é definido no método MessagesList
, com o
valor padrão de rememberLazyListState()
. Esse é um padrão comum no Compose.
Isso torna os elementos combináveis mais reutilizáveis e flexíveis. Em seguida, use o elemento combinável
em diferentes partes do app que talvez não precisem controlar o estado. Geralmente,
esse é o caso ao testar ou visualizar um elemento combinável. É assim que
a LazyColumn define o próprio estado.

Classe de detentor de estado simples como proprietária do estado
Quando uma função combinável contém uma lógica de IU complexa que envolve um ou vários campos de estado de um elemento da IU, ela precisa delegar essa responsabilidade aos detentores de estado, como uma classe simples. Isso faz com que a lógica da função combinável seja mais testável em isolamento e reduz a complexidade. Essa abordagem favorece o princípio de separação de conceitos (link em inglês): o elemento combinável é responsável por emitir elementos da IU, e o detentor do estado contém a lógica da IU e o estado de elemento da IU.
As classes simples do detentor de estado fornecem funções convenientes aos autores da chamada da função combinável, para que eles não precisem criar essa lógica por conta própria.
Essas classes simples são criadas e lembradas na composição. Como
seguem o ciclo de vida do elemento combinável, elas podem usar tipos fornecidos pela
biblioteca do Compose, como rememberNavController()
ou rememberLazyListState()
.
Um exemplo disso é a classe detentora de estado simples do LazyListState
,
implementada no Compose para controlar a complexidade da IU de LazyColumn
ou LazyRow
.
// LazyListState.kt
@Stable
class LazyListState constructor(
firstVisibleItemIndex: Int = 0,
firstVisibleItemScrollOffset: Int = 0
) : ScrollableState {
/**
* The holder class for the current scroll position.
*/
private val scrollPosition =
LazyListScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset)
suspend fun scrollToItem(...) { ... }
override suspend fun scroll() { ... }
suspend fun animateScrollToItem() { ... }
}
O LazyListState
encapsula o estado da LazyColumn
que armazena a
scrollPosition
desse elemento de IU. Ele também expõe métodos para modificar a
posição de rolagem, por exemplo, rolando até um determinado item.
Como você pode notar, incrementar as responsabilidades de um elemento combinável aumenta a necessidade de um detentor de estado. As responsabilidades podem estar na lógica da IU ou apenas na quantidade de estados para gerenciar.
Outro padrão comum é usar uma classe detentora de estado simples para lidar com a complexidade das funções combináveis raiz no app. É possível usar essa classe para encapsular o estado no nível do app, como o estado de navegação e o dimensionamento da tela. Uma descrição completa desse processo pode ser encontrada na lógica da IU e na página do detentor de estado.
Lógica de negócios
Se os elementos combináveis e as classes dos detentores de estado simples forem responsáveis pela lógica da IU e pelo estado do elemento da IU, um detentor de estado no nível da tela vai ser responsável por estas tarefas:
- Fornecer acesso à lógica de negócios do aplicativo, que normalmente é colocada em outras camadas da hierarquia, como as camadas de negócios e de dados.
- Preparar os dados do aplicativo para a apresentação em uma tela específica, que se torna o estado da IU na tela.
ViewModels como proprietários do estado
Os benefícios dos ViewModels AAC no desenvolvimento Android faz com que sejam adequados para fornecer acesso à lógica de negócios e preparar os dados do aplicativo para serem mostrados na tela.
Quando você eleva o estado da IU em ViewModel
, ele é movido para fora da
composição.

Os ViewModels não são armazenados como parte da composição. Eles são fornecidos pelo
framework e têm escopo definido como um ViewModelStoreOwner
, que pode ser uma
atividade, um fragmento, um gráfico de navegação ou um destino de um gráfico de navegação. Para
mais informações sobre escopos do ViewModel
, consulte a documentação.
O ViewModel
é a fonte da verdade e o menor ancestral comum do
estado da IU.
Estado da IU na tela
De acordo com as definições acima, o estado da IU da tela é produzido aplicando regras de
negócios. Como o detentor de estado no nível da tela é responsável por isso,
o estado da IU da tela normalmente é elevado no detentor do
estado da tela, nesse caso, um ViewModel
.
Considere o ConversationViewModel
de um app de chat e como ele expõe o estado
e os eventos da IU da tela para modificá-lo:
class ConversationViewModel(
private val channelId: String,
private val messagesRepository: MessagesRepository
) : ViewModel() {
val messages = messagesRepository
.getLatestMessages(channelId)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
// Business logic
fun sendMessage(message: Message) { /* ... */ }
}
Os elementos combináveis consomem o estado da IU da tela elevado no ViewModel
. Você precisa
injetar a instância de ViewModel
nos elementos combináveis no nível da tela para fornecer
acesso à lógica de negócios.
Confira abaixo um exemplo de ViewModel
usado em um elemento combinável da tela.
Aqui, o combinável ConversationScreen()
consome o estado da IU da tela elevado
no ViewModel
:
@Composable
private fun ConversationScreen(
conversationViewModel: ConversationViewModel = viewModel()
) {
val messages by conversationViewModel.messages.collectAsStateWithLifecycle()
ConversationScreen(messages, { message -> conversationViewModel.sendMessage(message) })
}
@Composable
private fun ConversationScreen(
messages: List<Messages>, onSendMessage: (Message) -> Unit)
) {
MessagesList(messages, onSendMessage)
// ...
}
Detalhamento de propriedade
"Detalhamento de propriedade" se refere à transmissão de dados por vários componentes filhos aninhados para o local em que são lidos.
Um exemplo típico de onde o detalhamento da propriedade pode aparecer no Compose é ao injetar o detentor de estado no nível da tela no nível superior e transmitir o estado e os eventos para elementos combináveis filhos. Isso também pode gerar uma sobrecarga de assinaturas de funções combináveis.
Embora a exposição de eventos como parâmetros lambda individuais possa sobrecarregar a assinatura da função, ela maximiza a visibilidade das responsabilidades das funções combináveis. Você pode conferir facilmente o que elas fazem.
O detalhamento de propriedade é preferível em relação à criação de classes de wrapper para encapsular estado e eventos em um só lugar, porque isso reduz a visibilidade das responsabilidades de composição. Como você não tem classes de wrapper, também é mais provável que você transmita os elementos combináveis apenas aos parâmetros necessários, o que é uma prática recomendada.
A mesma prática recomendada vai ser aplicada se esses eventos forem de navegação. Saiba mais sobre isso na documentação de navegação.
Se você identificou um problema de desempenho, também pode optar por adiar a leitura do estado. Consulte os documentos de desempenho para saber mais.
Estado do elemento da IU
Você vai poder elevar o estado do elemento da IU para o detentor de estado no nível da tela se houver uma lógica de negócios que precise ler ou gravar esse elemento.
Continuando com o exemplo de um app de chat, ele mostra sugestões de usuário em um
chat em grupo quando o usuário digita @
e uma dica. Essas sugestões são provenientes da
camada de dados, e a lógica para calcular uma lista de sugestões de usuário é considerada a
lógica de negócios. O recurso vai ser parecido com este:

O ViewModel
que implementa esse recurso fica assim:
class ConversationViewModel(...) : ViewModel() {
// Hoisted state
var inputMessage by mutableStateOf("")
private set
val suggestions: StateFlow<List<Suggestion>> =
snapshotFlow { inputMessage }
.filter { hasSocialHandleHint(it) }
.mapLatest { getHandle(it) }
.mapLatest { repository.getSuggestions(it) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun updateInput(newInput: String){
inputMessage = newInput
}
}
inputMessage
é uma variável que armazena o estado TextField
. Sempre que o
usuário digita uma nova entrada, o app chama a lógica de negócios para produzir suggestions
.
suggestions
é o estado da IU da tela e é consumido pela IU do Compose pela coleta
do StateFlow
(link em inglês).
Ressalva
Para alguns estados do elemento da IU do Compose, a elevação para o ViewModel
pode exigir
considerações especiais. Por exemplo, alguns detentores de estado de elementos da IU do Compose
expõem métodos para modificar o estado. Alguns deles podem ser funções de suspensão que
acionam animações. Essas funções de suspensão podem gerar exceções quando você as chama
de um CoroutineScope
(link em inglês) sem escopo para a
composição.
Digamos que o conteúdo da gaveta de apps seja dinâmico e que ele precise ser buscado e atualizado
na camada de dados depois que for fechado. Você precisa elevar o estado da gaveta para
ViewModel
para chamar a IU e a lógica de negócios nesse elemento
do proprietário do estado.
No entanto, chamar close()
do DrawerState
usando o
viewModelScope
da IU do Compose gera uma exceção de execução do tipo
IllegalStateException
(link em inglês) com a mensagem "um
MonotonicFrameClock
não está disponível neste
CoroutineContext”
" (link em inglês).
Para corrigir isso, use um CoroutineScope
com escopo para a composição. Ele fornece um
MonotonicFrameClock
no CoroutineContext
que é necessário para que as
funções de suspensão funcionem.
Para corrigir essa falha, troque o CoroutineContext
da corrotina no
ViewModel
para um que tenha escopo para a composição. Ele pode ser assim:
class ConversationViewModel(...) : ViewModel() {
val drawerState = DrawerState(initialValue = DrawerValue.Closed)
private val _drawerContent = MutableStateFlow<DrawerContent>(DrawerContent.Empty)
val drawerContent: StateFlow<DrawerContent> = _drawerContent.asStateFlow()
fun closeDrawer(uiScope: CoroutineScope) {
viewModelScope.launch {
withContext(uiScope.coroutineContext) { // Use instead of the default context
drawerState.close()
}
// Fetch drawer content and update state
_drawerContent.update { ... }
}
}
}
// in Compose
@Composable
private fun ConversationScreen(
conversationViewModel = viewModel()
) {
val scope = rememberCoroutineScope()
ConversationScreen(onCloseDrawer = { viewModel.closeDrawer(uiScope = scope) })
}
Saiba mais
Para saber mais sobre o estado e Jetpack Compose, consulte os recursos abaixo.