State Hoisting é um padrão no Jetpack Compose onde o estado é movido para fora de uma função Composable filha para a função pai, e a filha recebe dados através de parâmetros e notifica sobre mudanças via callbacks. Esta é uma implementação do princípio de fluxo de dados unidirecional (UDF), onde o estado sobe e os eventos descem. De acordo com Google Android Developers, 2026, State Hoisting torna os componentes reutilizáveis, testáveis e previsíveis.
Principais pontos
State Hoisting é um padrão onde uma função Composable não possui o estado, mas o recebe de fora. Em vez de var dentro da função, dois parâmetros são usados: um valor para exibição e um callback lambda para lidar com mudanças. Tecnicamente, isso significa que o componente filho se torna stateless (sem estado próprio), enquanto o pai é stateful (possui o estado).
Exemplo: o componente TextField do Material3 não armazena o texto inserido internamente. Ele aceita value: String e onValueChange: (String) -> Unit. O pai que chama TextField declara var value by remember { mutableStateOf("") } e passa value e onValueChange. Isso é State Hoisting clássico: TextField é um componente burro (apenas exibe e reporta a entrada), o pai é inteligente (possui o estado).
Stateless vs Stateful: Um componente stateless é mais fácil de testar — não depende de estado interno, seu comportamento é totalmente determinado pelos parâmetros de entrada. Um componente stateful é conveniente para prototipagem rápida, mas mais difícil de reutilizar: está fortemente acoplado a uma única fonte de dados. State Hoisting lhe dá a escolha: qualquer componente pode ser tornado stateless movendo o estado para cima.
UDF (Fluxo de Dados Unidirecional) é um princípio arquitetônico onde os dados se movem em uma direção: da fonte da verdade (ViewModel ou Composable pai) para a UI, e os eventos fluem na direção oposta. State Hoisting é a implementação do UDF no nível de componentes individuais. Em vez de cada componente decidir quando e como mudar seu próprio estado, ele notifica o pai sobre um evento, e o pai decide como mudar o estado.
Vantagens do UDF: previsibilidade — o estado muda em apenas um lugar, eliminando condições de corrida; rastreabilidade — a pilha de chamadas permite reconstruir a cadeia de mudanças; testes — a lógica stateful pode ser extraída para uma classe separada e testada sem UI. Em projetos grandes, UDF combinado com State Hoisting é o padrão de facto.
Fonte Única da Verdade (Single Source of Truth) é outro princípio que acompanha o UDF. Cada parte do estado tem exatamente uma fonte. Se dois componentes usam o mesmo estado, a fonte deve ser compartilhada (no nível da ViewModel ou pai comum). State Hoisting garante que a fonte esteja acima na hierarquia e não ocorra duplicação de estado.
| Direção | O que é passado | Como é implementado |
|---|---|---|
| Para baixo (pai → filho) | Valor para exibir | Parâmetro value: T |
| Para cima (filho → pai) | Evento de mudança | Parâmetro onValueChange: (T) -> Unit |
A regra principal: o estado deve ser elevado ao nível mínimo possível suficiente para todos os componentes que precisam dele. Se o estado é usado apenas dentro de um componente — mantenha-o local. Se dois componentes adjacentes precisam do mesmo estado — eleve ao pai comum. Se o estado é necessário em toda a tela — eleve à ViewModel.
A regra de elevação mínima evita complexidade desnecessária. Não faz sentido elevar o estado de um campo de texto para uma ViewModel se ele é usado apenas dentro de uma tela e não é persistido quando a Activity é recriada. Use rememberSaveable no nível do pai da tela, não na ViewModel, para estado de UI que deve sobreviver a uma rotação de tela, mas não é necessário pela lógica de negócio.
Quando elevar para ViewModel: se o estado deve sobreviver à recriação da Activity, se é necessário por várias telas, se mudar o estado dispara lógica de negócio (requisições de rede, banco de dados). State Hoisting no nível da ViewModel é um padrão padrão na arquitetura MVVM, onde a camada de UI é stateless e a ViewModel é stateful.
// ❌ Ruim: componente possui seu próprio estado
@Composable
fun BadTextField(label: String) {
var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it }, label = { Text(label) })
}
// ✅ Bom: State Hoisting — estado no pai
@Composable
fun GoodTextField(value: String, onValueChange: (String) -> Unit, label: String) {
TextField(value = value, onValueChange = onValueChange, label = { Text(label) })
}
// Uso: o pai possui o estado
@Composable
fun Form() {
var name by rememberSaveable { mutableStateOf("") }
GoodTextField(value = name, onValueChange = { name = it }, label = "Name")
}
Considere uma tela de login com dois campos (email, senha) e um botão. Os três componentes recebem estado através de State Hoisting: email e senha são gerenciados pelo pai, o botão recebe seu status enabled como um valor.
// State Hoisting no nível da tela
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val uiState by viewModel.uiState.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
// Campo Email — State Hoisting via lambda
EmailField(
email = uiState.email,
onEmailChange = { viewModel.onEmailChanged(it) }
)
// Campo Senha — similarmente
PasswordField(
password = uiState.password,
onPasswordChange = { viewModel.onPasswordChanged(it) }
)
// Botão — recebe apenas enabled (somente leitura)
LoginButton(enabled = uiState.isFormValid, onClick = viewModel::login)
}
}
// Componente Stateless: recebe email + callback
@Composable
fun EmailField(email: String, onEmailChange: (String) -> Unit) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
singleLine = true
)
}
// Componente de botão Stateless
@Composable
fun LoginButton(enabled: Boolean, onClick: () -> Unit) {
Button(onClick = onClick, enabled = enabled) {
Text("Entrar")
}
}
EmailField e PasswordField são completamente stateless. Podem ser reutilizados em qualquer tela conectando-os a qualquer fonte de dados. LoginButton recebe enabled como somente leitura — esta é outra forma de State Hoisting onde o estado não é elevado (o botão não pode se habilitar sozinho) mas é passado pronto. Esta abordagem proporciona máxima flexibilidade com mínimo acoplamento entre componentes.
Nem todo estado precisa ser elevado. Estado local (State dentro de um Composable) é justificado quando: os dados são necessários apenas dentro de um componente, não afetam elementos irmãos e não devem sobreviver à recomposição de uma seção específica. Por exemplo, estado de animação, foco de campo de entrada, posição atual de rolagem — é razoável mantê-los localmente.
Quando State Hoisting é necessário: o estado é usado por vários componentes filhos; uma mudança em um filho deve ser refletida em outro; a lógica de mudanças de estado precisa ser testada separadamente da UI; o estado deve sobreviver à recriação da Activity. Nestes casos, o estado local cria duplicação e inconsistência de dados.
Abordagem híbrida: mantenha o estado mínimo localmente, eleve o resto. A regra do Compose: “eleve o estado tão alto quanto necessário e tão baixo quanto possível.” Na prática, isso significa começar com remember local e só elevar o nível quando o acesso de outro componente for necessário. Não aplique State Hoisting preventivamente — isso complica o código sem necessidade.
Perguntas frequentes
State Hoisting é um padrão de nível de componente de UI. ViewModel é uma camada arquitetônica para lógica de negócio. State Hoisting pode elevar o estado ao nível do Composable pai, ao nível da tela ou à ViewModel. ViewModel é o ponto mais alto de elevação para o estado que deve sobreviver à recriação da Activity.
Um componente stateless é testado simplesmente passando valores. Chame o Composable com os parâmetros necessários e verifique a exibição usando ComposeTestRule. As mudanças de estado são testadas no nível do pai ou da ViewModel — separadas da UI. Isso simplifica significativamente os testes: não há necessidade de simular recomposição dentro do componente.
Sim, é uma prática comum. Se um componente só precisa exibir dados sem a capacidade de modificá-los — passe State<T> (somente leitura). O componente será inscrito nas mudanças, mas não poderá iniciá-las. Isso fortalece o encapsulamento e protege os dados de mutações indesejadas.
Para passagem profunda use CompositionLocal ou passe através de parâmetros do Composable pai. Se o estado for necessário em toda a tela — extraia para uma ViewModel e use collectAsState(). Passar através de 5+ níveis é sinal de arquitetura incorreta; reconsidere a hierarquia de componentes.
State Hoisting pode aumentar ligeiramente o número de recomposições, já que uma mudança no pai pode recompor todos os filhos. Use derivedStateOf para filtrar mudanças e keys no LazyColumn para atualizações direcionadas. Na maioria dos cenários, a sobrecarga do State Hoisting é insignificante comparada ao benefício de manutenibilidade.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também