Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
- `native_modules` – native API modules for device capabilities (`native_modules/`)
- `native_views` – platform-specific native view creation and updates (`native_views/`)
- `package` – `src/pythonnative/__init__.py` exports and package boundary
- `page` – Page component, lifecycle, and reactive state (`page.py`)
- `reconciler` – virtual view tree diffing and reconciliation (`reconciler.py`)
- `screen` – screen host, native lifecycle bridge, and render scheduling (`screen.py`)
- `style` – StyleSheet and theming (`style.py`)
- `utils` – shared utilities (`utils.py`)

Expand Down Expand Up @@ -154,7 +154,7 @@ Breaking changes:
- Use `!` after the type/scope or a `BREAKING CHANGE:` footer.

```text
feat(core)!: rename Page.set_root_view to set_root
feat(screen)!: rename create_page to create_screen

BREAKING CHANGE: API renamed; update app code and templates.
```
Expand Down Expand Up @@ -288,7 +288,7 @@ pn run ios
maestro --platform ios test ../../tests/e2e/ios.yaml
```

Test flows live in `tests/e2e/flows/` and cover main page rendering, counter interaction, and multi-page navigation. The `e2e.yml` workflow runs these automatically on pushes to `main` and PRs.
Test flows live in `tests/e2e/flows/` and cover the main screen rendering, counter interaction, and multi-screen navigation. The `e2e.yml` workflow runs these automatically on pushes to `main` and PRs.

### CI

Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
- **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
- **Native-backed navigation:** Declarative `Stack`, `Tab`, and `Drawer` navigators inspired by React Navigation. The root stack drives the platform's native navigation controller (`UINavigationController` on iOS, AndroidX Navigation Component on Android), so transitions, back gestures, and the hardware back button match what users expect.
- **Fast Refresh hot reload:** `pn run --hot-reload` watches `app/` and patches edits into the running app on save, preserving component state across most changes.
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.

## Quick Start
Expand All @@ -55,7 +56,7 @@ import pythonnative as pn


@pn.component
def MainPage():
def App():
count, set_count = pn.use_state(0)
return pn.Column(
pn.Text(f"Count: {count}", style={"font_size": 24}),
Expand Down
2 changes: 1 addition & 1 deletion docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ slot across renders.

These hooks subscribe to values published by
`pythonnative.platform_metrics` and re-render the component when they
change. The page host is the only code that updates the underlying
change. The screen host is the only code that updates the underlying
values; user code consumes them.

- [`use_window_dimensions`][pythonnative.use_window_dimensions] — viewport size.
Expand Down
21 changes: 20 additions & 1 deletion docs/api/pythonnative.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ PythonNative re-exports a small public surface from
in this overview; deeper internals (`reconciler`, `native_views`,
`page`) are documented for contributors and integrators.

## Entry point

Your app module defines a top-level component named `App`:

```python
import pythonnative as pn

