# Flet Best Practices

> Idiomatic guide for Flet 1.0 applications. Covers declarative UI, hooks, observable state, services, async execution model, and breaking API changes from v0.x. Use this skill to write modern Flet code, migrate imperative apps to reactive patterns, apply breaking changes correctly, and avoid common pitfalls.

- Skill: `mykaro/flet-best-practices` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mykaro/flet-best-practices`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mykaro/flet-best-practices/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: mykaro (https://skillmd.com/u/mykaro)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mykaro/flet-best-practices

---


## 1. Main ideas (TL;DR)

- Flet 1.0 — **complete architecture overhaul** (not an incremental release).
- Alongside the existing **imperative** approach, a **declarative/reactive** approach has appeared.
- UI is now described as a **pure function of state**: `UI = f(state)`.
- The framework itself decides which parts of the tree to update — `update()` is no longer needed.
- Inspiration: React, SwiftUI, Jetpack Compose, SolidJS.
- Architectural changes: dataclasses instead of manual conversions, binary protocol (MessagePack), InheritedWidget + Provider instead of Redux.

---

## 2. Imperative vs Declarative

### Imperative (old approach)
```python
# You say HOW to build the UI step by step
left_column.visible = False
right_column.visible = True
right_column.controls.append(ft.Text("Complete!"))
page.update()
```
**Problem:** state, logic, and UI live in one place. As the application grows, the number of synchronization points grows exponentially.

### Declarative (new approach)
```python
# You describe WHAT to show for a given state
@ft.component
def App():
    count, set_count = ft.use_state(0)
    return ft.Row(controls=[
        ft.Text(value=f"{count}"),
        ft.Button("Add", on_click=lambda: set_count(count + 1)),
    ])

ft.run(lambda page: page.render(App))
```
**Advantage:** UI is always an exact reflection of the state. The framework itself updates only what has changed.

---

## 3. Components (components)

**Component** — a function with the `@ft.component` decorator that takes parameters and returns Flet controls.

```python
@ft.component
def Greeting(name):
    return ft.Text(f"Hello, {name}!")
```

### Control vs Component

| | Control | Component |
|---|---|---|
| What it is | UI element (`Text`, `Button`, `Row`) | Function that builds and returns controls |
| Rendered | Directly | No — through controls in its return |
| Example | `ft.Text("Hi")` | `@ft.component def Card(title): ...` |

### Rules for writing components
- Capitalized names: `def Counter(...)`, not `def counter(...)`.
- The `@ft.component` decorator is mandatory for a functional component.
- The function always returns a single control or a list of controls.
- Do not call `page.update()` inside a component.
- Components can be nested inside each other to build complex UI.

---

## 4. Hooks (hooks)

Hooks — lightweight functions that allow a component to **store state**, **react to lifecycle events**, or **get shared context** — without classes.

> ⚠️ Local variables are recreated on every render — their values disappear. Hooks survive the render.

### Built-in Flet hooks

| Hook | Purpose |
|---|---|
| `use_state(initial)` | Local state that is preserved between renders |
| `use_effect(fn, deps)` | Side effects when dependencies change |
| `use_context(key)` | Access to shared data / services |
| `use_memo(fn, deps)` | Memoization of computations |

### Example of use_state
```python
@ft.component
def Counter():
    count, set_count = ft.use_state(0)
    return ft.Row(controls=[
        ft.Text(value=f"{count}"),
        ft.Button("Add", on_click=lambda: set_count(count + 1)),
    ])
```

### OOP analogy for understanding
```python
# Imaginary: the use_state(0) hook is a class field with state
class Counter(Component):
    count: state(0)
    def build(self):
        return Row(...)
```

---

## 5. Observable (observable objects)

**Observable** — a reactive data holder: when the value changes — the UI updates automatically.

Two ways to make a class observable:

```python
# Option 1: inheritance
@dataclass
class AppState(ft.Observable):
    count: int

# Option 2: decorator
@dataclass
@ft.observable
class AppState:
    count: int
```

### Full example with Observable
```python
import asyncio
from dataclasses import dataclass
import flet as ft

