eu parei de chamar page.update() o tempo todo

Como usar estado observável no Flet para criar temas reativos sem controle manual da interface
Como usar estado observável no Flet para criar temas reativos sem controle manual da interface

Como usar estado observável no Flet para criar temas reativos sem controle manual da interface


O app até funcionava… mas a UI não acompanhava

Em um dos meus projetos, precisei montar um sistema de temas no Flet — e foi aí que comecei a perceber um padrão estranho:

  • o usuário troca o tema
  • eu atualizo variáveis
  • chamo page.update()
  • alguns controles atualizam, outros não
  • e você não sabe exatamente por quê

Começa aquela caça:

  • qual controle não atualizou?
  • qual referência ficou pra trás?
  • onde eu deveria chamar update() de novo?

Chega um ponto em que você não está mais construindo a interface.
Está tentando manter ela sincronizada.


O problema com mutação manual

A abordagem padrão no Flet é imperativa:

  • guardar referência de controle
  • escutar evento
  • alterar propriedade
  • chamar update()

Funciona — até o estado crescer.

Com tema, isso escala rápido:

  • cor muda → vários componentes impactados
  • múltiplos pontos de atualização
  • inconsistência visual

E o pior:

você começa a depender de lembrar onde atualizar.


A virada

Foi aí que eu parei de tentar atualizar a interface…
e comecei a mudar só o estado.


Estado observável

Com @ft.observable e @ft.component, a lógica muda completamente:

@ft.observable
@dataclass
class AppState:
    theme_name: str = "Aurora"
    dark_mode: bool = False

Qualquer componente que usa esse estado re-renderiza automaticamente.

Sem page.update()
Sem referência manual

a UI passou a ser só uma consequência


Como o tema funciona

Cada tema é um conjunto de tokens:

THEMES = {
    "Aurora": {
        "primary": "#22D3EE",
        "surface": "#FFFFFF",
    },
}

E uma função resolve o tema ativo:

def th(state: AppState) -> dict:
    return THEMES[state.theme_name]

Os componentes só fazem isso:

  • leem o estado
  • aplicam as cores

Quando theme_name muda, tudo re-renderiza automaticamente.


Dark mode sem gambiarra

Em vez de sobrescrever cor por cor, eu pareei temas:

DARK_PAIR = {
    "Aurora": "Midnight",
    "Lime": "Obsidian",
}
def on_dark_change(e):
    state.dark_mode = e.control.value
    state.theme_name = DARK_PAIR.get(state.theme_name, state.theme_name)

Resultado:

  • sem estado intermediário quebrado
  • sem UI metade clara / metade escura
  • troca consistente

Logotipo contextual (Opcional)

O estado guarda duas versões:

logo_light: str = ""
logo_dark: str = ""

E a escolha é feita assim:

def active_logo(state: AppState) -> str:
    if state.dark_mode:
        return state.logo_dark or state.logo_light
    return state.logo_light or state.logo_dark

Simples, previsível e com fallback.
*Exemplo ilustrativo de uma lógica útil


Uma ressalva

Esse padrão funciona muito bem enquanto o estado é local.

Se a aplicação crescer:

  • múltiplas telas
  • múltiplos estados compartilhados

vale pensar em como distribuir isso melhor.

Outro ponto:

@ft.component re-renderiza o componente inteiro.
Para componentes pequenos, irrelevante.
Para listas grandes, pode exigir atenção.


Conclusão

No fim, o problema não era o tema.
Era a forma como eu estava tentando controlar a interface.

Quando parei de atualizar tudo manualmente
e deixei o estado guiar a UI
o código ficou mais simples — e mais previsível.