@pn.component
def App():
return pn.NavigationContainer(...)
```

The bundled Android `ScreenFragment` and iOS `ViewController` load
your app by **module path** (`"app.main"`) and look up the
module's top-level `App` attribute. There is no registration step
or imperative bootstrap call. If you need to expose a
differently-named root component, configure the templates to load
an explicit dotted path like `"app.main.RootScreen"` instead.

::: pythonnative
options:
show_root_heading: false
Expand All @@ -25,7 +44,7 @@ The reference is split per module so each page stays scannable:
| Navigation | [Navigation](navigation.md) | [`NavigationContainer`][pythonnative.NavigationContainer], [`create_stack_navigator`][pythonnative.create_stack_navigator], [`create_tab_navigator`][pythonnative.create_tab_navigator], [`create_drawer_navigator`][pythonnative.create_drawer_navigator], [`use_navigation`][pythonnative.use_navigation] |
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`ThemeContext`][pythonnative.style.ThemeContext] |
| Element descriptor | [Element](element.md) | [`Element`][pythonnative.Element] |
| Page host | [Page](page.md) | [`create_page`][pythonnative.create_page] |
| Screen host | [Screen](screen.md) | [`create_screen`][pythonnative.create_screen] |
| Reconciler | [Reconciler](reconciler.md) | [`Reconciler`][pythonnative.reconciler.Reconciler] |
| Native modules | [Native modules](native_modules.md) | `Camera`, `Location`, `FileSystem`, `Notifications` |
| Native views | [Native views](native_views.md) | [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry], [`ViewHandler`][pythonnative.native_views.base.ViewHandler] |
Expand Down
12 changes: 6 additions & 6 deletions docs/api/page.md → docs/api/screen.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Page
# Screen

The page host owns a [`Reconciler`][pythonnative.reconciler.Reconciler],
The screen host owns a [`Reconciler`][pythonnative.reconciler.Reconciler],
schedules re-renders, and forwards platform lifecycle hooks (resume,
pause, destroy) to navigators and effects. The bundled
Android (`MainActivity`) and iOS (`ViewController`) templates create a
host via [`create_page`][pythonnative.create_page] and never need to be
edited by app code.
host via [`create_screen`][pythonnative.create_screen] and never need to
be edited by app code.

::: pythonnative.page
::: pythonnative.screen
options:
show_root_heading: false
show_root_toc_entry: false
Expand All @@ -17,5 +17,5 @@ edited by app code.
## Next steps

- Understand the render queue in [Lifecycle](../concepts/lifecycle.md).
- See how navigation owns its own pages in
- See how navigation hosts each screen in
[`NavigationContainer`][pythonnative.NavigationContainer].
86 changes: 57 additions & 29 deletions docs/concepts/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ platform APIs synchronously from Python.
the JNI bridge.
9. **Thin native bootstrap.** The host app remains native (Android
`Activity` or iOS `UIViewController`). It calls
[`create_page`][pythonnative.create_page] internally to bootstrap
[`create_screen`][pythonnative.create_screen] internally to bootstrap
your Python component, and the reconciler drives the UI from
there.
10. **`App` entry point.** The user's app module (`app/main.py`)
defines a top-level component named `App`. Native templates
import that module by path (`"app.main"`) and look up its `App`
attribute, so users never write a separate registration step.
Components with other names can still be loaded by passing an
explicit dotted path like `"app.main.RootScreen"` to the
template.

## How it works

Expand Down Expand Up @@ -113,7 +120,7 @@ Each component is a Python function that:
- Has its own hook state per call site (each instance gets its own
slot table).

The entry point [`create_page`][pythonnative.create_page] is called
The entry point [`create_screen`][pythonnative.create_screen] is called
internally by the bundled native templates to bootstrap your root
component. App code does not call it directly.

Expand Down Expand Up @@ -252,7 +259,7 @@ See [Mental model](mental-model.md) for a wider comparison table.
## iOS flow (rubicon-objc)

- The iOS template (Swift plus PythonKit) boots Python and calls
[`create_page`][pythonnative.create_page] internally with the
[`create_screen`][pythonnative.create_screen] internally with the
current `UIViewController` pointer.
- The reconciler creates UIKit views and attaches them to the
controller's view.
Expand All @@ -263,17 +270,31 @@ See [Mental model](mental-model.md) for a wider comparison table.

- The Android template (Kotlin plus Chaquopy) initializes Python in
`MainActivity` and passes the `Activity` to Python.
- `PageFragment` calls [`create_page`][pythonnative.create_page]
- `ScreenFragment` calls [`create_screen`][pythonnative.create_screen]
internally, which renders the root component and attaches views to
the fragment container.
- State changes trigger re-render; the reconciler patches Android
views in place.

## Hot reload
## Hot reload (Fast Refresh)

During development, `pn run --hot-reload` watches `app/` for file
changes and pushes updated Python files to the running app, enabling
near-instant UI updates without full rebuilds. See
near-instant UI updates without full rebuilds.

PythonNative uses a **Fast Refresh** strategy:

1. Reload the changed module(s) on the device.
2. For every active screen host, walk the VNode tree and collect every
component function defined in a reloaded module.
3. Match each one to its replacement by `__module__` +
`__qualname__` and rewrite `Element.type` in place.
4. Trigger one reconcile pass. Because the VNode and its `HookState`
are reused, component state (`use_state`, `use_reducer`, refs) is
preserved across the edit.

If Fast Refresh can't produce a clean swap, the host falls back to a
**full remount** of its root component. See
[Hot reload guide](../guides/hot-reload.md).

## Native API modules
Expand All @@ -293,33 +314,40 @@ See [Native modules guide](../guides/native-modules.md).

## Navigation

PythonNative provides two navigation approaches:

- **Declarative navigators** (recommended):
[`NavigationContainer`][pythonnative.NavigationContainer] with
[`create_stack_navigator`][pythonnative.create_stack_navigator],
[`create_tab_navigator`][pythonnative.create_tab_navigator], and
[`create_drawer_navigator`][pythonnative.create_drawer_navigator].
Navigation state is managed in Python as component state, and
navigators are composable; you can nest tabs inside stacks, and so
on.
- **Page-level navigation**:
[`use_navigation`][pythonnative.use_navigation] returns a
navigation handle with `.navigate()`, `.go_back()`, and
`.get_params()`, delegating to native platform navigation when
running on device.

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`][pythonnative.Provider].

See the [Navigation guide](../guides/navigation.md) for full details.
PythonNative navigation is **declarative** and **native-backed**:

