Skip to main content

Flet-ASP โ€“ The Flet Atomic State Pattern is a reactive state management library for Flet.

Project description

flet-asp

Flet ASP - Flet Atomic State Pattern

Main Dev Package version Supported Python versions PyPI Downloads


๐Ÿ“– 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
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?

  1. atom("count", 0) - Creates a reactive piece of state
  2. bind("count", count_text) - Connects state to UI
  3. 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)

    # Create atoms for form fields
    page.state.atom("email", "")
    page.state.atom("password", "")

    # 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":
            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)
        ])
    )

    # Two-way binding: TextField โ†” Atom
    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)

    # 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)

    # 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:
            status_text.current.value = f"Welcome, {user['name']}!"
        else:
            status_text.current.value = "Login failed"
        page.update()

    # Listen to loading state
    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.


๐Ÿค” 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?

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Do you need to COMPUTE a new value      โ”‚
โ”‚ from existing atoms?                    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚
        Yes  โ”‚  No
             โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ Use selector() โ”‚ โ† Automatically creates derived state
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚
             โ”‚
        No   โ”‚
             โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Do you need to REACT automatically      โ”‚
โ”‚ when a single atom changes?             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚
        Yes  โ”‚  No
             โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ Use listen()   โ”‚ โ† For side effects (logs, analytics, etc)
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚
             โ”‚
        No   โ”‚
             โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Do you need to execute complex logic    โ”‚
โ”‚ that modifies multiple atoms?           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
             โ”‚
        Yes  โ”‚
             โ–ผ
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    โ”‚ Use action()   โ”‚ โ† For business logic, workflows
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ’ก 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:

  1. Selector validates the form and creates the status message automatically
  2. Binding shows/hides button based on validation
  3. Action handles login when button is clicked
  4. 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 (use selector() 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]()

    def update_user_info(user_data):
        # Async selectors may return coroutines on first call, check the type
        import inspect
        if inspect.iscoroutine(user_data):
            # Skip coroutine objects - they will be resolved automatically
            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)

    # Listen to selector changes
    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)

    # State
    page.state.atom("cart_items", [])

    # Selectors
    @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)

    # 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:
            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()

    # Listen to cart changes
    page.state.listen("cart_items", lambda _: render_cart())

    # 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:

  1. Lazy updates - Property is always set (never fails)
  2. Immediate updates - Tries to update if control is mounted (99% of cases)
  3. Lifecycle hooks - Hooks into did_mount for custom controls
  4. 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:

Intermediate Examples:

Advanced Examples:

Atomic Design Examples:


๐Ÿงฉ Building Design Systems with Atomic Design

Atomic Design System

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

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

# 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

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

# 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

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


๐ŸŒ 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!

  1. Fork the repository
  2. Create a feature branch
  3. 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

flet_asp-0.3.0.tar.gz (91.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flet_asp-0.3.0-py3-none-any.whl (78.2 kB view details)

Uploaded Python 3

File details

Details for the file flet_asp-0.3.0.tar.gz.

File metadata

  • Download URL: flet_asp-0.3.0.tar.gz
  • Upload date:
  • Size: 91.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.3

File hashes

Hashes for flet_asp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e6b54d180fc642c0c03a7d582b83e9d73636023244c457a489f56268af9c6e23
MD5 1169c002d24b05324b3894e05abaa2ff
BLAKE2b-256 927657898728bb1e36d13550aecee5c1fb7dae2cffeb0d1a62daa5abbed1c497

See more details on using hashes here.

File details

Details for the file flet_asp-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: flet_asp-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 78.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.3

File hashes

Hashes for flet_asp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 447f1f534dcdd0c13bca35e608477b0626123bc3ee9453abcffabe4d28d16ae3
MD5 6ca2b63ebe7e115443116c70fe259d1c
BLAKE2b-256 c5d07faceadfa1b5a9f420e5f9c915cf831e2312c8d4d40ca277dd3ca29dfc6e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page