Widgets Android parecem pequenos detalhes de interface, mas costumam ser uma das formas mais fortes de retenção quando o produto tem informação recorrente: tarefas do dia, saldo, previsão, treino, entrega, hábito, checklist, métricas ou status de sincronização. Em 2026, criar widgets com Kotlin ficou mais natural graças ao Jetpack Glance, que aproxima a experiência de desenvolvimento do modelo declarativo do Compose sem exigir que você escreva tudo diretamente com RemoteViews.
Glance não é “Compose completo na tela inicial”. Ele é uma camada declarativa para gerar widgets compatíveis com o sistema de AppWidget do Android. Isso significa que você escreve código com uma aparência familiar para quem usa Jetpack Compose, mas ainda precisa respeitar as limitações da tela inicial: poucas interações, atualização controlada, layout mais restrito, tamanho variável e cuidado grande com bateria.
Este guia mostra como pensar um widget Android moderno com Kotlin: quando ele faz sentido, como organizar módulos, como guardar estado, como atualizar dados, onde entra o WorkManager e quais erros costumam quebrar a experiência. Se você está montando a base do app, leia também WorkManager com Kotlin no Android, DataStore Preferences com Kotlin e Modularização Android com Kotlin e Compose.
Quando um widget vale a pena
O erro comum é criar widget porque o app “precisa de presença” na tela inicial. Presença sozinha não sustenta uso. Um widget vale a pena quando ele responde a uma pergunta frequente sem exigir que a pessoa abra o app.
Bons casos de uso incluem:
- próxima tarefa ou próximo evento;
- resumo financeiro simples;
- status de entrega ou pedido;
- previsão do tempo resumida;
- hábito do dia;
- contador de estudos, treino ou foco;
- atalhos para ações recorrentes;
- métrica operacional de um app interno.
Casos ruins incluem widgets que apenas repetem a home do aplicativo, banners promocionais sem utilidade ou telas que dependem de muita navegação. A tela inicial não é lugar para fluxo complexo. Ela funciona melhor como painel de contexto e porta de entrada para uma ação específica.
Estrutura básica com Glance
Um widget Glance normalmente combina três peças: uma classe GlanceAppWidget, um receiver GlanceAppWidgetReceiver e alguma fonte de estado. A classe do widget descreve o conteúdo. O receiver conecta o widget ao sistema Android. A fonte de estado alimenta o layout.
Um exemplo mínimo fica assim:
class HabitoDoDiaWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme {
Column(
modifier = GlanceModifier
.fillMaxSize()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Hábito de hoje",
style = TextStyle(fontWeight = FontWeight.Bold),
)
Spacer(GlanceModifier.height(8.dp))
Text(text = "Beber água antes das 10h")
}
}
}
}
}
class HabitoDoDiaWidgetReceiver : GlanceAppWidgetReceiver() {
override val glanceAppWidget: GlanceAppWidget = HabitoDoDiaWidget()
}
O manifesto aponta para o receiver e para o arquivo de configuração do AppWidget. Essa parte continua parecida com widgets tradicionais porque o Android precisa descobrir o widget antes de executar seu código Kotlin.
<receiver
android:name=".widgets.HabitoDoDiaWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/habito_do_dia_widget_info" />
</receiver>
Layout não é tela Compose normal
A maior armadilha para quem vem do Compose é esquecer que Glance gera um widget de tela inicial, não uma Activity. Você não deve esperar a mesma liberdade de layout, animação, navegação, gesto ou estado efêmero.
Em vez de tentar reproduzir uma tela inteira, desenhe uma versão reduzida:
- uma informação principal;
- uma informação secundária;
- uma ação clara;
- um estado de erro ou vazio;
- um visual legível em tamanhos pequenos.
Pense em blocos como Column, Row, Text, Image, Button e Spacer. Evite densidade excessiva. Um widget pequeno precisa ser entendido em menos de dois segundos.
Para apps que já usam design system em Compose, vale criar uma camada de tokens compartilhados: nomes de cores, espaçamentos, textos e decisões de conteúdo. Mas não tente reutilizar todos os componentes Compose diretamente. O ideal é compartilhar intenção visual, não acoplar o widget ao mesmo componente da tela interna.
Estado com DataStore ou banco local
Widget bom precisa carregar rápido e funcionar mesmo quando o app não está aberto. Por isso, ele deve ler de uma fonte local simples. Para preferências e pequenos resumos, DataStore costuma ser suficiente. Para listas, histórico ou dados estruturados, Room ou SQLDelight fazem mais sentido.
Um padrão saudável é manter um WidgetStateRepository separado da UI principal:
data class ResumoWidget(
val titulo: String,
val detalhe: String,
val atualizadoEm: Instant,
)
class WidgetStateRepository(
private val dataStore: DataStore<Preferences>,
) {
val resumo: Flow<ResumoWidget> = dataStore.data.map { prefs ->
ResumoWidget(
titulo = prefs[stringPreferencesKey("widget_titulo")] ?: "Sem dados",
detalhe = prefs[stringPreferencesKey("widget_detalhe")] ?: "Abra o app para configurar",
atualizadoEm = Instant.fromEpochMilliseconds(
prefs[longPreferencesKey("widget_atualizado_em")] ?: 0L,
),
)
}
}
Esse repositório não deve depender de Activity, NavController nem estado de tela. Ele precisa ser fácil de chamar a partir do app, de um worker ou do próprio widget. Em projetos modularizados, uma organização comum é :feature:widget dependendo de :core:model, :core:datastore e, quando necessário, :core:sync.
Atualização com WorkManager
Widgets não devem acordar rede a todo momento. O sistema limita atualizações e o usuário percebe quando um app consome bateria por causa de painel decorativo. A regra prática é sincronizar dados no app ou em background controlado, salvar um resumo local e pedir atualização do widget depois.
WorkManager encaixa bem quando o dado pode ser atualizado em janela flexível:
class AtualizarWidgetWorker(
context: Context,
params: WorkerParameters,
private val repository: WidgetStateRepository,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return runCatching {
repository.sincronizarResumoRemoto()
HabitoDoDiaWidget().updateAll(applicationContext)
}.fold(
onSuccess = { Result.success() },
onFailure = { Result.retry() },
)
}
}
Para dados urgentes, como entrega em rota ou alerta crítico, avalie notificações em vez de forçar widgets como canal de tempo real. Widget é melhor para contexto persistente; notificação é melhor para evento que exige atenção.
Clique e navegação
A interação mais importante de muitos widgets é abrir o app já na tela certa. Em Glance, você pode usar ações para iniciar uma Activity ou enviar um broadcast. O widget não deve exigir que o usuário abra a home e procure manualmente o próximo passo.
Text(
text = "Ver detalhes",
modifier = GlanceModifier.clickable(
actionStartActivity<MainActivity>(
parameters = actionParametersOf(
ActionParameters.Key<String>("destino") to "habitos",
),
),
),
)
No app, trate esse destino como deep link interno. Se você já usa App Links ou Navigation Compose, mantenha uma rota estável para o widget. Isso evita que uma refatoração de navegação quebre atalhos instalados na tela inicial.
Estados que precisam aparecer
Um widget de produção deve ter pelo menos quatro estados planejados:
- Sem configuração: usuário adicionou o widget, mas ainda não escolheu conta, hábito ou lista.
- Com dados: resumo principal está pronto.
- Sem conexão ou erro: último dado válido existe, mas a atualização falhou.
- Conta desconectada: usuário saiu do app ou perdeu sessão.
Não esconda falha como se fosse dado atualizado. Mostre algo como “Atualizado ontem” ou “Abra o app para reconectar”. Transparência evita decisões erradas e reduz suporte.
Testes e validação prática
Testar widget é menos confortável que testar ViewModel, mas ainda dá para validar bastante coisa. Separe lógica de seleção de dados em classes puras, teste o repositório local, teste o worker e faça uma checklist manual em emulador ou aparelho real.
Antes de publicar, valide:
- widget pequeno, médio e grande;
- tema claro e escuro;
- fonte aumentada nas configurações de acessibilidade;
- usuário deslogado;
- modo avião;
- atualização depois de abrir o app;
- atualização depois de worker;
- clique abrindo a tela correta;
- remoção e adição do widget novamente.
Também vale medir uso. Se o widget existe para aumentar retenção, acompanhe eventos como adição, clique e atualização bem-sucedida. Um app Kotlin que usa analytics já pode padronizar isso em um módulo :core:analytics sem espalhar chamadas pela UI.
Erros comuns
O primeiro erro é colocar informação demais. Widget cheio vira ruído visual. O segundo é buscar rede diretamente durante a renderização, criando lentidão e falhas intermitentes. O terceiro é não pensar em sessão expirada: o widget continua mostrando dado antigo e passa falsa confiança.
Outro erro é tratar Glance como substituto de uma tela Compose. Se você precisa de lista longa, filtros, abas e formulário, abra o app. O widget deve ser o resumo e o atalho, não o produto inteiro.
Por fim, cuidado com compatibilidade. Diferentes launchers podem renderizar detalhes de forma ligeiramente diferente. Teste pelo menos no launcher padrão do emulador e em um aparelho real quando o widget for parte importante da proposta de valor.
Onde Glance entra na arquitetura
Em um app Android moderno, o widget deve ficar próximo da camada de experiência, mas longe das telas internas. Uma estrutura possível:
:app
:feature:home
:feature:widget
:core:model
:core:datastore
:core:sync
:core:analytics
O módulo :feature:widget renderiza Glance e traduz estado local para UI de tela inicial. O :core:sync busca dados. O :core:datastore ou :core:database persiste o resumo. O :core:analytics mede clique e atualização. Isso evita que o widget dependa de ViewModels de tela ou que o app principal precise conhecer detalhes de AppWidget.
Para times que trabalham também com backend Kotlin, a lógica de resumo pode ser pensada como contrato de produto: o servidor entrega um payload pequeno e estável, e o Android decide como mostrá-lo no widget. Quando o mesmo produto tem jobs, sincronização ou automações fora do Android, vale comparar como outras stacks estruturam workers. O guia de Celery em Python para tarefas em background ajuda a pensar em filas e retries do lado servidor sem misturar isso com a camada mobile.
Conclusão
Glance torna widgets Android muito mais agradáveis para equipes Kotlin que já pensam de forma declarativa, mas a disciplina de produto continua sendo a parte mais importante. Um widget útil não é uma miniatura do app. Ele é uma resposta rápida, confiável e acionável para uma necessidade recorrente.
Comece pequeno: um resumo local, uma ação clara, atualização controlada e estados honestos. Depois conecte com WorkManager, DataStore, analytics e navegação profunda. Se o widget realmente reduz atrito, ele vira um ponto de contato diário com o usuário. Se ele só replica tela, vira decoração. Em Android com Kotlin, essa diferença aparece rápido na retenção, na bateria e na qualidade percebida do produto.