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.From its SKILL.md
npx -y skills add mykaro/ai-skills --skill flet-best-practicesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
12.2 KB, ~3.0k tokens by cl100k_base, as published. Nobody here has run it
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)
# 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)
# 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.
@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(...), notdef counter(...). - The
@ft.componentdecorator 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
@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
# 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:
# Option 1: inheritance
@dataclass
class AppState(ft.Observable):
count: int
# Option 2: decorator
@dataclass
@ft.observable
class AppState:
count: int
Full example with Observable
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
# The service must be added to page.services
file_picker = ft.FilePicker()
page.services.append(file_picker)
Converted to services (Breaking Change!)
FilePickerAudio(ext:flet-audio)AudioRecorder(ext:flet-audio-recorder)HapticFeedbackFlashlight,Geolocator,PermissionHandler,SemanticsService,ShakeDetector
FilePicker — new async API
# 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:
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
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
# Instead of passing page into every function:
print(ft.context.page.web)
Ref — reference to a specific control
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)
@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 → useawait 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
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
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
# 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
# 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
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
# 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
# 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")) # ✅
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.