@dataclass
@ft.observable
class AppState:
    counter: float

    async def start_counter(self):
        self.counter = 0
        for _ in range(10):
            self.counter += 0.1
            await asyncio.sleep(0.5)

@ft.component
def App():
    state, _ = ft.use_state(AppState(counter=0))
    return [
        ft.ProgressBar(state.counter),
        ft.Button("Run!", on_click=state.start_counter),
    ]

ft.run(lambda page: page.render(App))
```

**Observable vs React:** Observable allows mutable state, React requires full replacement.
**Optimization:** several quick changes to Observable properties are merged into a single UI update.

---

## 6. Services (services)

**Service** — a persistent, non-visual component that "survives" page updates and navigation.

### Adding a service
```python
# The service must be added to page.services
file_picker = ft.FilePicker()
page.services.append(file_picker)
```

### Converted to services (Breaking Change!)
- `FilePicker`
- `Audio` (ext: `flet-audio`)
- `AudioRecorder` (ext: `flet-audio-recorder`)
- `HapticFeedback`
- `Flashlight`, `Geolocator`, `PermissionHandler`, `SemanticsService`, `ShakeDetector`

### FilePicker — new async API
```python
# Old approach (v0): via result event handlers
# New approach (v1): async methods that immediately return the result
files = await file_picker.pick_files_async(allow_multiple=True)
file_name = await file_picker.save_file_async()
dir_name = await file_picker.get_directory_path_async()
```

---

## 7. Auto-update

In Flet 1.0, `Control.update()` **is called automatically** after the event handler finishes.

Most applications **no longer need an explicit `update()`**.

For long-running operations — use `yield`:
```python
async def button_click():
    progress.value = "Something started"
    yield                        # ← force UI update right now
    await asyncio.sleep(3)
    progress.value = "Something finished"
```

---

## 8. Bootstrap: how to run a declarative app

```python
import flet as ft

# Minimal option
def App():
    return ft.Text("Hello, world!")

ft.run(lambda page: page.render(App))

# Explicit option
@ft.component
def App():
    return ft.Text("Hello, Flet!")

def main(page: ft.Page):
    page.render(App)

ft.run(main)
```

> ⚠️ The declarative approach must go **top-down** — the entire UI tree is declarative. Just like async code remains async everywhere.

---

## 9. Utilities: Ref, context, page

### Access to page from anywhere
```python
# Instead of passing page into every function:
print(ft.context.page.web)
```

### Ref — reference to a specific control
```python
from dataclasses import dataclass, field

@dataclass
class State:
    txt_name: ft.Ref[ft.TextField] = field(default_factory=lambda: ft.Ref())

@ft.component
def App(state):
    return ft.TextField(ref=state.txt_name)
```

### Controlled inputs (recommended approach for forms)
```python
@dataclass
@ft.observable
class Form:
    name: str = ""

    def set_name(self, value):
        self.name = value

    async def submit(self, e: ft.Event[ft.Button]):
        e.page.show_dialog(
            ft.AlertDialog(title="Hello", content=ft.Text(f"Hello, {self.name}!"))
        )

    async def reset(self):
        self.name = ""

@ft.component
def App():
    form, _ = ft.use_state(Form())
    return [
        ft.TextField(
            label="Your name",
            value=form.name,
            on_change=lambda e: form.set_name(e.control.value),
        ),
        ft.Row([
            ft.FilledButton("Submit", on_click=form.submit),
            ft.FilledTonalButton("Reset", on_click=form.reset),
        ]),
    ]
