Skip to content

Commit b6b7721

Browse files
committed
feat(core)!: replace imperative widget API with declarative component model and reconciler
1 parent e00f10e commit b6b7721

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2697
-2970
lines changed

README.md

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,17 @@
2626

2727
## Overview
2828

29-
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a Pythonic API for native UI components, lifecycle events, and device capabilities, powered by Chaquopy on Android and rubicon-objc on iOS. Write your app once in Python and run it on both platforms with genuinely native interfaces.
29+
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Describe your UI as a tree of elements, manage state with `set_state()`, and let PythonNative handle creating and updating native views.
3030

3131
## Features
3232

33-
- **Cross-platform native UI:** Build Android and iOS apps from a single Python codebase with truly native rendering.
33+
- **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
34+
- **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed — no manual view mutation.
35+
- **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
3436
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
35-
- **Unified component API:** Components like `Page`, `StackView`, `Label`, `Button`, and `WebView` share a consistent interface across platforms.
36-
- **CLI scaffolding:** `pn init` creates a ready-to-run project structure; `pn run android` and `pn run ios` build and launch your app.
37-
- **Page lifecycle:** Hooks for `on_create`, `on_start`, `on_resume`, `on_pause`, `on_stop`, and `on_destroy`, with state save and restore.
37+
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
3838
- **Navigation:** Push and pop screens with argument passing for multi-page apps.
39-
- **Rich component set:** Core views (Label, Button, TextField, ImageView, WebView, Switch, DatePicker, and more) plus Material Design variants.
40-
- **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
39+
- **Bundled templates:** Android Gradle and iOS Xcode templates are included — scaffolding requires no network access.
4140

4241
## Quick Start
4342

@@ -56,17 +55,36 @@ import pythonnative as pn
5655
class MainPage(pn.Page):
5756
def __init__(self, native_instance):
5857
super().__init__(native_instance)
59-
60-
def on_create(self):
61-
super().on_create()
62-
stack = pn.StackView()
63-
stack.add_view(pn.Label("Hello from PythonNative!"))
64-
button = pn.Button("Tap me")
65-
button.set_on_click(lambda: print("Button tapped"))
66-
stack.add_view(button)
67-
self.set_root_view(stack)
58+
self.state = {"count": 0}
59+
60+
def render(self):
61+
return pn.Column(
62+
pn.Text(f"Count: {self.state['count']}", font_size=24),
63+
pn.Button(
64+
"Tap me",
65+
on_click=lambda: self.set_state(count=self.state["count"] + 1),
66+
),
67+
spacing=12,
68+
padding=16,
69+
)
6870
```
6971

72+
### Available Components
73+
74+
| Component | Description |
75+
|---|---|
76+
| `Text` | Display text |
77+
| `Button` | Tappable button with `on_click` callback |
78+
| `Column` / `Row` | Vertical / horizontal layout containers |
79+
| `ScrollView` | Scrollable wrapper |
80+
| `TextInput` | Text entry field with `on_change` callback |
81+
| `Image` | Display images |
82+
| `Switch` | Toggle with `on_change` callback |
83+
| `ProgressBar` | Determinate progress (0.0–1.0) |
84+
| `ActivityIndicator` | Indeterminate loading spinner |
85+
| `WebView` | Embedded web content |
86+
| `Spacer` | Empty space |
87+
7088
## Documentation
7189

7290
Visit [docs.pythonnative.com](https://docs.pythonnative.com/) for the full documentation, including getting started guides, platform-specific instructions for Android and iOS, API reference, and working examples.

docs/api/component-properties.md

Lines changed: 82 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,100 @@
1-
## Component Property Reference (v0.4.0)
1+
# Component Property Reference
22

3-
This page summarizes common properties and fluent setters added in v0.4.0. Unless noted, methods return `self` for chaining.
3+
All style and behaviour properties are passed as keyword arguments to element functions.
44

5-
### View (base)
5+
## Text
66

7-
- `set_background_color(color)`
8-
- Accepts ARGB int or `#RRGGBB` / `#AARRGGBB` string.
7+
```python
8+
pn.Text(text, font_size=None, color=None, bold=False, text_align=None,
9+
background_color=None, max_lines=None)
10+
```
911

