Começo rápido¶
Este guia leva você do zero a um app rodando no simulador em poucos minutos — mesmo que seja seu primeiro contato com o tempestroid. O caminho é sempre o mesmo: criar um projeto, rodar no simulador, editar e ver a mudança ao vivo.
Pré-requisitos
- Python ≥ 3.11 e o uv instalados.
- O framework com o simulador Qt:
pip install "tempestroid[qt]"(ou, neste repositório de desenvolvimento,uv sync). Detalhes em Instalação. - No WSL/Linux sem ambiente gráfico, o simulador Qt precisa de um servidor de display. Veja Rodar no dispositivo / WSL.
Passo 1 — Crie um projeto¶
Você já está dentro da pasta do seu projeto (e do seu ambiente virtual). O
comando tempest new, sem argumentos, gera os arquivos iniciais aqui
mesmo — um app.py (contador de exemplo), pyproject.toml, README.md e
.gitignore — e usa o nome da pasta atual como id do app. Sem pasta extra
em volta.
mkdir meu-app && cd meu-app # sua pasta de projeto (com seu venv)
uv run tempest new # scaffold AQUI; id = "meu-app"
Quer uma subpasta? Passe um nome:
uv run tempest new OutroAppcriaOutroApp/. Mas o fluxo recomendado é o in-place acima.Instalou via
pip? O binário fica disponível comotempest new(sem ouv run). Ao longo deste guia usamosuv run tempest …por ser o fluxo do repositório; remova ouv runse instalou pelopip.
O app.py gerado é Python puro, sem nenhum import de Qt no nível do módulo —
por isso o mesmo arquivo roda no simulador de desktop, vai pro dispositivo
via tempest serve e empacota com tempest build sem mudar uma linha.
Passo 2 — Rode no simulador¶
tempest dev (sem argumento) lê o caminho do app de [tool.tempest] app no
pyproject.toml — por isso você roda da raiz do projeto sem apontar o arquivo.
Uma janela abre com o contador (-, o valor, +). O terminal vira um cockpit
interativo:
| Tecla | Ação |
|---|---|
r |
Hot reload — recarrega o código preservando o estado atual. |
R |
Hot restart — recarrega do zero (estado limpo via make_state). |
s |
Traz a janela do simulador à frente. |
q |
Encerra. |
Escolha o tamanho de tela (presets de aparelho)¶
O simulador abre num tamanho de telefone genérico, mas você deve testar nos
tamanhos dos aparelhos-alvo. Passe --device (ou -d) com um preset:
uv run tempest dev --device pixel-7 # 412×915 dp
uv run tempest dev -d galaxy-s24 # 384×824 dp
uv run tempest dev -d redmi-note-12 # 393×873 dp (padrão)
Os tamanhos são em dp (pixels independentes de densidade) — exatamente o
espaço de layout que o Compose usa no aparelho, então o que cabe no simulador
cabe no device. Os nomes são tolerantes (pixel-7, PIXEL_7, pixel 7,
Google Pixel 7 resolvem o mesmo). O catálogo completo são 33 presets (Pixel,
Galaxy S/A, Redmi/Poco/Xiaomi, Moto, OnePlus) no enum Device — use
programaticamente com run_qt(state, view, device=Device.PIXEL_7).
Quais testar
Cubra um telefone pequeno/estreito (ex.: galaxy-s8, 360×740) e um
grande (ex.: pixel-8-pro, 448×998) — se o layout se comporta nos dois
extremos, os tamanhos intermediários seguem bem.
Até onde o simulador é fiel?
O simulador reflete fielmente estrutura, estado, eventos e a maior parte do
Style, mas não a aparência nativa (Material 3), animações, overlays e
fontes — esses só são 100% fiéis no aparelho. Veja
fidelidade do simulador.
Passo 3 — Edite e veja ao vivo¶
Com o simulador aberto, abra app.py no editor e mude algum texto — por
exemplo o título dentro do Text. Salve o arquivo. O tempest dev detecta a
gravação e faz hot reload automático: a janela atualiza sem perder o contador.
Se uma edição quebrar o app, o erro é impresso no terminal e o loop sobrevive — corrija e salve de novo. Se a recarga for incompatível com o estado vivo, ele cai automaticamente para um restart limpo.
Pronto: esse é o ciclo completo de desenvolvimento. O resto deste guia explica o que você acabou de rodar.
O modelo mental¶
Todo app tempestroid honra um contrato de duas funções:
make_state() -> S— devolve o estado inicial. É chamado a cada hot restart, então deve montar um estado limpo.Sé qualquer objeto seu (um@dataclassé o caminho natural).view(app: App[S]) -> Widget— recebe o app e devolve a árvore de UI para o estado atual. É uma função pura de estado → widgets: dado o mesmo estado, devolve a mesma árvore.
O ciclo que conecta as duas:
estado ──view(app)──▶ árvore de widgets ──diff──▶ patches ──▶ tela
▲ │
└────────────── app.set_state(...) ◀── handler de evento ◀───┘
viewconstrói a árvore a partir deapp.state.- Você liga handlers (clique, etc.) a
app.set_state(...). - Quando um handler chama
set_state, oAppreconstrói aview, faz o diff contra a árvore anterior e entrega só os patches (mudanças mínimas) ao renderizador. Várias chamadas deset_stateno mesmo tick viram um único rebuild (coalescido).
set_state recebe uma função que muta o estado no lugar:
Você nunca toca na tela diretamente — só descreve a UI em função do estado e muda o estado. O framework cuida do resto.
Um contador do zero¶
O scaffold já dá um contador completo, mas vale construir o mínimo à mão para
entender cada peça. Crie um app.py:
from dataclasses import dataclass
from tempestroid import App, Button, Column, Style, Text, Widget
from tempestroid.renderers.qt import run_qt
@dataclass
class CounterState:
"""O estado do app: um único contador."""
value: int = 0
def make_state() -> CounterState:
"""Devolve o estado inicial (chamado a cada hot restart)."""
return CounterState()
def view(app: App[CounterState]) -> Widget:
"""Constrói a árvore de UI para o estado atual."""
def increment() -> None:
app.set_state(lambda s: setattr(s, "value", s.value + 1))
return Column(
style=Style(gap=8.0),
children=[
Text(content=f"Count: {app.state.value}", key="label"),
Button(label="+", on_click=increment, key="inc"),
],
)
if __name__ == "__main__":
raise SystemExit(run_qt(make_state(), view, title="counter"))
Lendo de cima para baixo:
CounterState— seu estado, umdataclasssimples com um campovalue.make_state— a fábrica do estado inicial.view— descreve a tela: umaColumn(empilha verticalmente) com umTextque mostra o valor e umButtonque incrementa.app.state.valuelê o estado;incrementchamaset_statepara mudá-lo.key="..."— identifica cada widget para o diff casar o widget velho com o novo entre rebuilds. Dê keys estáveis a filhos de listas.Style(gap=8.0)— espaçamento entre filhos. Estilos são objetos tipados e imutáveis (veja o guia de estilos).if __name__ == "__main__"—run_qtabre a janela ao rodar o arquivo direto. Mantenha o import de Qt dentro deste bloco (ou só aqui no topo, sem ser usado porview/make_state) para o arquivo continuar rodando no dispositivo, que não tem Qt.
Rode direto, sem o cockpit:
Ou com hot reload (recomendado durante o desenvolvimento):
Handlers assíncronos¶
Handlers podem ser async — o runtime os agenda no loop asyncio sem travar a
UI. Útil para esperar I/O (rede, disco) antes de atualizar o estado:
import asyncio
def view(app: App[CounterState]) -> Widget:
async def increment_later() -> None:
await asyncio.sleep(0.5)
app.set_state(lambda s: setattr(s, "value", s.value + 1))
return Button(label="+0.5s", on_click=increment_later, key="inc")
Problemas comuns¶
| Sintoma | Causa / solução |
|---|---|
ModuleNotFoundError: tempestroid |
Framework não instalado no ambiente. Rode uv sync (repo) ou pip install "tempestroid[qt]". |
Erro de import do PySide6 / Qt ao rodar dev |
O extra qt não está instalado. Use pip install "tempestroid[qt]". |
app.py must define a make_state() / view |
O arquivo precisa expor as duas funções no nível do módulo, com esses nomes exatos. |
| A janela não abre no WSL/Linux headless | Falta um servidor de display. Veja dispositivo / WSL. |
| Edição não recarrega | Confirme que está rodando via tempest dev (não python app.py) e que salvou o arquivo; ou aperte r. |
Próximos passos¶
- Widgets — todas as primitivas (
Text,Column,Row,Button, entradas, mídia…). - Estilos — o modelo de
Styletipado. - Eventos — o contrato tipado de eventos.
- CLI — todos os comandos
tempest. - Galeria de exemplos — apps completos para estudar.
Veja também o exemplo de referência em
examples/counter/app.py.