```

---

## 10. Breaking Changes (Flet 1.0)

### Async model
- **Single-threaded async UI** — like JavaScript or Flutter.
- `time.sleep()` freezes the UI → use `await asyncio.sleep()`.
- CPU-bound tasks → `await asyncio.to_thread(...)`.
- All get/set methods of controls — are now **async**.

### API changes

| What changed | Was (v0) | Became (v1) |
|---|---|---|
| App launch | `ft.app(target=main)` | `ft.run(main)` |
| Dialogs (open) | `page.open(dialog)` | `page.show_dialog(dialog)` |
| Dialogs (close) | — | `page.pop_dialog()` |
| Drawers | `page.drawer = ...` | `NavigationDrawer(position=...)` + `page.show_dialog()` |
| Clipboard | `page.set_clipboard(v)` | `page.clipboard.set_async(v)` |
| Clipboard | `page.get_clipboard()` | `page.clipboard.get_async()` |
| Client storage | `page.client_storage` | `page.shared_preferences` |
| Buttons | `Button(text="...")` | `Button(content=...)` — no `text`! |
| Scroll interval | `on_scroll_interval` | `scroll_interval` |
| Charts | built-in | separate package `flet-charts` |
| Control ID | `e.target` — string | `e.target` — integer |
| FilePicker | controls list | `page.services` |

### before_main hook
```python
def config(page: ft.Page):
    page.on_resize = lambda e: print("Page resized!")

def main(page: ft.Page):
    page.add(ft.Text("Hello!"))

ft.run(main, before_main=config)
```

### Event handlers — parameter `e` is now optional
```python
button_1.on_click = lambda: print("Clicked!")       # without e — OK
button_2.on_click = lambda e: print("Clicked!", e)  # with e — also OK
```

---

## 11. New architecture (under the hood)

| Component | Was | Became |
|---|---|---|
| Python controls | manual conversions | **dataclasses** with auto-constructor |
| UI diffing | basic | new algorithm for imperative+declarative |
| Dart state mgmt | Redux | **InheritedWidget + Provider** |
| Protocol | JSON | **MessagePack** (binary, less traffic) |
| Binary data | base64 | raw binary |
| Extensions API | function `createControl` | class `FletExtension` with `createWidget` + `createService` |

---

## 12. Web: WASM and Offline

### WebAssembly
- WASM is enabled by default (where supported by the browser).
- Renderer: **SKWASM** (for WASM) + **CanvasKit** (for Dart2JS fallback).

### Offline / no-CDN mode
```python
# Via code
ft.run(main, no_cdn=True)

# Via environment variable
# FLET_WEB_NO_CDN=1

# Via CLI
flet build web --no-cdn
```
Bundles: CanvasKit, SkWASM, Pyodide, fonts.

---

## 13. Installing Flet 1.0

```bash
# pip
pip install --pre 'flet[all]>=0.70.0.dev0'

# uv
uv add 'flet[all]>=0.70.0.dev0' --prerelease=allow
```

### pyproject.toml for flet build
```toml
dependencies = [
    "flet >=0.70.0.dev0",
    "flet-audio >=0.2.0.dev0",
    "flet-video >=0.2.0.dev0",
]
```

> Extensions v1 have version `0.2.x+`, extensions v0 — `0.1.x`.

---

## 14. Best Practices

### ✅ Correct

```python
# 1. Use @ft.component for every reusable UI piece
@ft.component
def UserCard(name, avatar_url):
    return ft.Row([ft.Image(src=avatar_url), ft.Text(name)])

# 2. State — in Observable or use_state, not in global variables
@dataclass
@ft.observable
class AppState:
    items: list[str]

# 3. Async event handlers for IO operations
async def on_save(e):
    await db.save_async(data)

# 4. yield to display intermediate state in long-running operations
async def on_process(e):
    progress.visible = True
    yield
    await do_heavy_work()
    progress.visible = False

# 5. Services — in page.services
page.services.append(ft.FilePicker())
```

### ❌ Incorrect

```python
# Do not use time.sleep() — freezes the UI
time.sleep(3)  # ❌
await asyncio.sleep(3)  # ✅

# Do not call page.update() in a declarative app
page.update()  # ❌ in declarative mode

# Do not add FilePicker to controls — it is now a service
page.overlay.append(ft.FilePicker())  # ❌
page.services.append(ft.FilePicker())  # ✅

# Do not use page.open() for dialogs
page.open(dialog)  # ❌
page.show_dialog(dialog)  # ✅

# Do not use text= in buttons
ft.ElevatedButton(text="Click")  # ❌
ft.ElevatedButton(content=ft.Text("Click"))  # ✅
```

---

