---
name: imperative-to-declarative-flet
description: Convert an imperative Flet Python app in which controls are mutated and then page.update is called to declarative style using flet.component, flet.observable and state hooks.
metadata:
  short-description: Port Flet app to declarative
---

# Imperative → Declarative (Flet Python)

Port an existing imperative Flet app to “components mode” with `@ft.component`, hooks, and (optionally) `@ft.observable` models.

## Outcomes

- Original folder unchanged; new sibling folder created.
- Entry point uses `page.render(App)` (components mode).
- UI is derived from state; minimal/zero manual `page.update()` for normal UI updates.

## Choose a state strategy

- **Use `@ft.observable`** for nested app data you mutate in place (boards/lists/cards, drag+drop reorder).
- **Use `ft.use_state`** for local ephemeral UI state (hover flags, input text, dialog selection).
- Avoid storing live `Control`/component objects in state; store ids/enums and create controls during render.

## Workflow

### 1) Create a new declarative copy

- Copy the existing app folder to a new one (e.g. `trolli` → `trolli-declarative-*`).
- Keep `assets/` with the new folder.

### 2) Switch to components mode entrypoint

- Create a root component `@ft.component def App(): ...`.
- Run with:
  - `ft.run(lambda page: page.render(App), assets_dir=...)`
- Set page globals either:
  - in a `main(page)` function before `page.render(App)`, or
  - in `ft.on_mounted(...)` (works, but ordering can be less obvious).

### 3) Centralize routing in `App`

- Keep `app.route: str` as source of truth.
- In one place, define:
  - `route_change(e)` to normalize/redirect/validate routes and set `app.route`
  - a render-time `match app.route` (or a derived `active_screen`) to pick `content`
- Prefer: `route_change` mutates route state; `App()` render chooses UI based on that state.

### 4) Componentize UI

- Move UI chunks into `src/components/*.py`.
- Each component:
  - takes only the model(s) it needs
  - uses hooks for local UI state
  - mutates observable models for app data changes

### 5) Dialogs: reduce `page.update()` usage

If a dialog mutates existing controls and calls `page.update()`, convert it to:

- `@ft.component` dialog content with `ft.use_state` for `error`, `selected_color`, etc.
- event handlers call setters (no explicit updates)
- show it via `page.show_dialog(ft.AlertDialog(content=DialogContent(...)))`

### 6) Assets/fonts checklist

- Prefer an absolute `assets_dir` derived from `__file__`.
- If running via `flet run`, be aware it can set `FLET_ASSETS_DIR` and override `assets_dir=`.
- Font registration uses the dict key as the font family name:
  - `page.fonts = {"Pacifico": "Pacifico-Regular.ttf"}`
  - use `font_family="Pacifico"`.

## Common pitfalls (and fixes)

- **Event handler typing is invariant** (`Event[Sub]` ≠ `Event[Base]`):
  - If `on_click` is declared on `Button`, annotate `e` as `ft.Event[ft.Button]` (or use `def handler(): ...`).
- **`TemplateRoute` params are dynamic**:
  - `raw = getattr(troute, "id", None)` then `isinstance(raw, str)` before `int(raw)`.
- **`controls=[*[...], ...]` can confuse type checkers** with components:
  - build lists in two steps and annotate/cast, or return raw controls from non-component factories.
