Replies: 3 comments
-
|
Another aproach, instead passing arguments: from dataclasses import dataclass, field
from typing import Any, Optional, Type, TypeVar
import flet as ft
from pydantic import BaseModel
T = TypeVar("T")
# ============================================
# State Manager - Simulates Reflex's get_state
# ============================================
class StateManager:
"""Global state registry - simulates Reflex's StateManager"""
_registry: dict[Type, Any] = {}
@classmethod
def register_state(cls, state_type: Type[T], instance: T):
"""Register a state instance"""
cls._registry[state_type] = instance
@classmethod
def init_state(cls, state_instance: T) -> T:
"""
Initialize and register a state in one step.
Use this in the root component to set up states.
Usage:
@ft.component
def AppView():
StateManager.init_state(GlobalState())
StateManager.init_state(AppState(users=[...]))
return [...]
"""
state_type = type(state_instance)
state, _ = ft.use_state(state_instance)
cls._registry[state_type] = state
return state
@classmethod
def get_state(cls, state_type: Type[T]) -> Optional[T]:
"""
Get a state instance - similar to Reflex's get_state.
Use this inside observable class methods.
"""
return cls._registry.get(state_type)
@classmethod
def use_state(cls, state_type: Type[T]) -> T:
"""
Hook to access global state with auto-updates.
Use this inside @ft.component functions.
Usage:
@ft.component
def MyComponent():
global_state = StateManager.use_state(GlobalState)
return ft.Text(f"Counter: {global_state.counter}")
"""
state = cls._registry.get(state_type)
if state is None:
raise ValueError(
f"State {state_type.__name__} not registered in StateManager"
)
# Create subscription so component re-renders when state changes
_, _ = ft.use_state(state)
return state
# ============================================
# Domain Model
# ============================================
class User(BaseModel):
"""Domain model - framework agnostic"""
first_name: str
last_name: str
# ============================================
# Counter State
# ============================================
@ft.observable
@dataclass
class CounterState:
"""Counter application metrics"""
counter: int = 0
def increment(self):
self.counter += 1
def decrement(self):
self.counter -= 1
# ============================================
# User State
# ============================================
@ft.observable
@dataclass
class UserState:
"""State for individual user with editing logic"""
user: User
is_editing: bool = False
temp_first_name: str = ""
temp_last_name: str = ""
@property
def first_name(self) -> str:
return self.user.first_name
@property
def last_name(self) -> str:
return self.user.last_name
def start_edit(self):
self.temp_first_name = self.user.first_name
self.temp_last_name = self.user.last_name
self.is_editing = True
def save(self):
self.user.first_name = self.temp_first_name
self.user.last_name = self.temp_last_name
self.is_editing = False
def cancel(self):
self.is_editing = False
def update_temp_first_name(self, value: str):
self.temp_first_name = value
def update_temp_last_name(self, value: str):
self.temp_last_name = value
# ============================================
# App State
# ============================================
@ft.observable
@dataclass
class AppState:
"""Application-level state managing users"""
users: list[UserState] = field(default_factory=list)
new_first_name: str = ""
new_last_name: str = ""
def add_user(self):
"""Add user and update global counter"""
if self.new_first_name.strip() or self.new_last_name.strip():
user = User(first_name=self.new_first_name, last_name=self.new_last_name)
user_state = UserState(user=user)
self.users.append(user_state)
self.new_first_name = ""
self.new_last_name = ""
# ✅ Get state in class methods - like Reflex
global_state = StateManager.get_state(CounterState)
if global_state:
global_state.increment()
def delete_user(self, user_state: UserState):
"""Delete user and update global counter"""
self.users.remove(user_state)
global_state = StateManager.get_state(CounterState)
if global_state:
global_state.decrement()
def start_edit_user(self, user_state: UserState):
user_state.start_edit()
def save_user(self, user_state: UserState):
user_state.save()
def cancel_edit_user(self, user_state: UserState):
user_state.cancel()
def update_new_first_name(self, value: str):
self.new_first_name = value
def update_new_last_name(self, value: str):
self.new_last_name = value
# ============================================
# Components
# ============================================
@ft.component
def GlobalCounter() -> ft.Control:
"""Display global counter - gets state from StateManager"""
# ✅ Use StateManager.use_state in components
counter_state = StateManager.use_state(CounterState)
return ft.Text(f"Total users (adds): {counter_state.counter}", size=20)
@ft.component
def UserView(user_state: UserState) -> ft.Control:
"""Displays a single user"""
# ✅ Get app_state from StateManager
app_state = StateManager.use_state(AppState)
if not user_state.is_editing:
return ft.Row(
[
ft.Text(f"{user_state.first_name} {user_state.last_name}"),
ft.Button(
"Edit", on_click=lambda _: app_state.start_edit_user(user_state)
),
ft.Button(
"Delete", on_click=lambda _: app_state.delete_user(user_state)
),
]
)
return ft.Row(
[
ft.TextField(
label="First Name",
value=user_state.temp_first_name,
on_change=lambda e: user_state.update_temp_first_name(e.control.value),
width=180,
),
ft.TextField(
label="Last Name",
value=user_state.temp_last_name,
on_change=lambda e: user_state.update_temp_last_name(e.control.value),
width=180,
),
ft.Button("Save", on_click=lambda _: app_state.save_user(user_state)),
ft.Button(
"Cancel", on_click=lambda _: app_state.cancel_edit_user(user_state)
),
]
)
@ft.component
def AddUserForm() -> ft.Control:
"""Form to add users - gets state from StateManager"""
# ✅ Use StateManager.use_state
app_state = StateManager.use_state(AppState)
return ft.Row(
[
ft.TextField(
label="First Name",
width=200,
value=app_state.new_first_name,
on_change=lambda e: app_state.update_new_first_name(e.control.value),
),
ft.TextField(
label="Last Name",
width=200,
value=app_state.new_last_name,
on_change=lambda e: app_state.update_new_last_name(e.control.value),
),
ft.Button("Add", on_click=lambda _: app_state.add_user()),
]
)
@ft.component
def UserNameText(user_state: UserState) -> ft.Control:
"""Simple component to display user name"""
return ft.Text(f"{user_state.first_name} {user_state.last_name}")
@ft.component
def UserNameRow() -> ft.Control:
"""Display all user names in a row"""
# ✅ Get state from StateManager
app_state = StateManager.use_state(AppState)
return ft.Row(
[UserNameText(user_state) for user_state in app_state.users],
spacing=50,
)
@ft.component
def AppView() -> list[ft.Control]:
"""Root component - initializes and registers states"""
# ✅ Initialize and register states in one line each
StateManager.init_state(CounterState())
StateManager.init_state(
AppState(
users=[
UserState(user=User(first_name="John", last_name="Doe")),
UserState(user=User(first_name="Jane", last_name="Doe")),
UserState(user=User(first_name="Foo", last_name="Bar")),
]
)
)
return [
GlobalCounter(),
AddUserForm(),
UserNameRow(),
*[
UserView(user_state)
for user_state in StateManager.use_state(AppState).users
],
]
ft.run(lambda page: page.render(AppView)) |
Beta Was this translation helpful? Give feedback.
-
Enhanced State Management Pattern for Flet: Auto-Registration & Session IsolationUpdate to Previous DiscussionThis is an evolution of the Reflex-Inspired State Management Pattern, addressing key limitations and proposing integration into Flet's core to eliminate boilerplate and improve developer experience. 🎯 Problem StatementThe current approach requires manual state initialization in every root component: @ft.component
def AppView():
# ❌ Manual registration required for each state
StateManager.init_state(CounterState())
StateManager.init_state(AppState())
# ... rest of componentIssues:
✨ Proposed Solution: Auto-Registration + Session IsolationKey Improvements1. Automatic State Discovery via
|
Beta Was this translation helpful? Give feedback.
-
|
remove from dataclasses import dataclass, field
from typing import Any, ClassVar, Optional, Type, TypeVar
import flet as ft
from pydantic import BaseModel
T = TypeVar("T")
class StateRegistry:
"""
Internal registry for state classes and instances.
This is the infrastructure layer - not meant to be inherited from.
"""
STATE_KEY_PREFIX = "_state_manager_"
_registered_state_classes: ClassVar[set[Type["State"]]] = set()
@classmethod
def _get_session(cls):
"""Get current session from context"""
return ft.context.page.session
@classmethod
def _get_storage_key(cls, state_type: Type[T]) -> str:
"""Generate storage key for state type"""
return f"{cls.STATE_KEY_PREFIX}{state_type.__name__}"
@classmethod
def register_class(cls, state_class: Type["State"]):
"""Register a state class for auto-initialization"""
cls._registered_state_classes.add(state_class)
@classmethod
def register_instance(cls, state_type: Type[T], instance: T):
"""Register a state instance in current session"""
session = cls._get_session()
storage_key = cls._get_storage_key(state_type)
session.store.set(storage_key, instance)
@classmethod
def get_instance(cls, state_type: Type[T]) -> Optional[T]:
"""Get a state instance from current session"""
try:
session = cls._get_session()
storage_key = cls._get_storage_key(state_type)
return session.store.get(storage_key)
except RuntimeError:
return None
@classmethod
def init_all(cls):
"""Initialize all registered state classes for current session"""
initialized_states = {}
for state_class in cls._registered_state_classes:
instance = state_class()
state, _ = ft.use_state(instance)
cls.register_instance(state_class, state)
initialized_states[state_class.__name__] = state
return initialized_states
class State:
"""
Base class for all global application states.
Usage:
@ft.observable
@dataclass
class AppState(State):
count: int = 0
def increment(self):
self.count += 1
The state will be automatically registered and available via:
app_state = AppState.use() # 🎯 Direct access!
"""
def __init_subclass__(cls, **kwargs):
"""Auto-register when inherited"""
super().__init_subclass__(**kwargs)
StateRegistry.register_class(cls)
@classmethod
def use_state(cls: Type[T]) -> T:
"""
Hook to access this state with auto-updates.
Use this inside @ft.component functions.
Example:
@ft.component
def MyComponent():
app_state = AppState.use() # 🎯 Direct!
return ft.Text(f"Count: {app_state.count}")
"""
state = StateRegistry.get_instance(cls)
if state is None:
raise ValueError(
f"State {cls.__name__} not registered. "
f"Did you call State.init_all() in your root component?"
)
_, _ = ft.use_state(state)
return state
@classmethod
def get_state(cls: Type[T]) -> T:
"""
Get this state instance without subscribing to updates.
Use this inside observable class methods or event handlers.
Example:
def add_user(self):
counter = CounterState.get() # 🎯 Direct!
counter.increment()
Raises:
RuntimeError: If state not initialized (init_all not called)
"""
state = StateRegistry.get_instance(cls)
if state is None:
raise RuntimeError(
f"State {cls.__name__} not initialized. "
f"Make sure State.init_all() was called in your root component."
)
return state
@classmethod
def init_all(cls):
"""
Initialize all registered states. Call once in root component.
Example:
@ft.component
def App(page: ft.Page):
State.init_all()
return [...]
"""
return StateRegistry.init_all()
# ============================================================================
# DOMAIN MODELS
# ============================================================================
class User(BaseModel):
"""Domain model - can be used with databases, APIs, etc."""
first_name: str
last_name: str
# ============================================================================
# OBSERVABLE MODELS (not global states)
# ============================================================================
@ft.observable
@dataclass
class UserObs:
"""Observable model for individual user with editing logic"""
user: User
is_editing: bool = False
temp_first_name: str = ""
temp_last_name: str = ""
@property
def first_name(self) -> str:
return self.user.first_name
@property
def last_name(self) -> str:
return self.user.last_name
def start_edit(self):
"""Initialize editing mode"""
self.temp_first_name = self.user.first_name
self.temp_last_name = self.user.last_name
self.is_editing = True
def save(self):
"""Save changes to the domain model"""
self.user.first_name = self.temp_first_name
self.user.last_name = self.temp_last_name
self.is_editing = False
def cancel(self):
"""Cancel editing without saving"""
self.is_editing = False
# ============================================================================
# GLOBAL STATES (auto-registered via State inheritance)
# ============================================================================
@ft.observable
@dataclass
class CounterState(State):
"""State for a simple counter example"""
count: int = 0
def increment(self):
"""Increment the counter"""
self.count += 1
def decrement(self):
"""Decrement the counter"""
self.count -= 1
@ft.observable
@dataclass
class AppState(State):
"""Application-level state managing the collection"""
users: list[UserObs] = field(default_factory=list)
new_first_name: str = ""
new_last_name: str = ""
def add_user(self):
"""Add a new user to the collection"""
if self.new_first_name.strip() or self.new_last_name.strip():
user = User(first_name=self.new_first_name, last_name=self.new_last_name)
user_obs = UserObs(user=user)
self.users.append(user_obs)
# 🎯 Access other states directly without subscription
counter = CounterState.get_state()
counter.increment()
# Clear form
self.new_first_name = ""
self.new_last_name = ""
def delete_user(self, user_obs: UserObs):
"""Remove a user from the collection"""
counter = CounterState.get_state() # 🎯 Direct access!
self.users.remove(user_obs)
counter.decrement()
# ============================================================================
# COMPONENTS
# ============================================================================
@ft.component
def UserView(user_obs: UserObs) -> ft.Control:
"""Displays a single user - read-only or editing mode"""
app_state = AppState.use_state() # 🎯 Direct access!
if not user_obs.is_editing:
return ft.Row(
[
ft.Text(f"{user_obs.first_name} {user_obs.last_name}"),
ft.Button("Edit", on_click=lambda _: user_obs.start_edit()),
ft.Button("Delete", on_click=lambda _: app_state.delete_user(user_obs)),
]
)
return ft.Row(
[
ft.TextField(
label="First Name",
value=user_obs.temp_first_name,
on_change=lambda e: setattr(
user_obs, "temp_first_name", e.control.value
),
width=180,
),
ft.TextField(
label="Last Name",
value=user_obs.temp_last_name,
on_change=lambda e: setattr(
user_obs, "temp_last_name", e.control.value
),
width=180,
),
ft.Button("Save", on_click=lambda _: user_obs.save()),
ft.Button("Cancel", on_click=lambda _: user_obs.cancel()),
]
)
@ft.component
def AddUserForm() -> ft.Control:
"""Form to add new users"""
app_state = AppState.use_state() # 🎯 Direct access!
return ft.Row(
[
ft.TextField(
label="First Name",
width=200,
value=app_state.new_first_name,
on_change=lambda e: setattr(
app_state, "new_first_name", e.control.value
),
),
ft.TextField(
label="Last Name",
width=200,
value=app_state.new_last_name,
on_change=lambda e: setattr(
app_state, "new_last_name", e.control.value
),
),
ft.Button("Add", on_click=app_state.add_user),
]
)
@ft.component
def UserNameRow() -> ft.Control:
"""Display all user names in a row"""
users = AppState.use_state().users # 🎯 Direct access!
@ft.component
def UserName(user_obs: UserObs) -> ft.Control:
return ft.Text(f"{user_obs.first_name} {user_obs.last_name}")
return ft.Row(
[UserName(user_obs) for user_obs in users],
spacing=50,
)
@ft.component
def AppView(page: ft.Page) -> list[ft.Control]:
"""Root component - auto-initializes all registered states"""
# 🎯 Initialize ALL registered states with one call
State.init_all()
# 🎯 Access them directly using ClassName.use_state()
app_state = AppState.use_state()
counter_state = CounterState.use_state()
return [
ft.Text(f"Session ID: {page.session.id}", size=12, color=ft.Colors.GREY),
ft.Divider(),
AddUserForm(),
UserNameRow(),
ft.Text(f"Total users: {counter_state.count}"),
*[UserView(user_obs) for user_obs in app_state.users],
]
# ============================================================================
# RUN APP
# ============================================================================
ft.run(lambda page: page.render(lambda: AppView(page))) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Overview
This document presents an alternative approach to structuring Flet declarative applications, inspired by the Reflex framework. The key principle is separation of concerns: keeping your domain models clean and framework-agnostic while managing UI state in dedicated observable classes.
Core Principles
1. Domain Models Stay Pure
Your business logic and data models should be independent of the UI framework. Use Pydantic's BaseModel (or any other approach) for models that represent your actual data.
2. State Classes Handle Reactivity
Create
@ft.observablestate classes that wrap your domain models and contain all UI logic and event handlers.3. Components Are Pure Presentation
Components receive state objects and render UI based on them. They contain no business logic - only presentation and delegation to state event handlers.
Complete Example: User Management CRUD
https://docs.flet.dev/cookbook/declarative-vs-imperative-crud-app/#example-2-declarative
Step 1: Domain Model
Step 2: State Classes
Step 3: Components (Pure Presentation)
Key Differences from the Official Examples
1. No Hooks for Business Logic
Official example uses
use_statefor editing state:Our approach moves this to the state class:
2. No Functions Passed as Props
Official example passes functions between components:
Our approach passes state objects:
3. Domain Model Separation
Official example makes the domain model observable:
Our approach keeps them separate:
Critical Rule: Pass Observable Objects, Not Properties
❌ This WILL NOT work: (It doesn't work in the original version either because it expects
@observableto be passed)✅ This WILL work:
Why? When you pass
user_state.first_name, you're passing the string value at that moment in time (e.g., "John"). The component has no connection to the observable. When you passuser_state, you pass a reference to the observable object, so Flet can detect changes and trigger re-renders.Benefits of This Pattern
1. Testability
Your domain models can be tested without any UI framework:
2. Reusability
The same
Usermodel can be used in:3. Clear Separation of Concerns
4. Easier to Reason About
All state mutations happen in one place (state classes), not scattered across components.
5. Scalable
As your app grows, you add more state classes and compose them, rather than nesting hooks and callbacks.
Comparison with Reflex
This pattern closely mirrors Reflex's architecture:
Both frameworks follow the principle: State classes own the data and logic, components own the presentation.
When to Use This Pattern
Use this pattern when:
The official pattern might be better when:
Conclusion
This Reflex-inspired pattern offers a clean, scalable way to structure Flet applications by:
The result is code that's easier to test, maintain, and scale as your application grows.
Beta Was this translation helpful? Give feedback.
All reactions