Skip to content

Commit 828bbb0

Browse files
committed
feat(navigation)!: add declarative navigation system
1 parent bf6bb57 commit 828bbb0

File tree

17 files changed

+1532
-53
lines changed

17 files changed

+1532
-53
lines changed

docs/api/pythonnative.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,25 @@ Function component primitives:
2929
- `pythonnative.use_state(initial)` — local component state
3030
- `pythonnative.use_reducer(reducer, initial_state)` — reducer-based state management; returns `(state, dispatch)`
3131
- `pythonnative.use_effect(effect, deps)` — side effects, run after native commit
32-
- `pythonnative.use_navigation()` — navigation handle (push/pop/get_args)
32+
- `pythonnative.use_navigation()` — navigation handle (navigate/go_back/get_params)
33+
- `pythonnative.use_route()` — convenience hook for current route params
34+
- `pythonnative.use_focus_effect(effect, deps)` — like `use_effect` but only runs when the screen is focused
3335
- `pythonnative.use_memo(factory, deps)` — memoised values
3436
- `pythonnative.use_callback(fn, deps)` — stable function references
3537
- `pythonnative.use_ref(initial)` — mutable ref object
3638
- `pythonnative.use_context(context)` — read from context
3739
- `pythonnative.create_context(default)` — create a new context
3840
- `pythonnative.Provider(context, value, child)` — provide a context value
3941

42+
### Navigation
43+
44+
Declarative, component-based navigation system:
45+
46+
- `pythonnative.NavigationContainer(child)` — root container for the navigation tree
47+
- `pythonnative.create_stack_navigator()` — create a stack-based navigator (returns object with `.Navigator` and `.Screen`)
48+
- `pythonnative.create_tab_navigator()` — create a tab-based navigator
49+
- `pythonnative.create_drawer_navigator()` — create a drawer-based navigator
50+
4051
### Batching
4152

4253
- `pythonnative.batch_updates()` — context manager that batches multiple state updates into a single re-render

docs/concepts/architecture.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,17 @@ PythonNative provides cross-platform modules for common device APIs:
135135

136136
## Navigation model overview
137137

138-
- See the Navigation guide for full details.
139-
- Navigation is handled via the `use_navigation()` hook, which returns a `NavigationHandle` with `.push()`, `.pop()`, and `.get_args()`.
140-
- iOS: one host `UIViewController` class, many instances pushed on a `UINavigationController`.
141-
- Android: single host `Activity` with a `NavHostFragment` and a stack of generic `PageFragment`s driven by a navigation graph.
138+
PythonNative provides two navigation approaches:
139+
140+
- **Declarative navigators** (recommended): `NavigationContainer` with `create_stack_navigator()`, `create_tab_navigator()`, and `create_drawer_navigator()`. Navigation state is managed in Python as component state, and navigators are composable — you can nest tabs inside stacks, etc.
141+
- **Page-level navigation**: `use_navigation()` returns a `NavigationHandle` with `.navigate()`, `.go_back()`, and `.get_params()`, delegating to native platform navigation when running on device.
142+
143+
Both approaches are supported. The declarative system uses the existing reconciler pipeline — navigators are function components that render the active screen via `use_state`, and navigation context is provided via `Provider`.
144+
145+
See the [Navigation guide](../guides/navigation.md) for full details.
146+
147+
- iOS: one host `UIViewController` class, many instances pushed on a `UINavigationController`.
148+
- Android: single host `Activity` with a `NavHostFragment` and a stack of generic `PageFragment`s driven by a navigation graph.
142149

143150
## Related docs
144151

docs/concepts/components.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ Changing one `Counter` doesn't affect the other — each has its own hook state.
174174
- `use_callback(fn, deps)` — stable function references
175175
- `use_ref(initial)` — mutable ref that persists across renders
176176
- `use_context(context)` — read from a context provider
177-
- `use_navigation()` — navigation handle for push/pop between screens
177+
- `use_navigation()` — navigation handle for navigate/go_back/get_params
178+
- `use_route()` — convenience hook for current route params
179+
- `use_focus_effect(effect, deps)` — like `use_effect` but only runs when the screen is focused
178180