- The user describes their app as a tree of navigators
([`create_stack_navigator`][pythonnative.create_stack_navigator],
[`create_tab_navigator`][pythonnative.create_tab_navigator],
[`create_drawer_navigator`][pythonnative.create_drawer_navigator])
wrapped in
[`NavigationContainer`][pythonnative.NavigationContainer], and
names the root component `App` so the native templates can find
it.
- The outermost `Stack.Navigator` delegates `navigate(...)`,
`go_back()`, and `reset(...)` to the platform's native navigation
controller — `UINavigationController` on iOS and the AndroidX
Navigation Component on Android. Nested navigators (tabs inside a
stack, stacks inside tabs) stay in Python and reuse the existing
reconciler.
- Each pushed native screen is a fresh host with its own reconciler
and `_ScreenHost`. Initial routes are forwarded via host arguments
(`__pn_initial_route__` / `__pn_initial_params__`), so a pushed
screen knows which `Stack.Screen` to render on its first frame.
- Inside any screen, [`use_navigation`][pythonnative.use_navigation]
returns a `NavigationHandle`; [`use_route`][pythonnative.use_route]
returns the current route name and params. Both are the same
hooks regardless of whether the active navigator is native-backed
or pure-Python.

See the [Navigation guide](../guides/navigation.md) for the full
walkthrough, including how `options={"title": ...}` flows into the
native navigation bar.

- iOS: one host `UIViewController` class, many instances pushed on a
`UINavigationController`.
- Android: single host `Activity` with a `NavHostFragment` and a
stack of generic `PageFragment`s driven by a navigation graph.
stack of generic `ScreenFragment`s driven by a navigation graph.

## Next steps

Expand Down
11 changes: 6 additions & 5 deletions docs/concepts/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,16 @@ element tree:

```python
@pn.component
def MainPage():
def App():
name, set_name = pn.use_state("World")
return pn.Text(f"Hello, {name}!", style={"font_size": 24})
```

The entry point [`create_page`][pythonnative.create_page] is called
The entry point [`create_screen`][pythonnative.create_screen] is called
internally by native templates to bootstrap your root component. You
don't call it directly; just export your component and configure the
entry point in `pythonnative.json`.
don't call it directly: name your top-level component `App` (so the
templates can find it by convention) and `pythonnative.json` points
at the module that defines it.

## State and re-rendering

Expand Down Expand Up @@ -221,7 +222,7 @@ def Counter(label: str = "Count", initial: int = 0):


@pn.component
def MainPage():
def App():
return pn.Column(
Counter(label="Apples", initial=0),
Counter(label="Oranges", initial=5),
Expand Down
6 changes: 3 additions & 3 deletions docs/concepts/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ fold into it, and where you can hook in.

A render pass is triggered by:

- Initial mount via [`create_page`][pythonnative.create_page].
- Initial mount via [`create_screen`][pythonnative.create_screen].
- A setter from [`use_state`][pythonnative.use_state] or a `dispatch`
from [`use_reducer`][pythonnative.use_reducer].
- A navigation event (`navigate`, `go_back`, `replace`).
Expand All @@ -29,7 +29,7 @@ The phases:
first; new [`use_effect`][pythonnative.use_effect] callbacks run
after, in depth-first order so children commit before parents.
4. **Drain**. If any effect set state, another render pass is queued
immediately. The page host caps the loop to prevent runaway
immediately. The screen host caps the loop to prevent runaway
re-renders.

```text
Expand Down Expand Up @@ -86,7 +86,7 @@ When the user navigates away:

## App lifecycle (Android / iOS)

The page host forwards the platform's app-level lifecycle to navigators
The screen host forwards the platform's app-level lifecycle to navigators
and effects:

- **Resume / `viewWillAppear`**: the active screen's `use_focus_effect`
Expand Down
4 changes: 2 additions & 2 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ project scaffolded with `pn init`.
```bash
pn init my-app
cd my-app
# Edit app/main_page.py and paste any of the snippets below.
# Edit app/main.py and paste any of the snippets below.
pn run android # or: pn run ios
```

The `app/main_page.py` that `pn init` writes already returns a small
The `app/main.py` that `pn init` writes already returns a small
counter; replace it with one of the snippets to try a different
example.

Expand Down
8 changes: 4 additions & 4 deletions docs/examples/hello-world.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ The smallest possible PythonNative app. You'll learn how to:

## The code

Save this as `app/main_page.py`:
Save this as `app/main.py`:

```python
import pythonnative as pn


@pn.component
def MainPage():
def App():
count, set_count = pn.use_state(0)
return pn.Column(
pn.Text(f"Count: {count}", style={"font_size": 24, "bold": True}),
Expand All @@ -27,11 +27,11 @@ def MainPage():

## What's happening

- `@pn.component` registers `MainPage` as a function component. Hooks
- `@pn.component` registers `App` as a function component. Hooks
(like `use_state`) work because the decorator establishes a hook
context for each call.
- `pn.use_state(0)` returns `(value, setter)`. The setter triggers a
re-render scheduled by the page host.
re-render scheduled by the screen host.
- `pn.Column(*children, style=...)` returns a vertical container
element. Both the children and the style are read on every render;
the reconciler diffs them against the previous render and updates
Expand Down
Loading
Loading