Flet-ASP โ The Flet Atomic State Pattern is a reactive state management library for Flet.
Project description
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 stateSelectorโ derived/computed stateActionโ handles async workflows like login, fetch, etc.
๐ฆ Installation
Install using your package manager of choice:
# Pip
pip install flet-asp
# Poetry
poetry add flet-asp
# UV
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):
# 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!
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)
# 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!
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)
# 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.
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)
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.
๐ค Listen, Selector, and Action: When to use each one?
Flet-ASP provides three powerful tools for managing reactive state. Understanding when to use each one is key to writing clean, performant code.
๐ Quick Comparison
| 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()) |
๐ listen() - For Side Effects
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
๐ selector() - For Derived State
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)
โก action() - For Business Logic
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:
Decorator style (recommended):
@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()
Direct instantiation (advanced):
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
๐ฏ Decision Tree: Which One To Use?
๐ก Combining All Three
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
๐ Best Practices
โ 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!)
๐ 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]()
# 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)
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)
# 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)
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
# 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 | โ ๏ธ Specific scenarios |
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.
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)
# 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)
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)
# 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)
โก 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_mountfor custom controls - Queue fallback - Retries when
page.update()is called
Python 3.14+ Optimizations:
| 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 = False
For more details, see PERFORMANCE.md.
๐ More Examples
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
๐งฉ 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
| 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 |
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
# 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_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
๐จ 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
# All components automatically adapt to current theme
theme = get_theme()
button = filled_button("Submit") # Uses theme.colors.primary
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
โ๏ธ Example 16: Reactive Atomic Components
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
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
- 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
๐ Community
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flet_asp-0.3.1.tar.gz.
File metadata
- Download URL: flet_asp-0.3.1.tar.gz
- Upload date:
- Size: 58.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13f0965c9026b3e661885626813e0f70a690e2c2d49b0976999e1e208802b70c
|
|
| MD5 |
f56da552d44b4260c4f717b393598e2c
|
|
| BLAKE2b-256 |
363e764505a6df05c8eaa884cbb70e9a959786624234087c5a4f4a3608efd99b
|
File details
Details for the file flet_asp-0.3.1-py3-none-any.whl.
File metadata
- Download URL: flet_asp-0.3.1-py3-none-any.whl
- Upload date:
- Size: 28.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d92d7e61e3cc939836524e7c98e6550070391cd02c582b7488ff68fc9989d728
|
|
| MD5 |
f7dfd85971f843e17af7ace90e8678ba
|
|
| BLAKE2b-256 |
dc1ae4de60e536d16a3701da11e337eb43562a3fe809ff7abb9ba36c80b7031a
|