179181
### Custom hooks
180182

docs/concepts/hooks.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ Dependency control:
119119

120120
### use_navigation
121121

122-
Access the navigation stack from any component. Returns a `NavigationHandle` with `.push()`, `.pop()`, and `.get_args()`.
122+
Access navigation from any component. Returns a `NavigationHandle` with `.navigate()`, `.go_back()`, and `.get_params()`.
123123

124124
```python
125125
@pn.component
@@ -130,25 +130,49 @@ def HomeScreen():
130130
pn.Text("Home", style={"font_size": 24}),
131131
pn.Button(
132132
"Go to Details",
133-
on_click=lambda: nav.push(DetailScreen, args={"id": 42}),
133+
on_click=lambda: nav.navigate("Detail", params={"id": 42}),
134134
),
135135
style={"spacing": 12, "padding": 16},
136136
)
137137

138138
@pn.component
139139
def DetailScreen():
140140
nav = pn.use_navigation()
141-
item_id = nav.get_args().get("id", 0)
141+
item_id = nav.get_params().get("id", 0)
142142

143143
return pn.Column(
144144
pn.Text(f"Detail #{item_id}", style={"font_size": 20}),
145-
pn.Button("Back", on_click=nav.pop),
145+
pn.Button("Back", on_click=nav.go_back),
146146
style={"spacing": 12, "padding": 16},
147147
)
148148
```
149149

150150
See the [Navigation guide](../guides/navigation.md) for full details.
151151

152+
### use_route
153+
154+
Convenience hook to read the current route's parameters:
155+
156+
```python
157+
@pn.component
158+
def DetailScreen():
159+
params = pn.use_route()
160+
item_id = params.get("id", 0)
161+
return pn.Text(f"Detail #{item_id}")
162+
```
163+
164+
### use_focus_effect
165+
166+
Like `use_effect` but only runs when the screen is focused. Useful for refreshing data when navigating back to a screen:
167+
168+
```python
169+
@pn.component
170+
def FeedScreen():
171+
items, set_items = pn.use_state([])
172+
pn.use_focus_effect(lambda: load_items(set_items), [])
173+
return pn.FlatList(data=items, render_item=lambda item, i: pn.Text(item))
174+
```
175+
152176
### use_memo
153177

154178
Memoise an expensive computation:

docs/guides/navigation.md

Lines changed: 136 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,170 @@
11
# Navigation
22

3-
This guide shows how to navigate between screens and pass data using the `use_navigation()` hook.
3+
PythonNative offers two approaches to navigation:
44

5-
## Push / Pop
5+
1. **Declarative navigators** (recommended) — component-based, inspired by React Navigation
6+
2. **Legacy push/pop** — imperative navigation via `use_navigation()`
67

7-
Call `pn.use_navigation()` inside a `@pn.component` to get a `NavigationHandle`. Use `.push()` and `.pop()` to change screens, passing a component reference with optional `args`.
8+
## Declarative Navigation
9+
10+
Declarative navigators manage screen state as components. Define your screens once, and the navigator handles rendering, transitions, and state.
11+
12+
### Stack Navigator
13+
14+
A stack navigator manages a stack of screens — push to go forward, pop to go back.
815

