Skip to content

Commit 9f26a6e

Browse files
authored
feat(sdk,style)!: add native-component SDK and typed Style API (#2)
1 parent 6bcb874 commit 9f26a6e

20 files changed

Lines changed: 2261 additions & 155 deletions

File tree

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
112112
- `platform_metrics` – platform-reported metrics like safe-area insets and bar heights (`platform_metrics.py`)
113113
- `reconciler` – virtual view tree diffing and reconciliation (`reconciler.py`)
114114
- `screen` – screen host, native lifecycle bridge, and render scheduling (`screen.py`)
115+
- `sdk` – public extension SDK for custom native components (`sdk/`)
115116
- `style` – StyleSheet and theming (`style.py`)
116117
- `utils` – shared utilities (`utils.py`)
117118

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
3232

3333
- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
3434
- **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
35-
- **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
35+
- **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`.
3636
- **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.
3737
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
3838
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
39+
- **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.
3940
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
4041
- **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.
4142
- **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.
@@ -59,12 +60,12 @@ import pythonnative as pn
5960
def App():
6061
count, set_count = pn.use_state(0)
6162
return pn.Column(
62-
pn.Text(f"Count: {count}", style={"font_size": 24}),
63+
pn.Text(f"Count: {count}", style=pn.style(font_size=24, bold=True)),
6364
pn.Button(
6465
"Tap me",
6566
on_click=lambda: set_count(count + 1),
6667
),
67-
style={"spacing": 12, "padding": 16},
68+
style=pn.style(spacing=12, padding=16),
6869
)
6970
```
7071

docs/api/pythonnative.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,14 @@ The reference is split per module so each page stays scannable:
4242
| System dialogs | [Alerts](alerts.md) | [`Alert`][pythonnative.Alert] |
4343
| Platform | [Platform](platform.md) | [`Platform`][pythonnative.Platform] |
4444
| 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] |
45-
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`ThemeContext`][pythonnative.style.ThemeContext] |
45+
| Styling | [Style](style.md) | [`StyleSheet`][pythonnative.StyleSheet], [`Style`][pythonnative.style.Style], [`StyleProp`][pythonnative.style.StyleProp], [`style`][pythonnative.style.style], [`ThemeContext`][pythonnative.style.ThemeContext] |
4646
| Element descriptor | [Element](element.md) | [`Element`][pythonnative.Element] |
4747
| Screen host | [Screen](screen.md) | [`create_screen`][pythonnative.create_screen] |
4848
| Reconciler | [Reconciler](reconciler.md) | [`Reconciler`][pythonnative.reconciler.Reconciler] |
4949
| Native modules | [Native modules](native_modules.md) | `Camera`, `Location`, `FileSystem`, `Notifications` |
5050
| Native views | [Native views](native_views.md) | [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry], [`ViewHandler`][pythonnative.native_views.base.ViewHandler] |
5151
| Hot reload | [Hot reload](hot_reload.md) | [`FileWatcher`][pythonnative.hot_reload.FileWatcher], [`ModuleReloader`][pythonnative.hot_reload.ModuleReloader] |
52+
| 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] |
5253
| Utilities | [Utilities](utils.md) | `IS_ANDROID`, `IS_IOS`, [`get_android_context`][pythonnative.utils.get_android_context] |
5354
| CLI | [CLI (`pn`)](cli.md) | `pn init`, `pn run`, `pn clean` |
5455

