
Flet ASP - Flet Atomic State Pattern
📖 Overview
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 state
Selector – derived/computed state
Action – handles async workflows like login, fetch, etc.
📦 Installation
Install using your package manager of choice:
pip install flet-asp
poetry add flet-asp
uv add flet-asp
✨ Key Features
✅ 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
🚀 Quick Start
1. Basic Counter (Your First Atom)
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):
fa.get_state_manager(page)
page.state.atom("count", 0)
count_text = ft.Ref[ft.Text]()
def increment(e):
current = page.state.get("count")
page.state.set("count", current + 1)
page.add(
ft.Column([
ft.Text("Counter", size=30),
ft.Text(ref=count_text, size=50),
ft.ElevatedButton("Increment", on_click=increment)
])
)
page.state.bind("count", count_text)
ft.app(target=main)
What's happening here?
atom("count", 0) - Creates a reactive piece of state
bind("count", count_text) - Connects state to UI
set("count", value) - Updates state → UI updates automatically!
2. Form with Two-Way Binding
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)
page.state.atom("email", "")
page.state.atom("password", "")
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":
message_text.current.value = f"Welcome, {email}!"
else:
message_text.current.value = "Invalid credentials"
page.update()
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)
])
)
page.state.bind_two_way("email", email_field)
page.state.bind_two_way("password", password_field)
ft.app(target=main)
Key concept: bind_two_way() keeps the TextField and atom in perfect sync!
3. Computed State with Selectors
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)
page.state.atom("first_name", "John")
page.state.atom("last_name", "Doe")
@page.state.selector("full_name")
def compute_full_name(get):
return f"{get('first_name')} {get('last_name')}"
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)
])
)
page.state.bind_two_way("first_name", first_field)
page.state.bind_two_way("last_name", last_field)
page.state.bind("full_name", full_name_text)
ft.app(target=main)
Magic! The full name updates automatically when first or last name changes.
4. Async Operations with Actions
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)
async def login_action(get, set_value, params):
set_value("loading", True)
await asyncio.sleep(2)
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)
login = fa.Action(login_action)
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:
status_text.current.value = f"Welcome, {user['name']}!"
else:
status_text.current.value = "Login failed"
page.update()
def on_loading_change(is_loading):
status_text.current.value = "Logging in..." if is_loading else ""
page.update()
page.state.listen("loading", on_loading_change)
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)
])
)
ft.app(target=main)
Actions encapsulate complex async logic in a testable, reusable way.
📚 Advanced Usage
Custom Controls with Reactive State
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]()
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):
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)
Navigation with State Preservation
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)
page.state.atom("count", 0)
page.views.append(home_screen(page))
count_ref = page.views[0].controls[1].controls[1]
page.state.bind("count", ft.Ref[ft.Text]())
ft.app(target=main)
Global State Outside Page Scope
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
global_state = fa.StateManager()
def screen_a(page: ft.Page):
"""Main screen with counter."""
count_ref = ft.Ref[ft.Text]()
def increment(e):
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,
)
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."""
global_state.page = page
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:
| 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:
| 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 | ⚠️ Specific scenarios |
Common pitfalls:
global_state = fa.StateManager()
def main(page: ft.Page):
global_state.atom("count", 0)
global_state = fa.StateManager()
def main(page: ft.Page):
global_state.page = page
global_state.atom("count", 0)
Testing example:
import unittest
import flet_asp as fa
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_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.
Complex Selectors with Async Data
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)
page.state.atom("user_id", 1)
@page.state.selector("user_data")
async def fetch_user(get):
user_id = get("user_id")
await asyncio.sleep(1)
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"})
user_info = ft.Ref[ft.Text]()
def update_user_info(user_data):
import inspect
if inspect.iscoroutine(user_data):
return
if user_data:
user_info.current.value = f"{user_data['name']} ({user_data['email']})"
else:
user_info.current.value = "Loading..."
page.update()
def next_user(e):
current_id = page.state.get("user_id")
page.state.set("user_id", (current_id % 3) + 1)
page.state.listen("user_data", update_user_info)
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)
])
)
ft.app(target=main)
Shopping Cart Example
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)
page.state.atom("cart_items", [])
@page.state.selector("cart_total")
def calculate_total(get):
items = get("cart_items")
return sum(item["price"] * item["quantity"] for item in items)
@page.state.selector("cart_count")
def count_items(get):
items = get("cart_items")
return sum(item["quantity"] for item in items)
products = [
{"id": 1, "name": "Laptop", "price": 999.99},
{"id": 2, "name": "Mouse", "price": 29.99},
{"id": 3, "name": "Keyboard", "price": 79.99}
]
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")
existing = next((item for item in items if item["id"] == product["id"]), None)
if existing:
existing["quantity"] += 1
else:
items.append({**product, "quantity": 1})
page.state.set("cart_items", items)
def render_cart():
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")]
page.update()
page.state.listen("cart_items", lambda _: render_cart())
page.add(
ft.Row([
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),
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)
])
)
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)
⚡ Performance & Python 3.14+
Flet-ASP includes a hybrid update strategy that ensures bindings work reliably, even when controls are bound before being added to the page.
Hybrid Strategy:
- 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_mount for custom controls
- Queue fallback - Retries when
page.update() is called
Python 3.14+ Optimizations:
| 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
Atom.MAX_PARALLEL_BINDS = 8
Atom.ENABLE_FREE_THREADING = False
For more details, see PERFORMANCE.md.
📁 More Examples
Explore the examples/ folder for complete applications:
Basic Examples:
Intermediate Examples:
Advanced Examples:
Atomic Design Examples:
🧩 Building Design Systems with Atomic Design
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.
What is Atomic Design?
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
How Flet-ASP Maps to Atomic Design
| 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 |
Real-World Atomic Design with Flet-ASP
We provide two comprehensive examples that demonstrate professional design system architecture:
📊 Example 14: Dashboard Design System
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
from atoms import heading1, primary_button
from molecules import stat_card
from organisms import stats_grid
from templates import dashboard_template
from pages import dashboard_page
Features:
- ✅ 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
View Example →
🎨 Example 15: Theme-Aware Component Library
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
theme = get_theme()
button = filled_button("Submit")
Features:
- ✅ 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
View Example →
⚛️ Example 16: Reactive Atomic Components
Components that combine visual structure + reactive state in a single, reusable package:
from reactive_atoms import ReactiveCounter, ReactiveStatCard, ReactiveForm
counter = ReactiveCounter(page, "Counter A", initial_count=0)
page.add(counter.control)
counter.increment()
counter.decrement()
counter.reset()
print(counter.value)
users_card = ReactiveStatCard(
page,
title="Total Users",
atom_key="users",
initial_value="1,234",
icon_name=ft.Icons.PEOPLE,
show_trend=True
)
users_card.update_with_trend("2,500", "+15%")
Features:
- ✅ Components with built-in reactive state
- ✅ No manual binding needed
- ✅ Clean, intuitive API
- ✅ Encapsulated state management
- ✅ Reusable across projects
View Example →
Why Atomic Design + Flet-ASP?
🎯 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
Learn More About Atomic Design
Join the community to contribute or get help:
⭐ Support
If you like this project, please give it a GitHub star ⭐
🤝 Contributing
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