10-
- `set_padding(*, all=None, horizontal=None, vertical=None, left=None, top=None, right=None, bottom=None)`
11-
- Android: applies padding in dp.
12-
- iOS: currently a no-op for most views.
12+
- `text` — display string
13+
- `font_size` — size in sp (Android) / pt (iOS)
14+
- `color` — text colour (`#RRGGBB` or `#AARRGGBB`)
15+
- `bold` — bold weight
16+
- `text_align``"left"`, `"center"`, or `"right"`
17+
- `background_color` — view background
18+
- `max_lines` — limit visible lines
1319

14-
- `set_margin(*, all=None, horizontal=None, vertical=None, left=None, top=None, right=None, bottom=None)`
15-
- Android: applied when added to `StackView` (LinearLayout) as `LayoutParams` margins (dp).
16-
- iOS: not currently applied.
20+
## Button
1721

18-
- `wrap_in_scroll()``ScrollView`
19-
- Returns a `ScrollView` containing this view.
22+
```python
23+
pn.Button(title, on_click=None, color=None, background_color=None,
24+
font_size=None, enabled=True)
25+
```
2026

21-
### ScrollView
27+
- `title` — button label
28+
- `on_click` — callback `() -> None`
29+
- `color` — title text colour
30+
- `background_color` — button background
31+
- `enabled` — interactive state
2232

23-
- `ScrollView.wrap(view)``ScrollView`
24-
- Class helper to wrap a single child.
33+
## Column / Row
2534

26-
### StackView
35+
```python
36+
pn.Column(*children, spacing=0, padding=None, alignment=None, background_color=None)
37+
pn.Row(*children, spacing=0, padding=None, alignment=None, background_color=None)
38+
```
2739

28-
- `set_axis('vertical'|'horizontal')`
29-
- `set_spacing(n)`
30-
- Android: dp via divider drawable.
31-
- iOS: `UIStackView.spacing` (points).
32-
- `set_alignment('fill'|'center'|'leading'|'trailing'|'top'|'bottom')`
33-
- Cross-axis alignment; top/bottom map appropriately for horizontal stacks.
40+
- `spacing` — gap between children (dp / pt)
41+
- `padding` — inner padding (int for all sides, or dict with `horizontal`, `vertical`, `left`, `top`, `right`, `bottom`)
42+
- `alignment` — cross-axis: `"fill"`, `"center"`, `"leading"`, `"trailing"`, `"start"`, `"end"`, `"top"`, `"bottom"`
43+
- `background_color` — container background
3444

35-
### Text components
45+
## ScrollView
3646

37-
Applies to `Label`, `TextField`, `TextView`:
47+
```python
48+
pn.ScrollView(child, background_color=None)
49+
```
3850

39-
- `set_text(text)`
40-
- `set_text_color(color)`
41-
- `set_text_size(size)`
51+
## TextInput
4252

43-
Platform notes:
44-
- Android: `setTextColor(int)`, `setTextSize(sp)`.
45-
- iOS: `setTextColor(UIColor)`, `UIFont.systemFont(ofSize:)`.
53+
```python
54+
pn.TextInput(value="", placeholder="", on_change=None, secure=False,
55+
font_size=None, color=None, background_color=None)
56+
```
4657

47-
### Button
58+
- `on_change` — callback `(str) -> None` receiving new text
4859

49-
- `set_title(text)`
50-
- `set_on_click(callback)`
60+
## Image
5161

62+
```python
63+
pn.Image(source="", width=None, height=None, scale_type=None, background_color=None)
64+
```
5265

66+
## Switch
67+
68+
```python
69+
pn.Switch(value=False, on_change=None)
70+
```
71+
72+
- `on_change` — callback `(bool) -> None`
73+
74+
## ProgressBar
75+
76+
```python
77+
pn.ProgressBar(value=0.0, background_color=None)
78+
```
79+
80+
- `value` — 0.0 to 1.0
81+
82+
## ActivityIndicator
83+
84+
```python
85+
pn.ActivityIndicator(animating=True)
86+
```
87+
88+
## WebView
89+
90+
```python
91+
pn.WebView(url="")
92+
```
93+
94+
## Spacer
95+
96+
```python
97+
pn.Spacer(size=None)
98+
```
99+
100+
- `size` — fixed dimension in dp / pt

