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", 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", 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!",
]
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,
e: form.set_name(e.control.value),
),
ft.Row([
ft.FilledButton("Submit",
ft.FilledTonalButton("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")) # ✅