--- 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.