docs/api/pythonnative.md

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
# pythonnative package
22

3-
API reference will be generated here via mkdocstrings.
3+
## Public API
44

5-
Key flags and helpers:
5+
### Page
66

7-
- `pythonnative.utils.IS_ANDROID`: platform flag with robust detection for Chaquopy/Android.
8-
- `pythonnative.utils.get_android_context()`: returns the current Android Activity/Context when running on Android.
9-
- `pythonnative.utils.set_android_context(ctx)`: set by `pythonnative.Page` on Android; you generally don’t call this directly.
10-
- `pythonnative.utils.get_android_fragment_container()`: returns the current Fragment container `ViewGroup` used for page rendering.
11-
- `pythonnative.utils.set_android_fragment_container(viewGroup)`: set by the host `PageFragment`; you generally don’t call this directly.
7+
`pythonnative.Page` — base class for screens. Subclass it, implement `render()`, and use `set_state()` to trigger re-renders.
8+
9+
### Element functions
10+
11+
- `pythonnative.Text`, `Button`, `Column`, `Row`, `ScrollView`, `TextInput`, `Image`, `Switch`, `ProgressBar`, `ActivityIndicator`, `WebView`, `Spacer`
12+
13+
Each returns an `Element` descriptor. See the Component Property Reference for full signatures.
14+
15+
### Element
16+
17+
`pythonnative.Element` — the descriptor type returned by element functions. You generally don't create these directly.
18+
19+
## Internal helpers
20+
21+
- `pythonnative.utils.IS_ANDROID` — platform flag with robust detection for Chaquopy/Android.
22+
- `pythonnative.utils.get_android_context()` — returns the current Android `Activity`/`Context` when running on Android.
23+
- `pythonnative.utils.set_android_context(ctx)` — set by `Page` on Android; you generally don't call this directly.
24+
- `pythonnative.utils.get_android_fragment_container()` — returns the current Fragment container `ViewGroup` used for page rendering.
25+
- `pythonnative.utils.set_android_fragment_container(viewGroup)` — set by the host `PageFragment`; you generally don't call this directly.
26+
27+
## Reconciler
28+
29+
`pythonnative.reconciler.Reconciler` — diffs element trees and applies minimal native mutations. Used internally by `Page`.
30+
31+
## Native view registry
32+
33+
`pythonnative.native_views.NativeViewRegistry` — maps element type names to platform-specific handlers. Use `set_registry()` to inject a mock for testing.

docs/concepts/architecture.md

Lines changed: 26 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,47 @@
11
# Architecture
22

3-
PythonNative maps Python directly to native platform APIs. Conceptually, it is closer to NativeScript's dynamic bindings than to React Native's bridge-and-module approach.
3+
PythonNative combines **direct native bindings** with a **declarative reconciler**, giving you React-like ergonomics while calling native platform APIs synchronously from Python.
44

55
## High-level model
66

7-
- Direct bindings: call native APIs synchronously from Python.
8-
- iOS: rubicon-objc exposes Objective-C/Swift classes (e.g., UIViewController, UIButton, WKWebView) and lets you create dynamic Objective-C subclasses and selectors.
9-
- Android: Chaquopy exposes Java classes (e.g., android.widget.Button, android.webkit.WebView) via the java bridge so you can construct and call methods directly.
10-
- Shared Python API: components like Page, StackView, Label, Button, and WebView have a small, consistent surface. Platform-specific behavior is chosen at import time using pythonnative.utils.IS_ANDROID.
11-
- Thin native bootstrap: the host app remains native (Android Activity or iOS UIViewController). It passes a live instance/pointer into Python, and Python drives the UI from there.
7+
1. **Declarative element tree:** Your `Page.render()` method returns a tree of `Element` descriptors (similar to React elements / virtual DOM nodes).
8+
2. **Reconciler:** On first render, the reconciler walks the tree and creates real native views via the platform backend. On subsequent renders (triggered by `set_state`), it diffs the new tree against the old one and applies the minimal set of native mutations.
9+
3. **Direct bindings:** Under the hood, native views are created and updated through direct platform calls:
10+
- **iOS:** rubicon-objc exposes Objective-C/Swift classes (`UILabel`, `UIButton`, `UIStackView`, etc.).
11+
- **Android:** Chaquopy exposes Java classes (`android.widget.TextView`, `android.widget.Button`, etc.) via the JNI bridge.
12+
4. **Thin native bootstrap:** The host app remains native (Android `Activity` or iOS `UIViewController`). It passes a live instance/pointer into Python, and Python drives the UI through the reconciler.
13+
14+
## How it works
15+
16+
```
17+
Page.render() → Element tree → Reconciler → Native views
18+
19+
Page.set_state() → re-render → diff → patch native views
20+
```
21+
22+
The reconciler uses **positional diffing** (comparing children by index). When a child at a given position has the same element type, its props are updated in-place on the native view. When the type changes, the old native view is destroyed and a new one is created.
1223