916
```python
1017
import pythonnative as pn
11-
from app.second_page import SecondPage
18+
from pythonnative.navigation import NavigationContainer, create_stack_navigator
19+
20+
Stack = create_stack_navigator()
1221

22+
@pn.component
23+
def App():
24+
return NavigationContainer(
25+
Stack.Navigator(
26+
Stack.Screen("Home", component=HomeScreen),
27+
Stack.Screen("Detail", component=DetailScreen),
28+
initial_route="Home",
29+
)
30+
)
1331

1432
@pn.component
1533
def HomeScreen():
1634
nav = pn.use_navigation()
1735
return pn.Column(
1836
pn.Text("Home", style={"font_size": 24}),
1937
pn.Button(
20-
"Go next",
21-
on_click=lambda: nav.push(
22-
SecondPage,
23-
args={"message": "Hello from Home"},
24-
),
38+
"Go to Detail",
39+
on_click=lambda: nav.navigate("Detail", params={"id": 42}),
2540
),
2641
style={"spacing": 12, "padding": 16},
2742
)
43+
44+
@pn.component
45+
def DetailScreen():
46+
nav = pn.use_navigation()
47+
params = nav.get_params()
48+
return pn.Column(
49+
pn.Text(f"Detail #{params.get('id')}", style={"font_size": 20}),
50+
pn.Button("Back", on_click=nav.go_back),
51+
style={"spacing": 12, "padding": 16},
52+
)
53+
```
54+
55+
### Tab Navigator
56+
57+
A tab navigator renders a tab bar and switches between screens.
58+
59+
```python
60+
from pythonnative.navigation import create_tab_navigator
61+
62+
Tab = create_tab_navigator()
63+
64+
@pn.component
65+
def App():
66+
return NavigationContainer(
67+
Tab.Navigator(
68+
Tab.Screen("Home", component=HomeScreen, options={"title": "Home"}),
69+
Tab.Screen("Settings", component=SettingsScreen, options={"title": "Settings"}),
70+
)
71+
)
2872
```
2973

30-
On the target screen, retrieve args with `nav.get_args()`:
74+
### Drawer Navigator
75+
76+
A drawer navigator provides a side menu for switching screens.
3177

3278
```python
79+
from pythonnative.navigation import create_drawer_navigator
80+
81+
Drawer = create_drawer_navigator()
82+
83+
@pn.component
84+
def App():
85+
return NavigationContainer(
86+
Drawer.Navigator(
87+
Drawer.Screen("Home", component=HomeScreen, options={"title": "Home"}),
88+
Drawer.Screen("Profile", component=ProfileScreen, options={"title": "Profile"}),
89+
)
90+
)
91+
3392
@pn.component
34-
def SecondPage():
93+
def HomeScreen():
3594
nav = pn.use_navigation()
36-
message = nav.get_args().get("message", "Second Page")
3795
return pn.Column(
38-
pn.Text(message, style={"font_size": 20}),
39-
pn.Button("Back", on_click=nav.pop),
40-
style={"spacing": 12, "padding": 16},
96+
pn.Button("Open Menu", on_click=nav.open_drawer),
97+
pn.Text("Home Screen"),
98+
)
99+
```
100+
101+
### Nesting Navigators
102+
103+
Navigators can be nested — for example, tabs containing stacks:
104+
105+
```python
106+
Stack = create_stack_navigator()
107+
Tab = create_tab_navigator()
108+
109+
@pn.component
110+
def HomeStack():
111+
return Stack.Navigator(
112+
Stack.Screen("Feed", component=FeedScreen),
113+
Stack.Screen("Post", component=PostScreen),
114+
)
115+
116+
@pn.component
117+
def App():
118+
return NavigationContainer(
119+
Tab.Navigator(
120+
Tab.Screen("Home", component=HomeStack, options={"title": "Home"}),
121+
Tab.Screen("Settings", component=SettingsScreen, options={"title": "Settings"}),
122+
)
41123
)
42124
```
43125

44126
## NavigationHandle API
45127

46-
`pn.use_navigation()` returns a `NavigationHandle` with:
128+
Inside any screen rendered by a navigator, `pn.use_navigation()` returns a handle with:
129+
130+
- **`.navigate(route_name, params=...)`** — navigate to a named route with optional params
131+
- **`.go_back()`** — pop the current screen
132+
- **`.get_params()`** — get the current route's params dict
133+
- **`.reset(route_name, params=...)`** — reset the stack to a single route
134+
135+
### Drawer-specific methods
136+
137+
When inside a drawer navigator, the handle also provides:
138+
139+
- **`.open_drawer()`** — open the drawer
140+
- **`.close_drawer()`** — close the drawer
141+
- **`.toggle_drawer()`** — toggle the drawer open/closed
142+
143+
## Focus-aware Effects
47144

