flet-stack
Simple, intuitive routing with automatic view stacking for Flet applications.
✨ What's New in 0.3.0
Version 0.3.0 brings a major simplification to the API:
- 🎯 Even simpler routing - Views are now just functions that return
ft.Viewobjects - 🔄 Stack navigation - Use
+/routeto stack views,/routeto replace the entire stack - ❌ No more
@ft.component- Just simple functions, no decorator boilerplate - ⚡ Automatic reactivity - State changes trigger re-renders automatically
- 🧹 Cleaner code - Less boilerplate, more straightforward
See the migration guide if upgrading from 0.2.x
Features
- 🎯 Decorator-based routing - Clean
@route()decorator for route definitions - 📚 Stack navigation - Intuitive stack vs replace navigation with "+" prefix
- 🔄 Observable state management - Built-in state with
@ft.observabledataclasses - ⚡ Async support - Handle async data loading with automatic loading indicators
- 🎨 URL parameters - Extract parameters from routes like
/user/{id} - 🚀 Simple setup - Just call
page.render_views(FletStack)in your app - 🔗 No boilerplate - Views are simple functions returning
ft.Viewobjects
Requirements
- Python 3.9+
- Flet >= 0.70.0.dev6281
Installation
From PyPI
pip install flet-stack
From GitHub
pip install git+https://github.com/fasilwdr/flet-stack.git
Install Specific Version
pip install git+https://github.com/fasilwdr/flet-stack.git@v0.3.0
From Source
git clone https://github.com/fasilwdr/flet-stack.git
cd flet-stack
pip install .
Quick Start
import flet as ft
from flet_stack import route, FletStack
import asyncio
# Define your routes with the @route decorator
@route("/")
def home_view():
return ft.View(
appbar=ft.AppBar(title=ft.Text("Home")),
controls=[
ft.Text("Home Page", size=30),
ft.Button(
"Go to Profile",
on_click=lambda _: asyncio.create_task(
ft.context.page.push_route("+/profile")
)
),
]
)
@route("/profile")
def profile_view():
return ft.View(
appbar=ft.AppBar(title=ft.Text("Profile")),
controls=[
ft.Text("Profile Page", size=30),
]
)
# Run your app
ft.run(lambda page: page.render_views(FletStack))
That's it! Clean, simple routing with no boilerplate.
Navigation: Stack vs Replace
flet-stack supports two navigation modes:
Stack Navigation (Add to Stack)
Use the "+" prefix to add a view on top of the current stack:
# Adds /profile on top of the current view
asyncio.create_task(ft.context.page.push_route("+/profile"))
# User can press back to return to previous view
Replace Navigation (Replace Stack)
Use no prefix to replace the entire navigation stack:
# Replaces entire stack with just /home
asyncio.create_task(ft.context.page.push_route("/home"))
# Previous views are cleared - back button goes to previous view in new stack
Common Pattern:
# From home, stack other views
ft.Button("Products", on_click=lambda _: push_route("+/products"))
# From anywhere, return home (clearing stack)
ft.IconButton(icon=ft.Icons.HOME, on_click=lambda _: push_route("/"))
Advanced Usage
URL Parameters
Extract parameters from your routes:
@route("/user/{user_id}")
def user_view(user_id):
return ft.View(
appbar=ft.AppBar(title=ft.Text(f"User {user_id}")),
controls=[
ft.Text(f"User Profile: {user_id}", size=30),
]
)
State Management
Use observable dataclasses to manage component state. State automatically triggers re-renders when methods are called:
from dataclasses import dataclass
@ft.observable
@dataclass
class CounterState:
count: int = 0
def increment(self, e):
self.count += 1
def decrement(self, e):
self.count -= 1
@route("/counter", state_class=CounterState)
def counter_view(state):
return ft.View(
appbar=ft.AppBar(title=ft.Text("Counter")),
controls=[
ft.Text(f"Count: {state.count}", size=30),
ft.Row([
ft.Button("Decrement", on_click=state.decrement),
ft.Button("Increment", on_click=state.increment),
]),
]
)
State automatically triggers re-renders when you call methods like increment() or decrement() - no manual update needed!
Async Data Loading
Load data asynchronously before showing your view:
@ft.observable
@dataclass
class UserState:
user_data: dict = None
async def load_user_data(state, user_id):
# Simulate API call
await asyncio.sleep(1)
state.user_data = {
"id": user_id,
"name": f"User {user_id}",
"email": f"user{user_id}@example.com"
}
@route("/user/{user_id}", state_class=UserState, on_load=load_user_data)
def user_detail_view(state, user_id):
return ft.View(
appbar=ft.AppBar(title=ft.Text(state.user_data['name'])),
controls=[
ft.Text(f"Name: {state.user_data['name']}", size=20),
ft.Text(f"Email: {state.user_data['email']}", size=16),
ft.Text(f"ID: {state.user_data['id']}", size=16),
]
)
While on_load executes, a loading spinner is automatically displayed.
Sync Data Loading
You can also use synchronous loading functions:
def load_item_info(state, category, item_id):
"""Sync data loading"""
state.info = {
"category": category.capitalize(),
"item_id": item_id,
"name": f"{category.capitalize()} Item #{item_id}",
"price": f"${int(item_id) * 10}.99"
}
@route(
"/category/{category}/item/{item_id}",
state_class=ItemState,
on_load=load_item_info
)
def item_view(state, category, item_id):
return ft.View(
appbar=ft.AppBar(title=ft.Text(state.info['name'])),
controls=[
ft.Text(f"{state.info['name']}", size=20),
ft.Text(f"Price: {state.info['price']}", size=18),
]
)
Multiple URL Parameters
Handle routes with multiple parameters:
@route("/category/{category}/item/{item_id}")
def item_view(category, item_id):
return ft.View(
appbar=ft.AppBar(title=ft.Text(f"{category} Items")),
controls=[
ft.Text(f"Category: {category}", size=20),
ft.Text(f"Item ID: {item_id}", size=20),
]
)
Setting Initial Route
You can start your app at any route instead of the default /:
def main(page: ft.Page):
page.title = "My App"
page.route = "/login" # Start at login page
page.render_views(FletStack)
ft.run(main)
API Reference
@route Decorator
@route(path: str, state_class: Type = None, on_load: Optional[Callable] = None)
- path: The route path for this view (e.g.,
/,/user/{user_id}) - state_class: Optional dataclass decorated with
@ft.observablefor state management - on_load: Optional function to call before rendering (can be async)
- Parameters are automatically injected based on function signature
- Can accept:
state(if state_class provided) and any URL parameters - While executing, a loading view is displayed automatically
FletStack Component
Main component that manages the routing stack and renders views.
# Option 1: Direct render
ft.run(lambda page: page.render_views(FletStack))
# Option 2: In main function
def main(page: ft.Page):
page.title = "My App"
page.route = "/login" # Optional: Set initial route
page.render_views(FletStack)
ft.run(main)
Navigation
Use asyncio.create_task with ft.context.page.push_route:
# Stack navigation - add to current stack
asyncio.create_task(ft.context.page.push_route("+/profile"))
# Replace navigation - replace entire stack
asyncio.create_task(ft.context.page.push_route("/"))
# In button click handler
ft.Button(
"Go to Profile",
on_click=lambda _: asyncio.create_task(
ft.context.page.push_route("+/profile")
)
)
Examples
Check the examples/ directory for more detailed examples:
basic_example.py- Simple routing and navigationadvanced_example.py- State management, async loading, and URL parameters
How It Works
flet-stack provides a FletStack component that:
- Registers all
@routedecorated functions - Manages a navigation stack with stack vs replace modes
- Handles state management with observable dataclasses and automatic re-renders
- Manages async/sync loading with automatic progress indicators
- Renders views with proper navigation support
- Supports custom initial routes via
page.route - Isolates state per route instance for parameterized routes
Migration from 0.2.x
If you're upgrading from version 0.2.x, here are the key changes:
1. Decorator Renamed
# Before (0.2.x)
from flet_stack import view
# After (0.3.0)
from flet_stack import route
2. Views Return ft.View Objects
Views no longer return lists of controls wrapped in @ft.component. They now return ft.View objects directly:
# Before (0.2.x)
@view("/profile", appbar=ft.AppBar())
@ft.component
def profile_view():
return [
ft.Text("Profile"),
ft.Button("Click me")
]
# After (0.3.0)
@route("/profile")
def profile_view():
return ft.View(
appbar=ft.AppBar(),
controls=[
ft.Text("Profile"),
ft.Button("Click me")
]
)
3. Stack Navigation Syntax
Use the "+" prefix for stacking views:
# Before (0.2.x) - always stacked
asyncio.create_task(ft.context.page.push_route("/products"))
# After (0.3.0) - explicit stack vs replace
asyncio.create_task(ft.context.page.push_route("+/products")) # Stack
asyncio.create_task(ft.context.page.push_route("/")) # Replace
4. Simplified on_load
on_load no longer accepts page or view parameters:
# Before (0.2.x)
async def load_user(state, view, user_id):
state.user = fetch_user(user_id)
view.appbar = ft.AppBar(title=ft.Text(state.user['name']))
# After (0.3.0)
async def load_user(state, user_id):
state.user = fetch_user(user_id)
# Set appbar directly in view function
5. No More @ft.component
Simply remove the @ft.component decorator:
# Before (0.2.x)
@view("/counter", state_class=CounterState)
@ft.component
def counter_view(state):
return [ft.Text(f"Count: {state.count}")]
# After (0.3.0)
@route("/counter", state_class=CounterState)
def counter_view(state):
return ft.View(
controls=[ft.Text(f"Count: {state.count}")]
)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
Built on top of the amazing Flet framework by Feodor Fitsner.
Support
If you encounter any issues or have questions:
- Open an issue on GitHub
- Check the examples directory
- Read the Flet documentation
Metadata
Release files for flet-stack 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| flet_stack-0.3.0.tar.gz | 16.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flet_stack-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 26.7 kB
Release files / flet_stack-0.3.0.tar.gz
| Download URL | flet_stack-0.3.0.tar.gz |
|---|---|
| Size | 16.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
facd2d23b419a996867423cf80c92dbd057c6c8b6eff25e4bfdce04931700801
|
|
BLAKE2b-256 checksum How to use checksums |
a0c4108ea98b842a0ab3c1d8c7197cb92ddf8069baf781daaf3cddc70f0e471d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Release files / flet_stack-0.3.0-py3-none-any.whl
| Download URL | flet_stack-0.3.0-py3-none-any.whl |
|---|---|
| Size | 10.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
378ad811f068dfcf3863378c0b4ccf08ee2703dce4cac8da5c8389d20a4cb770
|
|
BLAKE2b-256 checksum How to use checksums |
75d4b22b9be1cbd1250930c6de25b54b7e3285348a37f1f5102a473e54272666
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Security for flet-stack 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.