Skip to content

Commit ee354a5

Browse files
committed
Release v0.1.5
- Decorator-based Route Registration
1 parent 53993c7 commit ee354a5

File tree

4 files changed

+131
-36
lines changed

4 files changed

+131
-36
lines changed

README.md

+15-16
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,11 @@ pip install flet-model
2323

2424
```python
2525
import flet as ft
26-
from flet_model import Model, Router
26+
from flet_model import Model, route
2727

2828

29+
@route('home')
2930
class HomeModel(Model):
30-
route = 'home'
31-
3231
# Layout configuration
3332
vertical_alignment = ft.MainAxisAlignment.CENTER
3433
horizontal_alignment = ft.CrossAxisAlignment.CENTER
@@ -51,9 +50,8 @@ class HomeModel(Model):
5150
self.page.go('/home/profile')
5251

5352

53+
@route('profile')
5454
class ProfileModel(Model):
55-
route = 'profile'
56-
5755
# Layout configuration
5856
vertical_alignment = ft.MainAxisAlignment.CENTER
5957
horizontal_alignment = ft.CrossAxisAlignment.CENTER
@@ -74,11 +72,8 @@ class ProfileModel(Model):
7472

7573
def main(page: ft.Page):
7674
page.title = "Flet Model Demo"
77-
Router(
78-
{'home': HomeModel(page)},
79-
{'profile': ProfileModel(page)},
80-
)
81-
page.go(page.route)
75+
# Router is automatically initialized
76+
page.go('/home')
8277

8378

8479
ft.app(target=main)
@@ -92,6 +87,7 @@ ft.app(target=main)
9287
# Navigate with data
9388
self.page.go('/products#id=123&category=electronics')
9489

90+
@route('products')
9591
class ProductModel(Model):
9692
def init(self):
9793
# Access route data
@@ -102,6 +98,7 @@ class ProductModel(Model):
10298
### 2. Navigation Drawers
10399

104100
```python
101+
@route('drawer-demo')
105102
class DrawerModel(Model):
106103
drawer = ft.NavigationDrawer(
107104
controls=[
@@ -131,6 +128,7 @@ class DrawerModel(Model):
131128
### 3. Event Handlers and Lifecycle Hooks
132129

133130
```python
131+
@route('events')
134132
class EventModel(Model):
135133
def init(self):
136134
# Called before view creation
@@ -152,6 +150,7 @@ class EventModel(Model):
152150
### 4. Floating Action Button
153151

154152
```python
153+
@route('fab-demo')
155154
class FABModel(Model):
156155
floating_action_button = ft.FloatingActionButton(
157156
icon=ft.Icons.ADD,
@@ -163,6 +162,7 @@ class FABModel(Model):
163162
### 5. Bottom Navigation
164163

165164
```python
165+
@route('navigation')
166166
class NavigationModel(Model):
167167
navigation_bar = ft.NavigationBar(
168168
destinations=[
@@ -176,6 +176,7 @@ class NavigationModel(Model):
176176
### 6. Overlay Controls
177177

178178
```python
179+
@route('overlay')
179180
class OverlayModel(Model):
180181
overlay_controls = [
181182
ft.Banner(
@@ -194,8 +195,8 @@ class OverlayModel(Model):
194195
### 7. Fullscreen Dialogs
195196

196197
```python
198+
@route('dialog')
197199
class DialogModel(Model):
198-
route = "dialog"
199200
fullscreen_dialog = True
200201

201202
controls = [
@@ -214,7 +215,7 @@ Here's a complete example of a todo application using Flet Model:
214215

215216
```python
216217
import flet as ft
217-
from flet_model import Model, Router
218+
from flet_model import Model, route
218219
from typing import List
219220

220221

@@ -224,8 +225,8 @@ class TodoItem:
224225
self.completed = completed
225226

226227

228+
@route('todo')
227229
class TodoModel(Model):
228-
route = "todo"
229230
todos: List[TodoItem] = []
230231

231232
appbar = ft.AppBar(
@@ -275,9 +276,7 @@ class TodoModel(Model):
275276

276277

277278
def main(page: ft.Page):
278-
Router(
279-
{'todo': TodoModel(page)}
280-
)
279+
# No manual router initialization needed
281280
page.go('todo')
282281

283282

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "flet-model"
7-
version = "0.1.4"
7+
version = "0.1.5"
88
description = "A Model-based router for Flet applications that simplifies the creation of multi-page applications"
99
readme = "README.md"
1010
authors = [{ name = "Fasil", email = "fasilwdr@hotmail.com" }]

src/flet_model/__init__.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from .model import Model
2-
from .router import Router
2+
from .router import Router, route
33

4-
__version__ = "0.1.4"
4+
__version__ = "0.1.5"
55

6-
__all__ = ["Model", "Router"]
6+
__all__ = ["Model", "Router", "route"]

src/flet_model/router.py

+112-16
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,44 @@
1-
from typing import Dict, Optional, Type, Any
1+
from typing import Dict, Optional, Type, Any, Callable, TypeVar, List, Tuple
22
import urllib.parse
33
import flet as ft
44
from .model import Model
55

6+
# Type variable for decorator return type annotation
7+
T = TypeVar('T', bound=Type[Model])
8+
9+
# Global storage for route registrations
10+
_pending_registrations: List[Tuple[str, Type[Model]]] = []
11+
12+
13+
def route(route_path: str) -> Callable[[T], T]:
14+
"""
15+
Decorator for registering a model class with a specific route.
16+
17+
Example:
18+
@route('todo')
19+
class TodoModel(Model):
20+
# model implementation
21+
22+
Args:
23+
route_path: The route path to register
24+
25+
Returns:
26+
The decorated model class
27+
"""
28+
29+
def decorator(model_class: T) -> T:
30+
if not issubclass(model_class, Model):
31+
raise TypeError(f"Class {model_class.__name__} must inherit from Model")
32+
33+
# Store the route in the class for reference
34+
model_class.route = route_path
35+
36+
# Save for deferred registration
37+
_pending_registrations.append((route_path, model_class))
38+
return model_class
39+
40+
return decorator
41+
642

743
class Router:
844
"""Router class for handling navigation in Flet applications."""
@@ -11,22 +47,29 @@ class Router:
1147
_routes: Dict[str, Model] = {}
1248
_page: Optional[ft.Page] = None
1349
_view_cache: Dict[str, ft.View] = {}
50+
_initialized: bool = False
1451

1552
def __new__(cls, *args, **kwargs):
1653
if cls._instance is None:
1754
cls._instance = super(Router, cls).__new__(cls)
1855
return cls._instance
1956

2057
def __init__(self, *route_maps: Dict[str, Model]):
21-
if not self._routes:
22-
self._routes = {}
23-
for route_map in route_maps:
24-
self._routes.update(route_map)
58+
if self._initialized:
59+
return
60+
61+
self._routes = {}
2562

26-
if route_maps and list(route_maps[0].values()):
27-
first_model = list(route_maps[0].values())[0]
28-
self._page = first_model.page
29-
self._setup_routing()
63+
# Add any routes from explicit dictionaries
64+
for route_map in route_maps:
65+
self._routes.update(route_map)
66+
67+
# If we have routes and a page from dict-based initialization
68+
if route_maps and list(route_maps[0].values()):
69+
first_model = list(route_maps[0].values())[0]
70+
self._page = first_model.page
71+
self._setup_routing()
72+
self._initialized = True
3073

3174
def _parse_route_and_hash(self, route: str) -> tuple[list[str], dict[str, dict]]:
3275
parts = route.split('/')
@@ -47,7 +90,6 @@ def _parse_route_and_hash(self, route: str) -> tuple[list[str], dict[str, dict]]
4790

4891
return route_parts, hash_data
4992

50-
5193
def _setup_routing(self) -> None:
5294
"""Set up route handling and initialize default route."""
5395
if not self._page:
@@ -56,11 +98,6 @@ def _setup_routing(self) -> None:
5698
self._page.on_route_change = self._handle_route_change
5799
self._page.on_view_pop = self._handle_view_pop
58100

59-
if not self._page.route or self._page.route == '/':
60-
default_route = next(iter(self._routes.keys()))
61-
self._page.route = default_route
62-
self._page.go(default_route)
63-
64101
def _handle_route_change(self, e: ft.RouteChangeEvent) -> None:
65102
route_parts, hash_data = self._parse_route_and_hash(self._page.route.lstrip('/'))
66103
self._page.views.clear()
@@ -104,4 +141,63 @@ def get_current_model(cls) -> Optional[Model]:
104141
return None
105142

106143
current_route = cls._instance._page.route.split('/')[-1]
107-
return cls._instance._routes.get(current_route)
144+
return cls._instance._routes.get(current_route)
145+
146+
# Auto-initialize with the page when app starts
147+
@classmethod
148+
def initialize(cls, page: ft.Page) -> None:
149+
"""
150+
Initialize the router with a page and register any pending routes.
151+
This also sets up the route handlers.
152+
153+
Args:
154+
page: The flet Page instance
155+
"""
156+
global _pending_registrations
157+
158+
# Create instance if it doesn't exist
159+
if not cls._instance:
160+
cls._instance = cls()
161+
162+
cls._instance._page = page
163+
cls._instance._initialized = True
164+
165+
# Register any routes that were decorated
166+
for route_path, model_class in _pending_registrations:
167+
model_instance = model_class(page)
168+
cls._instance._routes[route_path] = model_instance
169+
170+
# Clear pending registrations
171+
_pending_registrations = []
172+
173+
# Setup routing
174+
cls._instance._setup_routing()
175+
176+
# Return the instance for method chaining if needed
177+
return cls._instance
178+
179+
@classmethod
180+
def navigate(cls, route: str) -> None:
181+
"""
182+
Navigate to a specific route, ensuring the router is initialized first.
183+
184+
Args:
185+
route: The route to navigate to
186+
"""
187+
if cls._instance and cls._instance._page:
188+
cls._instance._page.go(route)
189+
190+
191+
# Modify flet.Page to inject our router
192+
def enhanced_page_go(self, route, *args, **kwargs):
193+
"""Enhanced go method that ensures router is initialized before navigation"""
194+
# Initialize router if there are pending routes
195+
if _pending_registrations and not Router._instance:
196+
Router.initialize(self)
197+
# Call the original go method
198+
original_go(self, route, *args, **kwargs)
199+
200+
201+
# Store original method and apply patch
202+
original_go = ft.Page.go
203+
ft.Page.go = enhanced_page_go

0 commit comments

Comments
 (0)