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
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
- `platform_metrics` – platform-reported metrics like safe-area insets and bar heights (`platform_metrics.py`)
- `reconciler` – virtual view tree diffing and reconciliation (`reconciler.py`)
- `screen` – screen host, native lifecycle bridge, and render scheduling (`screen.py`)
- `sdk` – public extension SDK for custom native components (`sdk/`)
- `style` – StyleSheet and theming (`style.py`)
- `utils` – shared utilities (`utils.py`)

Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app

- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
- **Typed `style` prop:** Pass all visual and layout properties through a single `style` dict, fully described by the `pn.Style` `TypedDict` and the ergonomic `pn.style(...)` helper for IDE autocomplete and static checking. Compose reusable styles with `StyleSheet`.
- **Cross-platform flexbox engine:** A pure-Python, Yoga-style layout engine computes frames once and applies them to native views, so `flex`, `padding`, `aspect_ratio`, and `position: "absolute"` produce the same geometry on Android and iOS.
- **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.
- **Custom-component SDK:** Wrap any platform widget as a first-class element with type-checked props via `pythonnative.sdk` (`Props`, `@native_component`, `element_factory`). Plugins distributed on PyPI auto-register through the `pythonnative.handlers` entry-point group.
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
- **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.
Expand All @@ -59,12 +60,12 @@ import pythonnative as pn
def App():
count, set_count = pn.use_state(0)
return pn.Column(
pn.Text(f"Count: {count}", style={"font_size": 24}),
pn.Text(f"Count: {count}", style=pn.style(font_size=24, bold=True)),
pn.Button(
"Tap me",
on_click=lambda: set_count(count + 1),
),
style={"spacing": 12, "padding": 16},
style=pn.style(spacing=12, padding=16),
)
```

Expand Down
3 changes: 2 additions & 1 deletion docs/api/pythonnative.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ The reference is split per module so each page stays scannable:
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
| 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] |
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`style`][pythonnative.style.style], [`ThemeContext`][pythonnative.style.ThemeContext] |
| Element descriptor | [Element](element.md) | [`Element`][pythonnative.Element] |
| 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] |
| Hot reload | [Hot reload](hot_reload.md) | [`FileWatcher`][pythonnative.hot_reload.FileWatcher], [`ModuleReloader`][pythonnative.hot_reload.ModuleReloader] |
| Custom components SDK | [SDK](sdk.md) | [`Props`][pythonnative.sdk._components.Props], [`ViewHandler`][pythonnative.native_views.base.ViewHandler], [`native_component`][pythonnative.sdk._components.native_component], [`register_component`][pythonnative.sdk._components.register_component], [`element_factory`][pythonnative.sdk._components.element_factory] |
| Utilities | [Utilities](utils.md) | `IS_ANDROID`, `IS_IOS`, [`get_android_context`][pythonnative.utils.get_android_context] |
| CLI | [CLI (`pn`)](cli.md) | `pn init`, `pn run`, `pn clean` |

Expand Down
74 changes: 74 additions & 0 deletions docs/api/sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SDK

`pythonnative.sdk` is the public extension API for adding new native
widgets to PythonNative. It re-exports the
[`Element`][pythonnative.element.Element] descriptor, the
[`ViewHandler`][pythonnative.native_views.base.ViewHandler] protocol,
and the typed style primitives so plugin authors only need a single
import path. The reference here documents the symbols that are
*unique* to the SDK module — the re-exports are documented on their
canonical pages and linked below.

The full walkthrough lives in
[Custom native components](../guides/custom-native-components.md);
this page is the symbol-level reference.

## Re-exports

The following names are re-exported from `pythonnative.sdk` for
convenience and are documented on their canonical pages:

| Symbol | Defined in |
|---|---|
| [`Element`][pythonnative.element.Element] | [Element](element.md) |
| [`ViewHandler`][pythonnative.native_views.base.ViewHandler] | [Native views](native_views.md) |
| [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`Color`][pythonnative.style.Color], [`Dimension`][pythonnative.style.Dimension], [`EdgeInsets`][pythonnative.style.EdgeInsets], [`EdgeValue`][pythonnative.style.EdgeValue], `FlexDirection`, `JustifyContent`, `Overflow`, `Position`, [`TransformSpec`][pythonnative.style.TransformSpec], [`style`][pythonnative.style.style] | [Style](style.md) |
| `parse_color_int`, `resolve_padding` | `pythonnative.native_views.base` |

## Custom-component primitives

::: pythonnative.sdk
options:
show_root_heading: false
show_root_toc_entry: false
members_order: source
members:
- ENTRY_POINT_GROUP
- Props
- native_component
- register_component
- unregister_component
- element_factory
- install_into_registry
- list_components
- get_props_type

## Entry-point discovery

Third-party packages can register handlers automatically by exposing
an entry point in the `pythonnative.handlers` group (the value of
[`ENTRY_POINT_GROUP`][pythonnative.sdk.ENTRY_POINT_GROUP]). The first
call to [`get_registry()`][pythonnative.native_views.get_registry]
loads every registered entry point exactly once. A misbehaving plugin
raises an exception that is caught and logged; it never breaks
PythonNative startup.

```toml
# In your plugin's pyproject.toml
[project.entry-points."pythonnative.handlers"]
my_widget = "my_pkg:register"
```

The function pointed at by the entry point should perform whatever
imports are needed to call
[`@native_component`][pythonnative.sdk.native_component] or
[`register_component`][pythonnative.sdk.register_component].

## Next steps

- [Custom native components guide](../guides/custom-native-components.md)
walks through a complete `Badge` widget across iOS and Android.
- [Native views (concept)](../concepts/native-views.md) describes the
reconciler boundary the SDK plugs into.
- [Native views API](native_views.md) documents the runtime registry
the SDK installs handlers onto.
56 changes: 39 additions & 17 deletions docs/concepts/native-views.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,32 +168,54 @@ introspect.

## Custom widgets

Adding a widget is a three-step process:

1. Implement a handler subclass for each platform you support.
2. Register it under a unique type string.
3. Add a small Python factory that returns
`Element(<type>, props, children)`.
Adding a widget is a three-step process, and the
[`pythonnative.sdk`](../api/sdk.md) module gives you a ready-made,
type-checked entry point for each step:

1. Define a frozen [`Props`][pythonnative.sdk._components.Props]
dataclass listing the widget's API surface.
2. Implement a [`ViewHandler`][pythonnative.native_views.base.ViewHandler]
subclass per platform and decorate it with
[`@native_component`][pythonnative.sdk._components.native_component].
3. Hand callers an
[`element_factory`][pythonnative.sdk._components.element_factory]
that validates kwargs against the dataclass and returns regular
`Element` instances.

```python
from dataclasses import dataclass
from typing import Optional
import pythonnative as pn
from pythonnative.element import Element
from pythonnative.native_views import get_registry
from pythonnative.sdk import Props, ViewHandler, element_factory, native_component

