Flet ASP (Flet Atomic State Pattern) is a reactive state management library for Flet, bringing atom-based architecture and separation of concerns into Python apps — inspired by Flutter's Riverpod and ASP.
It provides predictable, testable, and declarative state through:
Atom– single reactive unit of stateSelector– derived/computed stateAction– handles async workflows like login, fetch, etc.
Install using your package manager of choice:
# Pip
pip install flet-asp
# Poetry
poetry add flet-asp
# UV
uv add flet-asp✅ Reactive atoms - Automatic UI updates when state changes
✅ Selectors - Derived/computed state (sync & async)
✅ Actions - Async-safe workflows for API calls, auth, etc.
✅ One-way & two-way binding - Seamless form input synchronization
✅ Hybrid update strategy - Bindings work even before controls are mounted
✅ Python 3.14+ optimizations - Free-threading, incremental GC, 3-5% faster
✅ Lightweight - No dependencies beyond Flet
✅ Type-safe - Full type hints support
The simplest way to use Flet-ASP: create an atom, bind it to a control, and update it.
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
# Initialize state manager
fa.get_state_manager(page)
# Create a reactive atom
page.state.atom("count", 0)
# Create UI references
count_text = ft.Ref[ft.Text]()
def increment(e):
# Update the atom - UI updates automatically!
current = page.state.get("count")
page.state.set("count", current + 1)
# Build UI
page.add(
ft.Column([
ft.Text("Counter", size=30),
ft.Text(ref=count_text, size=50),
ft.ElevatedButton("Increment", on_click=increment)
])
)
# Bind atom to UI - the Text will update automatically
page.state.bind("count", count_text)
ft.app(target=main)What's happening here?
atom("count", 0)- Creates a reactive piece of statebind("count", count_text)- Connects state to UIset("count", value)- Updates state → UI updates automatically!
Perfect for input fields that need to sync with state.
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
fa.get_state_manager(page)
# Create atoms for form fields
page.state.atom("email", "")
page.state.atom("password", "")
page.state.atom("message", "") # Atom for login message
# UI references
email_field = ft.Ref[ft.TextField]()
password_field = ft.Ref[ft.TextField]()
message_text = ft.Ref[ft.Text]()
def login(e):
email = page.state.get("email")
password = page.state.get("password")
if email == "user@example.com" and password == "123":
page.state.set("message", f"Welcome, {email}!")
else:
page.state.set("message", "Invalid credentials")
# No page.update() needed - bind() handles it!
page.add(
ft.Column([
ft.Text("Login Form", size=24),
ft.TextField(ref=email_field, label="Email"),
ft.TextField(ref=password_field, label="Password", password=True),
ft.ElevatedButton("Login", on_click=login),
ft.Text(ref=message_text)
])
)
# Two-way binding: TextField ↔ Atom
page.state.bind_two_way("email", email_field)
page.state.bind_two_way("password", password_field)
page.state.bind("message", message_text) # One-way binding for message
ft.app(target=main)Key concept: bind_two_way() keeps the TextField and atom in perfect sync!
Derive new values from existing state automatically.
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
fa.get_state_manager(page)
# Base atoms
page.state.atom("first_name", "John")
page.state.atom("last_name", "Doe")
# Computed state - automatically recalculates when dependencies change
@page.state.selector("full_name")
def compute_full_name(get):
return f"{get('first_name')} {get('last_name')}"
# UI
first_field = ft.Ref[ft.TextField]()
last_field = ft.Ref[ft.TextField]()
full_name_text = ft.Ref[ft.Text]()
page.add(
ft.Column([
ft.Text("Name Builder", size=24),
ft.TextField(ref=first_field, label="First Name"),
ft.TextField(ref=last_field, label="Last Name"),
ft.Divider(),
ft.Text("Full Name:", weight=ft.FontWeight.BOLD),
ft.Text(ref=full_name_text, size=20, color=ft.Colors.BLUE)
])
)
# Bind inputs
page.state.bind_two_way("first_name", first_field)
page.state.bind_two_way("last_name", last_field)
# Bind computed state
page.state.bind("full_name", full_name_text)
ft.app(target=main)Magic! The full name updates automatically when first or last name changes.
Handle API calls, async operations, and side effects cleanly.
import asyncio
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
fa.get_state_manager(page)
page.state.atom("user", None)
page.state.atom("loading", False)
page.state.atom("status", "") # Atom for status message
# Define async action
async def login_action(get, set_value, params):
set_value("loading", True)
# Simulate API call
await asyncio.sleep(2)
# Validate credentials
email = params.get("email")
password = params.get("password")
if email == "test@test.com" and password == "123":
set_value("user", {"email": email, "name": "Test User"})
else:
set_value("user", None)
set_value("loading", False)
# Create action
login = fa.Action(login_action)
# UI
email_field = ft.Ref[ft.TextField]()
password_field = ft.Ref[ft.TextField]()
status_text = ft.Ref[ft.Text]()
async def handle_login(e):
await login.run_async(
page.state,
{
"email": email_field.current.value,
"password": password_field.current.value
}
)
user = page.state.get("user")
if user:
page.state.set("status", f"Welcome, {user['name']}!")
else:
page.state.set("status", "Login failed")
# No page.update() needed - bind() handles it!
# Selector to derive status from loading state
@page.state.selector("loading_status")
def compute_loading_status(get):
return "Logging in..." if get("loading") else get("status")
page.add(
ft.Column([
ft.Text("Async Login", size=24),
ft.TextField(ref=email_field, label="Email"),
ft.TextField(ref=password_field, label="Password", password=True),
ft.ElevatedButton("Login", on_click=handle_login),
ft.Text(ref=status_text)
])
)
# Bind selector to status text - fully declarative!
page.state.bind("loading_status", status_text)
ft.app(target=main)Actions encapsulate complex async logic in a testable, reusable way.
Flet-ASP provides three powerful tools for managing reactive state. Understanding when to use each one is key to writing clean, performant code.
| Feature | listen() |
selector() |
action() |
|---|---|---|---|
| Purpose | Execute side effects | Calculate derived state | Execute business logic |
| Returns value? | ❌ No | ✅ Yes (creates atom) | ❌ No |
| Execution | 🔄 Automatic (reactive) | 🔄 Automatic (reactive) | 👆 Manual (on-demand) |
| Tracks dependencies | ❌ No (1 atom only) | ✅ Yes (automatic) | ❌ No |
| Memoization | ❌ No | ✅ Yes (5-20x faster) | ❌ No |
| Can modify state | ✅ Yes (via state.set()) |
❌ No (read-only) | ✅ Yes (via set()) |
Use listen() when you need to react to state changes with side effects (operations that don't produce state).
Common use cases:
- Logging/debugging state changes
- Sending analytics events
- Syncing with localStorage or databases
- Showing notifications
- Making API calls when state changes
Example:
# Listen to user login to track analytics
page.state.listen("user", lambda user: send_analytics({
"event": "user_login",
"user_id": user["id"] if user else None
}))
# Debug state changes
page.state.listen("count", lambda value: print(f"Count changed: {value}"))Key characteristics:
- Listens to ONE atom at a time
- Does NOT create new state
- Executes immediately when the atom changes
Use selector() when you need to compute a value based on other atoms.
Common use cases:
- Form validation (checking multiple fields)
- Calculations (totals, averages, conversions)
- Filtering/mapping lists
- Formatting data (combining first + last name)
- Combining multiple atoms into one value
Example:
# Validate form - automatically recalculates when email OR password changes
@page.state.selector("form_valid")
def validate_form(get):
email = get("email")
password = get("password")
return bool(email and password and "@" in email and len(password) >= 6)
# Calculate cart total - recomputes when items change
@page.state.selector("cart_total")
def calculate_total(get):
items = get("cart_items")
return sum(item["price"] * item["quantity"] for item in items)Key characteristics:
- Tracks ALL dependencies automatically
- Creates a new atom with the computed value
- Memoized - only recalculates when dependencies change
- 5-20x faster than manual listeners for derivations
Why it's better than listen():
# ❌ With listen() - verbose, manual, no cache
def update_total(value):
items = state.get("cart_items")
tax = state.get("tax_rate")
total = sum(i["price"] for i in items) * (1 + tax)
state.set("total", total)
page.state.listen("cart_items", update_total)
page.state.listen("tax_rate", update_total) # Duplicate code!
# ✅ With selector() - clean, automatic, cached
@page.state.selector("total")
def calculate_total(get):
items = get("cart_items")
tax = get("tax_rate")
return sum(i["price"] for i in items) * (1 + tax)Use action() when you need to execute complex operations that read and/or modify multiple atoms.
Common use cases:
- Login/authentication workflows
- Saving forms (validation + API call + state updates)
- Checkout process (multiple state changes)
- Resetting application state
- Complex multi-step operations
Two ways to use:
@page.state.action
def submit_form(get, set):
# Read state
email = get("email")
password = get("password")
# Validation
if not email or not password:
set("error", "Fill all fields")
return
# Update multiple atoms
set("loading", True)
set("error", None)
# Business logic
result = authenticate(email, password)
if result.success:
set("user", result.user)
set("logged_in", True)
else:
set("error", result.message)
set("loading", False)
# Call it manually
on_click=lambda _: submit_form()def submit_form_fn(get, set, args):
email = get("email")
# ... same logic ...
submit_form = fa.Action(submit_form_fn)
# Call with state
on_click=lambda _: submit_form.run(page.state)
# Or async
await submit_form.run_async(page.state, args={"extra": "data"})Key characteristics:
- Called manually (not reactive)
- Can read and modify multiple atoms
- Supports sync and async operations
- Perfect for organizing complex workflows
Here's a real-world example using all three together:
import flet as ft
import flet_asp as fa
class AuthResult:
def __init__(self, success, user=None, message=None):
self.success = success
self.user = user
self.message = message
def authenticate(email, password):
import time
time.sleep(2) # Simulates API delay
return AuthResult(
success=True,
user={
"id": 1,
"name": email
}
)
def send_analytics(param):
print(param)
def main(page: ft.Page):
state = fa.get_state_manager(page)
# Atoms - raw data
state.atom("email", "")
state.atom("password", "")
state.atom("user", None)
state.atom("loading", False)
# Selector - derived state (form validation)
@state.selector("form_valid")
def validate_form(get):
"""Automatically recalculates when email or password changes"""
email = get("email")
password = get("password")
if not email or not password:
return False
if "@" in get("email"):
return True
return False
# Listen - side effect (analytics)
state.listen("user", lambda user: send_analytics({
"event": "login",
"user_id": user["id"] if user else None
}))
# Listen - reset user when fields are cleared
def reset_user_on_clear(value):
# If email or password is cleared, reset user
email = state.get("email")
password = state.get("password")
if (not email or not password) and state.get("user"):
state.set("user", None)
state.listen("email", reset_user_on_clear)
state.listen("password", reset_user_on_clear)
# Action - business logic (login workflow)
@state.action
def login(get, set):
"""Executes when user clicks login button"""
set("loading", True)
email = get("email")
password = get("password")
# Call API
result = authenticate(email, password)
if result.success:
set("user", result.user) # ← This triggers the listen() above!
else:
set("error", result.message)
set("loading", False)
# UI
email_ref = ft.Ref[ft.TextField]()
password_ref = ft.Ref[ft.TextField]()
login_btn = ft.Ref[ft.ElevatedButton]()
loading_spinner = ft.Ref[ft.ProgressRing]()
status_text = ft.Ref[ft.Text]()
page.add(
ft.Column([
ft.Text("Login Form", size=24, weight=ft.FontWeight.BOLD),
ft.Divider(),
ft.TextField(ref=email_ref, label="Email"),
ft.TextField(ref=password_ref, label="Password", password=True),
ft.ElevatedButton(
ref=login_btn,
text="Login",
on_click=lambda _: login(), # ← Calls action manually
visible=False # ← Controlled by selector
),
ft.Row([
ft.ProgressRing(ref=loading_spinner, visible=False, width=20, height=20),
ft.Text(ref=status_text, color=ft.Colors.GREEN, size=16)
])
])
)
# Selector to create the status message (derived from the user)
@state.selector("status_message")
def get_status_message(get):
user = get("user")
if user:
return f"Welcome, {user['name']}!"
return ""
# Bindings
state.bind_two_way("email", email_ref)
state.bind_two_way("password", password_ref)
state.bind("form_valid", login_btn, prop="visible") # ← Selector binding
state.bind("loading", loading_spinner, prop="visible") # ← Loading spinner
state.bind("status_message", status_text, prop="value") # ← Status message (selector)
ft.app(target=main)What's happening:
- Selector validates the form and creates the status message automatically
- Binding shows/hides button based on validation
- Action handles login when button is clicked
- Listen sends analytics when user state changes
✅ DO:
- Use
selector()for any derived/computed state - Use
listen()for side effects (logs, analytics, storage sync) - Use
action()for complex workflows with multiple state changes - Combine all three when appropriate
❌ DON'T:
- Don't use
listen()to create derived state (useselector()instead) - Don't use
action()for simple one-line state updates - Don't duplicate listeners when a selector can track dependencies automatically
- Don't forget that selectors are memoized (much faster!)
Create reusable components with encapsulated state.
import flet as ft
import flet_asp as fa
class Counter(ft.Column):
"""Reusable counter component with its own state."""
def __init__(self, page: ft.Page, counter_id: str, title: str):
super().__init__()
self.page = page
self.counter_id = counter_id
self.value_text = ft.Ref[ft.Text]()
# Initialize state for this counter
page.state.atom(f"{counter_id}_count", 0)
self.controls = [
ft.Container(
content=ft.Column([
ft.Text(title, size=20, weight=ft.FontWeight.BOLD),
ft.Text(ref=self.value_text, size=40, color=ft.Colors.BLUE),
ft.Row([
ft.IconButton(
icon=ft.Icons.REMOVE,
on_click=self.decrement
),
ft.IconButton(
icon=ft.Icons.ADD,
on_click=self.increment
)
], alignment=ft.MainAxisAlignment.CENTER)
], horizontal_alignment=ft.CrossAxisAlignment.CENTER),
padding=20,
border=ft.border.all(2, ft.Colors.BLUE),
border_radius=10
)
]
def did_mount(self):
# Bind when component is mounted
self.page.state.bind(f"{self.counter_id}_count", self.value_text)
def increment(self, e):
current = self.page.state.get(f"{self.counter_id}_count")
self.page.state.set(f"{self.counter_id}_count", current + 1)
def decrement(self, e):
current = self.page.state.get(f"{self.counter_id}_count")
self.page.state.set(f"{self.counter_id}_count", current - 1)
def main(page: ft.Page):
fa.get_state_manager(page)
page.add(
ft.Column([
ft.Text("Multiple Counters", size=30),
ft.Row([
Counter(page, "counter1", "Counter A"),
Counter(page, "counter2", "Counter B"),
Counter(page, "counter3", "Counter C")
])
])
)
ft.app(target=main)State persists across navigation automatically!
import flet as ft
import flet_asp as fa
def home_screen(page: ft.Page):
"""Home screen with shared state."""
count_text = ft.Ref[ft.Text]()
def go_to_settings(e):
page.views.clear()
page.views.append(settings_screen(page))
page.update()
return ft.View(
"/",
[
ft.AppBar(title=ft.Text("Home"), bgcolor=ft.Colors.BLUE),
ft.Column([
ft.Text("Counter Value:", size=20),
ft.Text(ref=count_text, size=50, color=ft.Colors.BLUE),
ft.ElevatedButton("Go to Settings", on_click=go_to_settings)
])
]
)
def settings_screen(page: ft.Page):
"""Settings screen - modifies shared state."""
def increment(e):
current = page.state.get("count")
page.state.set("count", current + 1)
def go_back(e):
page.views.clear()
page.views.append(home_screen(page))
page.update()
return ft.View(
"/settings",
[
ft.AppBar(title=ft.Text("Settings"), bgcolor=ft.Colors.GREEN),
ft.Column([
ft.Text("Modify Counter", size=20),
ft.ElevatedButton("Increment", on_click=increment),
ft.ElevatedButton("Go Back", on_click=go_back)
])
]
)
def main(page: ft.Page):
fa.get_state_manager(page)
# Shared state across screens
page.state.atom("count", 0)
page.views.append(home_screen(page))
# Bind state after adding view (works with hybrid strategy!)
count_ref = page.views[0].controls[1].controls[1] # Get the count text
page.state.bind("count", ft.Ref[ft.Text]())
ft.app(target=main)For advanced scenarios like testing, multi-window applications, or complex state architectures, you can create a StateManager outside the page scope.
import flet as ft
import flet_asp as fa
# Create global StateManager OUTSIDE the page
global_state = fa.StateManager()
def screen_a(page: ft.Page):
"""Main screen with counter."""
count_ref = ft.Ref[ft.Text]()
def increment(e):
# Use global_state instead of page.state
global_state.set("count", global_state.get("count") + 1)
def go_to_b(e):
page.go("/b")
view = ft.View(
"/",
[
ft.Text("Screen A - Global State", size=24, weight=ft.FontWeight.BOLD),
ft.Text(ref=count_ref, size=40, color=ft.Colors.BLUE_700),
ft.ElevatedButton("Increment", on_click=increment),
ft.ElevatedButton("Go to Screen B", on_click=go_to_b),
],
padding=20,
)
# Bind using global_state
global_state.bind("count", count_ref)
return view
def screen_b(page: ft.Page):
"""Secondary screen displaying the counter."""
def go_back(e):
page.go("/")
return ft.View(
"/b",
[
ft.Text("Screen B - Global State", size=24, weight=ft.FontWeight.BOLD),
ft.Text(f"Counter value: {global_state.get('count')}", size=16),
ft.Text("State is managed globally!", color=ft.Colors.GREEN_700),
ft.ElevatedButton("Go back", on_click=go_back),
],
padding=20,
)
def main(page: ft.Page):
"""App entry point."""
# IMPORTANT: Attach the page to the global StateManager
global_state.page = page
# Initialize atoms
global_state.atom("count", 0)
def route_change(e):
page.views.clear()
if page.route == "/b":
page.views.append(screen_b(page))
else:
page.views.append(screen_a(page))
page.update()
page.on_route_change = route_change
page.go("/")
ft.app(target=main)When to use global state:
| Use Case | Why Global State? |
|---|---|
| Unit Testing | Test state logic without creating a Flet page |
| Multi-Window Apps | Share state between multiple page instances |
| Advanced Architectures | State exists independently of UI lifecycle |
| Framework Integration | Flet-ASP as part of a larger system |
Key differences:
| Aspect | page.state |
global_state |
|---|---|---|
| Creation | fa.get_state_manager(page) |
fa.StateManager() |
| Page binding | Automatic | Manual (global_state.page = page) |
| Scope | Inside main() |
Global (module level) |
| Lifecycle | Managed by page | Manual |
| When to use | ✅ Most cases |
Common pitfalls:
# ❌ WRONG - Forgot to attach page
global_state = fa.StateManager()
def main(page: ft.Page):
global_state.atom("count", 0) # Error: page not attached!
# ✅ CORRECT - Attach page first
global_state = fa.StateManager()
def main(page: ft.Page):
global_state.page = page # Attach first!
global_state.atom("count", 0)Testing example:
import unittest
import flet_asp as fa
# Global state for testing
test_state = fa.StateManager()
class TestMyLogic(unittest.TestCase):
def setUp(self):
test_state._atoms.clear()
test_state.atom("count", 0)
def test_increment(self):
# Test logic without creating a Flet page
test_state.set("count", test_state.get("count") + 1)
self.assertEqual(test_state.get("count"), 1)
def test_computed_value(self):
test_state.atom("double", lambda: test_state.get("count") * 2)
test_state.set("count", 5)
self.assertEqual(test_state.get("double"), 10)For a complete example, see 11.1_global_state_outside.py.
Fetch and compute data asynchronously.
import asyncio
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
fa.get_state_manager(page)
# Base atoms
page.state.atom("user_id", 1)
# Async selector - fetches user data
@page.state.selector("user_data")
async def fetch_user(get):
user_id = get("user_id")
# Simulate API call
await asyncio.sleep(1)
# Mock user data
users = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"},
3: {"name": "Charlie", "email": "charlie@example.com"}
}
return users.get(user_id, {"name": "Unknown", "email": "N/A"})
# UI
user_info = ft.Ref[ft.Text]()
# Selector to format user info (depends on async selector)
@page.state.selector("user_display")
def format_user_display(get):
user_data = get("user_data")
# Async selectors return None while loading
if user_data is None:
return "Loading..."
return f"{user_data['name']} ({user_data['email']})"
def next_user(e):
current_id = page.state.get("user_id")
page.state.set("user_id", (current_id % 3) + 1)
page.add(
ft.Column([
ft.Text("User Profile", size=24),
ft.Text(ref=user_info, size=18),
ft.ElevatedButton("Next User", on_click=next_user)
])
)
# Bind selector to text - no page.update() needed!
page.state.bind("user_display", user_info)
ft.app(target=main)Real-world e-commerce state management.
import flet as ft
import flet_asp as fa
def main(page: ft.Page):
fa.get_state_manager(page)
# State
page.state.atom("cart_items", [])
# Selectors
@page.state.selector("cart_total")
def calculate_total(get):
items = get("cart_items")
total = sum(item["price"] * item["quantity"] for item in items)
return f"Total: ${total:.2f}"
@page.state.selector("cart_count")
def count_items(get):
items = get("cart_items")
count = sum(item["quantity"] for item in items)
return f"Items: {count}"
# Available products
products = [
{"id": 1, "name": "Laptop", "price": 999.99},
{"id": 2, "name": "Mouse", "price": 29.99},
{"id": 3, "name": "Keyboard", "price": 79.99}
]
# UI refs
cart_list = ft.Ref[ft.Column]()
cart_count_text = ft.Ref[ft.Text]()
cart_total_text = ft.Ref[ft.Text]()
def add_to_cart(product):
items = page.state.get("cart_items")
# Check if item already in cart
existing = next((item for item in items if item["id"] == product["id"]), None)
if existing:
# Create new list with updated item (immutable update)
new_items = [
{**item, "quantity": item["quantity"] + 1} if item["id"] == product["id"] else item
for item in items
]
else:
# Create new list with new item
new_items = [*items, {**product, "quantity": 1}]
page.state.set("cart_items", new_items)
def render_cart():
"""
Note: This uses page.update() because we're dynamically creating
control lists. For simple value bindings, use state.bind() instead.
"""
items = page.state.get("cart_items")
cart_list.current.controls = [
ft.ListTile(
title=ft.Text(item["name"]),
subtitle=ft.Text(f"${item['price']:.2f} × {item['quantity']}"),
trailing=ft.Text(f"${item['price'] * item['quantity']:.2f}")
) for item in items
] if items else [ft.Text("Cart is empty")]
cart_list.current.update() # Update only the cart list, not the whole page
# Listen to cart changes (immediate=False to avoid calling before UI is mounted)
page.state.listen("cart_items", lambda _: render_cart(), immediate=False)
# Build UI
page.add(
ft.Row([
# Products column
ft.Column([
ft.Text("Products", size=24),
*[
ft.ElevatedButton(
f"{p['name']} - ${p['price']:.2f}",
on_click=lambda e, product=p: add_to_cart(product)
) for p in products
]
], expand=1),
# Cart column
ft.Column([
ft.Text("Shopping Cart", size=24),
ft.Text(ref=cart_count_text),
ft.Column(ref=cart_list),
ft.Divider(),
ft.Text(ref=cart_total_text, size=20, weight=ft.FontWeight.BOLD)
], expand=1)
])
)
# Bind computed values
page.state.bind("cart_count", cart_count_text, prop="value")
page.state.bind("cart_total", cart_total_text, prop="value")
render_cart()
ft.app(target=main)Flet-ASP includes a hybrid update strategy that ensures bindings work reliably, even when controls are bound before being added to the page.
- Lazy updates - Property is always set (never fails)
- Immediate updates - Tries to update if control is mounted (99% of cases)
- Lifecycle hooks - Hooks into
did_mountfor custom controls - Queue fallback - Retries when
page.update()is called
| Feature | Benefit | Performance Gain |
|---|---|---|
| Free-threading | Process bindings in parallel without GIL | Up to 4x faster for large apps |
| Incremental GC | Smaller garbage collection pauses | 10x smaller pauses (20ms → 2ms) |
| Tail call interpreter | Faster Python execution | 3-5% overall speedup |
Configuration (optional):
from flet_asp.atom import Atom
# For giant apps with 1000+ bindings on Python 3.14+
Atom.MAX_PARALLEL_BINDS = 8
# For small apps or to disable free-threading
Atom.ENABLE_FREE_THREADING = FalseFor more details, see PERFORMANCE.md.
Explore the examples/ folder for complete applications:
Basic Examples:
1.0_counter_atom.py- Simple counter1.1_counter_atom_using_state_alias.py- Counter with page.state2_counter_atom_bind_dynamic.py- Dynamic binding
Intermediate Examples:
3_computed_fullname.py- Computed state4_action_login.py- Async actions5_selector_user_email.py- Selectors6_listen_user_login.py- State listeners7_bind_two_way_textfield.py- Two-way binding8_session_reset_clear.py- State cleanup
Advanced Examples:
9_todo.py- Complete ToDo app10_cart_app.py- Shopping cart11_screen_a_navigation_screen_b.py- Navigation with page.state11.1_global_state_outside.py- Global state outside page scope12_python314_performance.py- Performance demo13_hybrid_binding_advanced.py- Hybrid binding
Atomic Design Examples:
14_atomic_design_dashboard/- Complete dashboard with atoms, molecules, organisms, templates, and pages15_atomic_design_theming/- Theme-aware component library with design tokens16_reactive_atomic_components/- Reactive components with built-in state management17_atomic_design_send_button/- Send button with reactive state management
Flet-ASP is designed from the ground up to work seamlessly with the Atomic Design methodology - a powerful approach for building scalable, maintainable design systems.
Atomic Design is a methodology for creating design systems by breaking down interfaces into fundamental building blocks, inspired by chemistry:
🔬 Atoms → 🧪 Molecules → 🧬 Organisms → 📄 Templates → 📱 Pages
| Atomic Design Layer | Flet-ASP Feature | Example |
|---|---|---|
| Atoms | Reactive state values | page.state.atom("email", "") |
| Molecules | Computed state | @page.state.selector("full_name") |
| Organisms | Actions & workflows | Action(login_function) |
| Templates | State bindings | page.state.bind("count", ref) |
| Pages | Complete screens | Custom controls with encapsulated state |
We provide two comprehensive examples that demonstrate professional design system architecture:
A complete dashboard application showcasing the full Atomic Design hierarchy:
- Atoms: Buttons, inputs, text styles, icons, dividers
- Molecules: Stat cards, menu items, form fields, search bars
- Organisms: Sidebar, top bar, data tables, stats grid
- Templates: Dashboard layouts with different content arrangements
- Pages: Dashboard, analytics, users, orders, settings screens
# Atoms define the foundation
from atoms import heading1, primary_button
# Molecules combine atoms
from molecules import stat_card
# Organisms compose molecules
from organisms import stats_grid
# Templates arrange organisms
from templates import dashboard_template
# Pages bring it all together
from pages import dashboard_pageFeatures:
- ✅ Complete component hierarchy following Atomic Design
- ✅ Real-time data updates with reactive state bindings
- ✅ Navigation with state preservation
- ✅ Reusable components across multiple pages
- ✅ Consistent design language
An advanced example demonstrating design tokens and dynamic theming:
- Design Tokens: Colors, typography, spacing, border radius
- Theme-Aware Atoms: Components that adapt to light/dark modes
- Reactive Theming: Real-time theme switching with flet-asp
- Semantic Colors: Success, warning, error, info states
from theme_tokens import get_theme
from atoms import filled_button, text_field
from molecules import alert, stat_card
# All components automatically adapt to current theme
theme = get_theme()
button = filled_button("Submit") # Uses theme.colors.primaryFeatures:
- ✅ Complete design token system (colors, typography, spacing)
- ✅ Light and dark mode support
- ✅ Theme switching without page reload
- ✅ Semantic color system for alerts and states
- ✅ Professional design system architecture
Components that combine visual structure + reactive state in a single, reusable package:
from reactive_atoms import ReactiveCounter, ReactiveStatCard, ReactiveForm
# Create counter with built-in state!
counter = ReactiveCounter(page, "Counter A", initial_count=0)
page.add(counter.control)
# Interact via clean API
counter.increment() # +1
counter.decrement() # -1
counter.reset() # Set to 0
print(counter.value) # Get current value
# Stat card with auto-updates
users_card = ReactiveStatCard(
page,
title="Total Users",
atom_key="users",
initial_value="1,234",
icon_name=ft.Icons.PEOPLE,
show_trend=True
)
# Update programmatically
users_card.update_with_trend("2,500", "+15%") # ✨ UI updates automatically!Features:
- ✅ Components with built-in reactive state
- ✅ No manual binding needed
- ✅ Clean, intuitive API
- ✅ Encapsulated state management
- ✅ Reusable across projects
🎯 Consistency: Design tokens and atoms ensure uniform styling across your app
🔄 Reusability: Build components once, use them everywhere with different state bindings
📈 Scalability: Add new features by combining existing atoms and molecules
🧪 Testability: Test atoms, molecules, and organisms in isolation
🤝 Collaboration: Designers and developers work with the same component language
⚡ Reactivity: State changes propagate automatically through the component hierarchy
- Atomic Design by Brad Frost - The definitive guide
- Building Design Systems with Atomic Design
- Material Design System - Real-world example
- Flet-ASP Examples - Practical implementations
Join the community to contribute or get help:
If you like this project, please give it a GitHub star ⭐
Contributions and feedback are welcome!
- Fork the repository
- Create a feature branch
- Submit a pull request with detailed explanation
For feedback, open an issue with your suggestions.
Commit your work to the LORD, and your plans will succeed. Proverbs 16:3