docs/api/sdk.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SDK
2+
3+
`pythonnative.sdk` is the public extension API for adding new native
4+
widgets to PythonNative. It re-exports the
5+
[`Element`][pythonnative.element.Element] descriptor, the
6+
[`ViewHandler`][pythonnative.native_views.base.ViewHandler] protocol,
7+
and the typed style primitives so plugin authors only need a single
8+
import path. The reference here documents the symbols that are
9+
*unique* to the SDK module — the re-exports are documented on their
10+
canonical pages and linked below.
11+
12+
The full walkthrough lives in
13+
[Custom native components](../guides/custom-native-components.md);
14+
this page is the symbol-level reference.
15+
16+
## Re-exports
17+
18+
The following names are re-exported from `pythonnative.sdk` for
19+
convenience and are documented on their canonical pages:
20+
21+
| Symbol | Defined in |
22+
|---|---|
23+
| [`Element`][pythonnative.element.Element] | [Element](element.md) |
24+
| [`ViewHandler`][pythonnative.native_views.base.ViewHandler] | [Native views](native_views.md) |
25+
| [`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) |
26+
| `parse_color_int`, `resolve_padding` | `pythonnative.native_views.base` |
27+
28+
## Custom-component primitives
29+
30+
::: pythonnative.sdk
31+
options:
32+
show_root_heading: false
33+
show_root_toc_entry: false
34+
members_order: source
35+
members:
36+
- ENTRY_POINT_GROUP
37+
- Props
38+
- native_component
39+
- register_component
40+
- unregister_component
41+
- element_factory
42+
- install_into_registry
43+
- list_components
44+
- get_props_type
45+
46+
## Entry-point discovery
47+
48+
Third-party packages can register handlers automatically by exposing
49+
an entry point in the `pythonnative.handlers` group (the value of
50+
[`ENTRY_POINT_GROUP`][pythonnative.sdk.ENTRY_POINT_GROUP]). The first
51+
call to [`get_registry()`][pythonnative.native_views.get_registry]
52+
loads every registered entry point exactly once. A misbehaving plugin
53+
raises an exception that is caught and logged; it never breaks
54+
PythonNative startup.
55+
56+
```toml
57+
# In your plugin's pyproject.toml
58+
[project.entry-points."pythonnative.handlers"]
59+
my_widget = "my_pkg:register"
60+
```
61+
62+
The function pointed at by the entry point should perform whatever
63+
imports are needed to call
64+
[`@native_component`][pythonnative.sdk.native_component] or
65+
[`register_component`][pythonnative.sdk.register_component].
66+
67+
## Next steps
68+
69+
- [Custom native components guide](../guides/custom-native-components.md)
70+
walks through a complete `Badge` widget across iOS and Android.
71+
- [Native views (concept)](../concepts/native-views.md) describes the
72+
reconciler boundary the SDK plugs into.
73+
- [Native views API](native_views.md) documents the runtime registry
74+
the SDK installs handlers onto.

docs/concepts/native-views.md

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -168,32 +168,54 @@ introspect.
168168

169169
## Custom widgets
170170

171-
Adding a widget is a three-step process:
172-
173-
1. Implement a handler subclass for each platform you support.
174-
2. Register it under a unique type string.
175-
3. Add a small Python factory that returns
176-
`Element(<type>, props, children)`.
171+
Adding a widget is a three-step process, and the
172+
[`pythonnative.sdk`](../api/sdk.md) module gives you a ready-made,
173+
type-checked entry point for each step:
174+
175+
1. Define a frozen [`Props`][pythonnative.sdk._components.Props]
176+
dataclass listing the widget's API surface.
177+
2. Implement a [`ViewHandler`][pythonnative.native_views.base.ViewHandler]
178+
subclass per platform and decorate it with
179+
[`@native_component`][pythonnative.sdk._components.native_component].
180+
3. Hand callers an
181+
[`element_factory`][pythonnative.sdk._components.element_factory]
182+
that validates kwargs against the dataclass and returns regular
183+
`Element` instances.
177184

178185
```python
186+
from dataclasses import dataclass
187+
from typing import Optional
179188
import pythonnative as pn
180-
from pythonnative.element import Element
181-
from pythonnative.native_views import get_registry
189+
from pythonnative.sdk import Props, ViewHandler, element_factory, native_component
182190

183-
class _RatingHandler:
184-
def create_view(self, props):
185-
# platform-specific stars widget
186-
...
187-
def update_view(self, view, prev, next):
191+
192+
@dataclass(frozen=True)
193+
class RatingProps(Props):
194+
value: float = 0.0
195+
on_change: Optional[callable] = None
196+
style: Optional[pn.StyleProp] = None
197+
198+
199+
@native_component("Rating", props=RatingProps, platforms=("ios",))
200+
class IOSRatingHandler(ViewHandler):
201+
def create(self, props):
202+
... # build a UIView wrapping star UIImageViews
203+
def update(self, view, changed):
188204
...
189205

190-
get_registry().register("Rating", _RatingHandler())
191206

192-
def Rating(value: float, *, on_change=None, **kwargs):
193-
return Element("Rating", {"value": value, "on_change": on_change, **kwargs}, [])
207+
Rating = element_factory("Rating")
194208
```
195209

196-
The reconciler treats `Rating` like any other element after that.
210+
After registration the reconciler treats `Rating` like any other
211+
element. PyPI plugins can register their handlers automatically via
212+
the `pythonnative.handlers` entry-point group (see
213+
[`ENTRY_POINT_GROUP`][pythonnative.sdk._components.ENTRY_POINT_GROUP]),
214+
so users only have to `pip install` your package.
215+
216+
For the full walkthrough — typed props, iOS handler, Android handler,
217+
distribution as a plugin, unit-testing — see the
218+
[Custom native components guide](../guides/custom-native-components.md).
197219

198220
## Next steps
199221

0 commit comments

Comments
 (0)