48-
- **`.push(component, args=...)`** — navigate to a new screen. Pass a component reference (the `@pn.component` function itself), with an optional `args` dict.
49-
- **`.pop()`** — go back to the previous screen.
50-
- **`.get_args()`** — retrieve the args dict passed by the caller.
145+
Use `pn.use_focus_effect()` to run effects only when a screen is focused:
146+
147+
```python
148+
@pn.component
149+
def DataScreen():
150+
data, set_data = pn.use_state(None)
151+
152+
pn.use_focus_effect(lambda: fetch_data(set_data), [])
153+
154+
return pn.Text(f"Data: {data}")
155+
```
156+
157+
## Route Parameters
158+
159+
Use `pn.use_route()` for convenient access to route params:
160+
161+
```python
162+
@pn.component
163+
def DetailScreen():
164+
params = pn.use_route()
165+
item_id = params.get("id", 0)
166+
return pn.Text(f"Item #{item_id}")
167+
```
51168

52169
## Lifecycle
53170

@@ -63,11 +180,6 @@ PythonNative forwards lifecycle events from the host:
63180
- `on_save_instance_state`
64181
- `on_restore_instance_state`
65182

66-
## Notes
67-
68-
- On Android, `push` navigates via `NavController` to a `PageFragment` and passes `page_path` and optional JSON `args`.
69-
- On iOS, `push` uses the root `UINavigationController` to push a new `ViewController` and passes page info via KVC.
70-
71183
## Platform specifics
72184

73185
### iOS (UIViewController per page)

examples/hello-world/app/main_page.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def MainPage() -> pn.Element:
4747
counter_badge(),
4848
pn.Button(
4949
"Go to Second Page",
50-
on_click=lambda: nav.push(
50+
on_click=lambda: nav.navigate(
5151
"app.second_page.SecondPage",
52-
args={"message": "Greetings from MainPage"},
52+
params={"message": "Greetings from MainPage"},
5353
),
5454
),
5555
style=styles["section"],

examples/hello-world/app/second_page.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
@pn.component
55
def SecondPage() -> pn.Element:
66
nav = pn.use_navigation()
7-
message = nav.get_args().get("message", "Second Page")
7+
message = nav.get_params().get("message", "Second Page")
88
return pn.ScrollView(
99
pn.Column(
1010
pn.Text(message, style={"font_size": 24, "bold": True}),
1111
pn.Button(
1212
"Go to Third Page",
13-
on_click=lambda: nav.push("app.third_page.ThirdPage"),
13+
on_click=lambda: nav.navigate("app.third_page.ThirdPage"),
1414
),
15-
pn.Button("Back", on_click=nav.pop),
15+
pn.Button("Back", on_click=nav.go_back),
1616
style={"spacing": 16, "padding": 24, "align_items": "stretch"},
1717
)
1818
)

examples/hello-world/app/third_page.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def ThirdPage() -> pn.Element:
88
pn.Column(
99
pn.Text("Third Page", style={"font_size": 24, "bold": True}),
1010
pn.Text("You navigated two levels deep."),
11-
pn.Button("Back to Second", on_click=nav.pop),
11+
pn.Button("Back to Second", on_click=nav.go_back),
1212
style={"spacing": 16, "padding": 24, "align_items": "stretch"},
1313
)
1414
)

src/pythonnative/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ def App():
5252
use_ref,
5353
use_state,
5454
)
55+
from .navigation import (
56+
NavigationContainer,
57+
create_drawer_navigator,
58+
create_stack_navigator,
59+
create_tab_navigator,
60+
use_focus_effect,
61+
use_route,
62+
)
5563
from .page import create_page
5664
from .style import StyleSheet, ThemeContext
5765

@@ -86,12 +94,19 @@ def App():
8694
"use_callback",
8795
"use_context",
8896
"use_effect",
97+
"use_focus_effect",
8998
"use_memo",
9099
"use_navigation",
91100
"use_reducer",
92101
"use_ref",
102+
"use_route",
93103
"use_state",
94104
"Provider",
105+
# Navigation
106+
"NavigationContainer",
107+
"create_drawer_navigator",
108+
"create_stack_navigator",
109+
"create_tab_navigator",
95110
# Styling
96111
"StyleSheet",
97112
"ThemeContext",

0 commit comments

Comments
 (0)