1324
## Comparison
1425

15-
- Versus React Native: RN typically exposes capabilities via native modules/TurboModules and a bridge. PythonNative does not require authoring such modules for most APIs; you can access platform classes directly from Python.
16-
- Versus NativeScript: similar philosophy—dynamic, synchronous access to Obj-C/Java from the scripting runtime.
26+
- **Versus React Native:** RN uses JSX + a JavaScript bridge + Yoga layout. PythonNative uses Python + direct native calls + platform layout managers. No JS bridge, no serialisation overhead.
27+
- **Versus NativeScript:** Similar philosophy (direct, synchronous native access), but PythonNative adds a declarative reconciler layer that NativeScript does not have by default.
28+
- **Versus the old imperative API:** The previous PythonNative API required manual `add_view()` calls and explicit setter methods. The new declarative model handles view lifecycle automatically.
1729

1830
## iOS flow (Rubicon-ObjC)
1931

2032
- The iOS template (Swift + PythonKit) boots Python and instantiates your `MainPage` with the current `UIViewController` pointer.
21-
- In Python, Rubicon wraps the pointer; you then interact with UIKit classes directly.
22-
23-
```python
24-
from rubicon.objc import ObjCClass, ObjCInstance
25-
26-
UIButton = ObjCClass("UIButton")
27-
vc = ObjCInstance(native_ptr) # passed from Swift template
28-
button = UIButton.alloc().init()
29-
# Configure target/action via a dynamic Objective-C subclass (see Button implementation)
30-
```
33+
- `Page.on_create()` calls `render()`, the reconciler creates UIKit views, and attaches them to the controller's view.
34+
- State changes trigger `render()` again; the reconciler patches UIKit views in-place.
3135

3236
## Android flow (Chaquopy)
3337

34-
- The Android template (Kotlin + Chaquopy) initializes Python in MainActivity and provides the current Activity/Context to Python.
35-
- Components acquire the Context implicitly and construct real Android views.
36-
37-
```python
38-
from java import jclass
39-
from pythonnative.utils import get_android_context
40-
41-
WebViewClass = jclass("android.webkit.WebView")
42-
context = get_android_context()
43-
webview = WebViewClass(context)
44-
webview.loadUrl("https://example.com")
45-
```
46-
47-
## Key implications
48-
49-
- Synchronous native calls: no JS bridge; Python calls are direct.
50-
- Lifecycle rules remain native: Activities/ViewControllers are created by the OS. Python receives and controls them; it does not instantiate Android Activities directly.
51-
- Small, growing surface: the shared Python API favors clarity and consistency, expanding progressively.
38+
- The Android template (Kotlin + Chaquopy) initializes Python in `MainActivity` and passes the `Activity` to Python.
39+
- `PageFragment` calls `on_create()` on the Python `Page`, which renders and attaches views to the fragment container.
40+
- State changes trigger re-render; the reconciler patches Android views in-place.
5241

5342
## Navigation model overview
5443

55-
- See the Navigation guide for full details and comparisons with other frameworks.
44+
- See the Navigation guide for full details.
5645
- iOS: one host `UIViewController` class, many instances pushed on a `UINavigationController`.
5746
- Android: single host `Activity` with a `NavHostFragment` and a stack of generic `PageFragment`s driven by a navigation graph.
5847

0 commit comments

Comments
 (0)