class _RatingHandler:
def create_view(self, props):
# platform-specific stars widget
...
def update_view(self, view, prev, next):

@dataclass(frozen=True)
class RatingProps(Props):
value: float = 0.0
on_change: Optional[callable] = None
style: Optional[pn.StyleProp] = None


@native_component("Rating", props=RatingProps, platforms=("ios",))
class IOSRatingHandler(ViewHandler):
def create(self, props):
... # build a UIView wrapping star UIImageViews
def update(self, view, changed):
...

get_registry().register("Rating", _RatingHandler())

def Rating(value: float, *, on_change=None, **kwargs):
return Element("Rating", {"value": value, "on_change": on_change, **kwargs}, [])
Rating = element_factory("Rating")
```

The reconciler treats `Rating` like any other element after that.
After registration the reconciler treats `Rating` like any other
element. PyPI plugins can register their handlers automatically via
the `pythonnative.handlers` entry-point group (see
[`ENTRY_POINT_GROUP`][pythonnative.sdk._components.ENTRY_POINT_GROUP]),
so users only have to `pip install` your package.

For the full walkthrough — typed props, iOS handler, Android handler,
distribution as a plugin, unit-testing — see the
[Custom native components guide](../guides/custom-native-components.md).

## Next steps

Expand Down
Loading
Loading