pythonnative 0.4.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. pythonnative/__init__.py +94 -66
  2. pythonnative/cli/pn.py +153 -24
  3. pythonnative/components.py +563 -0
  4. pythonnative/element.py +53 -0
  5. pythonnative/hooks.py +287 -0
  6. pythonnative/hot_reload.py +143 -0
  7. pythonnative/native_modules/__init__.py +19 -0
  8. pythonnative/native_modules/camera.py +105 -0
  9. pythonnative/native_modules/file_system.py +131 -0
  10. pythonnative/native_modules/location.py +61 -0
  11. pythonnative/native_modules/notifications.py +151 -0
  12. pythonnative/native_views.py +1334 -0
  13. pythonnative/page.py +320 -247
  14. pythonnative/reconciler.py +262 -0
  15. pythonnative/style.py +115 -0
  16. pythonnative/templates/android_template/app/build.gradle +2 -7
  17. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PageFragment.kt +2 -1
  18. pythonnative/templates/android_template/build.gradle +1 -1
  19. pythonnative/utils.py +21 -29
  20. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/METADATA +20 -19
  21. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/RECORD +25 -40
  22. pythonnative/activity_indicator_view.py +0 -71
  23. pythonnative/button.py +0 -113
  24. pythonnative/collection_view.py +0 -0
  25. pythonnative/date_picker.py +0 -76
  26. pythonnative/image_view.py +0 -78
  27. pythonnative/label.py +0 -133
  28. pythonnative/list_view.py +0 -76
  29. pythonnative/material_activity_indicator_view.py +0 -71
  30. pythonnative/material_bottom_navigation_view.py +0 -0
  31. pythonnative/material_button.py +0 -69
  32. pythonnative/material_date_picker.py +0 -87
  33. pythonnative/material_progress_view.py +0 -70
  34. pythonnative/material_search_bar.py +0 -69
  35. pythonnative/material_switch.py +0 -69
  36. pythonnative/material_time_picker.py +0 -76
  37. pythonnative/material_toolbar.py +0 -0
  38. pythonnative/picker_view.py +0 -69
  39. pythonnative/progress_view.py +0 -70
  40. pythonnative/scroll_view.py +0 -101
  41. pythonnative/search_bar.py +0 -69
  42. pythonnative/stack_view.py +0 -199
  43. pythonnative/switch.py +0 -68
  44. pythonnative/text_field.py +0 -132
  45. pythonnative/text_view.py +0 -135
  46. pythonnative/time_picker.py +0 -77
  47. pythonnative/view.py +0 -173
  48. pythonnative/web_view.py +0 -60
  49. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/WHEEL +0 -0
  50. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/entry_points.txt +0 -0
  51. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/licenses/LICENSE +0 -0
  52. {pythonnative-0.4.0.dist-info → pythonnative-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,262 @@
1
+ """Virtual-tree reconciler.
2
+
3
+ Maintains a tree of :class:`VNode` objects (each wrapping a native view)
4
+ and diffs incoming :class:`Element` trees to apply the minimal set of
5
+ native mutations.
6
+
7
+ Supports:
8
+
9
+ - **Native elements** (type is a string like ``"Text"``).
10
+ - **Function components** (type is a callable decorated with
11
+ ``@component``). Their hook state is preserved across renders.
12
+ - **Provider elements** (type ``"__Provider__"``), which push/pop
13
+ context values during tree traversal.
14
+ - **Key-based child reconciliation** for stable identity across
15
+ re-renders.
16
+ """
17
+
18
+ from typing import Any, List, Optional
19
+
20
+ from .element import Element
21
+
22
+
23
+ class VNode:
24
+ """A mounted element paired with its native view and child VNodes."""
25
+
26
+ __slots__ = ("element", "native_view", "children", "hook_state", "_rendered")
27
+
28
+ def __init__(self, element: Element, native_view: Any, children: List["VNode"]) -> None:
29
+ self.element = element
30
+ self.native_view = native_view
31
+ self.children = children
32
+ self.hook_state: Any = None
33
+ self._rendered: Optional[Element] = None
34
+
35
+
36
+ class Reconciler:
37
+ """Create, diff, and patch native view trees from Element descriptors.
38
+
39
+ Parameters
40
+ ----------
41
+ backend:
42
+ An object implementing the :class:`NativeViewRegistry` protocol
43
+ (``create_view``, ``update_view``, ``add_child``, ``remove_child``,
44
+ ``insert_child``).
45
+ """
46
+
47
+ def __init__(self, backend: Any) -> None:
48
+ self.backend = backend
49
+ self._tree: Optional[VNode] = None
50
+ self._page_re_render: Optional[Any] = None
51
+
52
+ # ------------------------------------------------------------------
53
+ # Public API
54
+ # ------------------------------------------------------------------
55
+
56
+ def mount(self, element: Element) -> Any:
57
+ """Build native views from *element* and return the root native view."""
58
+ self._tree = self._create_tree(element)
59
+ return self._tree.native_view
60
+
61
+ def reconcile(self, new_element: Element) -> Any:
62
+ """Diff *new_element* against the current tree and patch native views.
63
+
64
+ Returns the (possibly replaced) root native view.
65
+ """
66
+ if self._tree is None:
67
+ self._tree = self._create_tree(new_element)
68
+ return self._tree.native_view
69
+
70
+ self._tree = self._reconcile_node(self._tree, new_element)
71
+ return self._tree.native_view
72
+
73
+ # ------------------------------------------------------------------
74
+ # Internal helpers
75
+ # ------------------------------------------------------------------
76
+
77
+ def _create_tree(self, element: Element) -> VNode:
78
+ # Provider: push context, create child, pop context
79
+ if element.type == "__Provider__":
80
+ context = element.props["__context__"]
81
+ context._stack.append(element.props["__value__"])
82
+ try:
83
+ child_node = self._create_tree(element.children[0]) if element.children else None
84
+ finally:
85
+ context._stack.pop()
86
+ native_view = child_node.native_view if child_node else None
87
+ children = [child_node] if child_node else []
88
+ return VNode(element, native_view, children)
89
+
90
+ # Function component: call with hook context
91
+ if callable(element.type):
92
+ from .hooks import HookState, _set_hook_state
93
+
94
+ hook_state = HookState()
95
+ hook_state._trigger_render = self._page_re_render
96
+ _set_hook_state(hook_state)
97
+ try:
98
+ rendered = element.type(**element.props)
99
+ finally:
100
+ _set_hook_state(None)
101
+
102
+ child_node = self._create_tree(rendered)
103
+ vnode = VNode(element, child_node.native_view, [child_node])
104
+ vnode.hook_state = hook_state
105
+ vnode._rendered = rendered
106
+ return vnode
107
+
108
+ # Native element
109
+ native_view = self.backend.create_view(element.type, element.props)
110
+ children: List[VNode] = []
111
+ for child_el in element.children:
112
+ child_node = self._create_tree(child_el)
113
+ self.backend.add_child(native_view, child_node.native_view, element.type)
114
+ children.append(child_node)
115
+ return VNode(element, native_view, children)
116
+
117
+ def _reconcile_node(self, old: VNode, new_el: Element) -> VNode:
118
+ if not self._same_type(old.element, new_el):
119
+ new_node = self._create_tree(new_el)
120
+ self._destroy_tree(old)
121
+ return new_node
122
+
123
+ # Provider
124
+ if new_el.type == "__Provider__":
125
+ context = new_el.props["__context__"]
126
+ context._stack.append(new_el.props["__value__"])
127
+ try:
128
+ if old.children and new_el.children:
129
+ child = self._reconcile_node(old.children[0], new_el.children[0])
130
+ old.children = [child]
131
+ old.native_view = child.native_view
132
+ elif new_el.children:
133
+ child = self._create_tree(new_el.children[0])
134
+ old.children = [child]
135
+ old.native_view = child.native_view
136
+ finally:
137
+ context._stack.pop()
138
+ old.element = new_el
139
+ return old
140
+
141
+ # Function component
142
+ if callable(new_el.type):
143
+ from .hooks import _set_hook_state
144
+
145
+ hook_state = old.hook_state
146
+ if hook_state is None:
147
+ from .hooks import HookState
148
+
149
+ hook_state = HookState()
150
+ hook_state.reset_index()
151
+ hook_state._trigger_render = self._page_re_render
152
+ _set_hook_state(hook_state)
153
+ try:
154
+ rendered = new_el.type(**new_el.props)
155
+ finally:
156
+ _set_hook_state(None)
157
+
158
+ if old.children:
159
+ child = self._reconcile_node(old.children[0], rendered)
160
+ else:
161
+ child = self._create_tree(rendered)
162
+ old.children = [child]
163
+ old.native_view = child.native_view
164
+ old.element = new_el
165
+ old.hook_state = hook_state
166
+ old._rendered = rendered
167
+ return old
168
+
169
+ # Native element
170
+ changed = self._diff_props(old.element.props, new_el.props)
171
+ if changed:
172
+ self.backend.update_view(old.native_view, old.element.type, changed)
173
+
174
+ self._reconcile_children(old, new_el.children)
175
+ old.element = new_el
176
+ return old
177
+
178
+ def _reconcile_children(self, parent: VNode, new_children: List[Element]) -> None:
179
+ old_children = parent.children
180
+ parent_type = parent.element.type
181
+ is_native = isinstance(parent_type, str) and parent_type != "__Provider__"
182
+
183
+ old_by_key: dict = {}
184
+ old_unkeyed: list = []
185
+ for child in old_children:
186
+ if child.element.key is not None:
187
+ old_by_key[child.element.key] = child
188
+ else:
189
+ old_unkeyed.append(child)
190
+
191
+ new_child_nodes: List[VNode] = []
192
+ used_keyed: set = set()
193
+ unkeyed_iter = iter(old_unkeyed)
194
+
195
+ for i, new_el in enumerate(new_children):
196
+ matched: Optional[VNode] = None
197
+
198
+ if new_el.key is not None and new_el.key in old_by_key:
199
+ matched = old_by_key[new_el.key]
200
+ used_keyed.add(new_el.key)
201
+ elif new_el.key is None:
202
+ matched = next(unkeyed_iter, None)
203
+
204
+ if matched is None:
205
+ node = self._create_tree(new_el)
206
+ if is_native:
207
+ self.backend.add_child(parent.native_view, node.native_view, parent_type)
208
+ new_child_nodes.append(node)
209
+ elif not self._same_type(matched.element, new_el):
210
+ if is_native:
211
+ self.backend.remove_child(parent.native_view, matched.native_view, parent_type)
212
+ self._destroy_tree(matched)
213
+ node = self._create_tree(new_el)
214
+ if is_native:
215
+ self.backend.insert_child(parent.native_view, node.native_view, parent_type, i)
216
+ new_child_nodes.append(node)
217
+ else:
218
+ updated = self._reconcile_node(matched, new_el)
219
+ new_child_nodes.append(updated)
220
+
221
+ # Destroy unused old nodes
222
+ for key, node in old_by_key.items():
223
+ if key not in used_keyed:
224
+ if is_native:
225
+ self.backend.remove_child(parent.native_view, node.native_view, parent_type)
226
+ self._destroy_tree(node)
227
+ for node in unkeyed_iter:
228
+ if is_native:
229
+ self.backend.remove_child(parent.native_view, node.native_view, parent_type)
230
+ self._destroy_tree(node)
231
+
232
+ parent.children = new_child_nodes
233
+
234
+ def _destroy_tree(self, node: VNode) -> None:
235
+ if node.hook_state is not None:
236
+ node.hook_state.cleanup_all_effects()
237
+ for child in node.children:
238
+ self._destroy_tree(child)
239
+ node.children = []
240
+
241
+ @staticmethod
242
+ def _same_type(old_el: Element, new_el: Element) -> bool:
243
+ if isinstance(old_el.type, str):
244
+ return old_el.type == new_el.type
245
+ return old_el.type is new_el.type
246
+
247
+ @staticmethod
248
+ def _diff_props(old: dict, new: dict) -> dict:
249
+ """Return props that changed (callables always count as changed)."""
250
+ changed = {}
251
+ for key, new_val in new.items():
252
+ if key.startswith("__"):
253
+ continue
254
+ old_val = old.get(key)
255
+ if callable(new_val) or old_val != new_val:
256
+ changed[key] = new_val
257
+ for key in old:
258
+ if key.startswith("__"):
259
+ continue
260
+ if key not in new:
261
+ changed[key] = None
262
+ return changed
pythonnative/style.py ADDED
@@ -0,0 +1,115 @@
1
+ """StyleSheet and theming support.
2
+
3
+ Provides a :class:`StyleSheet` helper for creating and composing
4
+ reusable style dictionaries, plus a built-in theme context.
5
+
6
+ Usage::
7
+
8
+ import pythonnative as pn
9
+
10
+ styles = pn.StyleSheet.create(
11
+ title={"font_size": 24, "bold": True, "color": "#333"},
12
+ container={"padding": 16, "spacing": 12},
13
+ )
14
+
15
+ pn.Text("Hello", **styles["title"])
16
+ pn.Column(..., **styles["container"])
17
+ """
18
+
19
+ from typing import Any, Dict
20
+
21
+ from .hooks import Context, create_context
22
+
23
+ # ======================================================================
24
+ # StyleSheet
25
+ # ======================================================================
26
+
27
+ _StyleDict = Dict[str, Any]
28
+
29
+
30
+ class StyleSheet:
31
+ """Utility for creating and composing style dictionaries."""
32
+
33
+ @staticmethod
34
+ def create(**named_styles: _StyleDict) -> Dict[str, _StyleDict]:
35
+ """Create a set of named styles.
36
+
37
+ Each keyword argument is a style name mapping to a dict of
38
+ property values::
39
+
40
+ styles = StyleSheet.create(
41
+ heading={"font_size": 28, "bold": True},
42
+ body={"font_size": 16},
43
+ )
44
+ """
45
+ return {name: dict(props) for name, props in named_styles.items()}
46
+
47
+ @staticmethod
48
+ def compose(*styles: _StyleDict) -> _StyleDict:
49
+ """Merge multiple style dicts, later values overriding earlier ones."""
50
+ merged: _StyleDict = {}
51
+ for style in styles:
52
+ if style:
53
+ merged.update(style)
54
+ return merged
55
+
56
+ @staticmethod
57
+ def flatten(styles: Any) -> _StyleDict:
58
+ """Flatten a style or list of styles into a single dict.
59
+
60
+ Accepts a single dict, a list of dicts, or ``None``.
61
+ """
62
+ if styles is None:
63
+ return {}
64
+ if isinstance(styles, dict):
65
+ return dict(styles)
66
+ result: _StyleDict = {}
67
+ for s in styles:
68
+ if s:
69
+ result.update(s)
70
+ return result
71
+
72
+
73
+ # ======================================================================
74
+ # Theming
75
+ # ======================================================================
76
+
77
+ DEFAULT_LIGHT_THEME: _StyleDict = {
78
+ "primary_color": "#007AFF",
79
+ "secondary_color": "#5856D6",
80
+ "background_color": "#FFFFFF",
81
+ "surface_color": "#F2F2F7",
82
+ "text_color": "#000000",
83
+ "text_secondary_color": "#8E8E93",
84
+ "error_color": "#FF3B30",
85
+ "success_color": "#34C759",
86
+ "warning_color": "#FF9500",
87
+ "font_size": 16,
88
+ "font_size_small": 13,
89
+ "font_size_large": 20,
90
+ "font_size_title": 28,
91
+ "spacing": 8,
92
+ "spacing_large": 16,
93
+ "border_radius": 8,
94
+ }
95
+
96
+ DEFAULT_DARK_THEME: _StyleDict = {
97
+ "primary_color": "#0A84FF",
98
+ "secondary_color": "#5E5CE6",
99
+ "background_color": "#000000",
100
+ "surface_color": "#1C1C1E",
101
+ "text_color": "#FFFFFF",
102
+ "text_secondary_color": "#8E8E93",
103
+ "error_color": "#FF453A",
104
+ "success_color": "#30D158",
105
+ "warning_color": "#FF9F0A",
106
+ "font_size": 16,
107
+ "font_size_small": 13,
108
+ "font_size_large": 20,
109
+ "font_size_title": 28,
110
+ "spacing": 8,
111
+ "spacing_large": 16,
112
+ "border_radius": 8,
113
+ }
114
+
115
+ ThemeContext: Context = create_context(DEFAULT_LIGHT_THEME)
@@ -20,14 +20,9 @@ android {
20
20
  abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
21
21
  }
22
22
  python {
23
- version "3.8"
23
+ version "3.11"
24
24
  pip {
25
- install "matplotlib"
26
- install "pythonnative"
27
-
28
- // "-r"` followed by a requirements filename, relative to the
29
- // project directory:
30
- // install "-r", "requirements.txt"
25
+ install "-r", "requirements.txt"
31
26
  }
32
27
  }
33
28
  }
@@ -65,7 +65,8 @@ class PageFragment : Fragment() {
65
65
  utils.callAttr("set_android_fragment_container", view)
66
66
  // Now that container exists, invoke on_create so Python can attach its root view
67
67
  page?.callAttr("on_create")
68
- } catch (_: Exception) {
68
+ } catch (e: Exception) {
69
+ Log.e(TAG, "on_create failed", e)
69
70
  }
70
71
  }
71
72
 
@@ -3,5 +3,5 @@ plugins {
3
3
  id 'com.android.application' version '8.2.2' apply false
4
4
  id 'com.android.library' version '8.2.2' apply false
5
5
  id 'org.jetbrains.kotlin.android' version '1.9.22' apply false
6
- id 'com.chaquo.python' version '14.0.2' apply false
6
+ id 'com.chaquo.python' version '15.0.1' apply false
7
7
  }
pythonnative/utils.py CHANGED
@@ -1,27 +1,29 @@
1
+ """Platform detection and shared helpers.
2
+
3
+ This module is imported early by most other modules, so it avoids
4
+ importing platform-specific packages at module level.
5
+ """
6
+
1
7
  import os
2
8
  from typing import Any, Optional
3
9
 
4
- # Platform detection with multiple fallbacks suitable for Chaquopy/Android
10
+ # ======================================================================
11
+ # Platform detection
12
+ # ======================================================================
13
+
5
14
  _is_android: Optional[bool] = None
6
15
 
7
16
 
8
17
  def _detect_android() -> bool:
9
- # 1) Direct environment hints commonly present on Android
10
18
  env = os.environ
11
19
  if "ANDROID_BOOTLOGO" in env or "ANDROID_ROOT" in env or "ANDROID_DATA" in env or "ANDROID_ARGUMENT" in env:
12
20
  return True
13
-
14
- # 2) Chaquopy-specific: the builtin 'java' package is available
15
21
  try:
16
- # Import inside try so importing this module doesn't explode off-device
17
- from java import jclass
22
+ from java import jclass # noqa: F401
18
23
 
19
- _ = jclass # silence linter unused
20
24
  return True
21
25
  except Exception:
22
26
  pass
23
-
24
- # 3) Last resort: some Android Python dists set os.name/others, but avoid false positives
25
27
  return False
26
28
 
27
29
 
@@ -39,53 +41,43 @@ def _get_is_android() -> bool:
39
41
 
40
42
  IS_ANDROID: bool = _get_is_android()
41
43
 
42
- # Global hooks to access current Android Activity/Context and Fragment container from Python code
44
+ # ======================================================================
45
+ # Android context management
46
+ # ======================================================================
47
+
43
48
  _android_context: Any = None
44
49
  _android_fragment_container: Any = None
45
50
 
46
51
 
47
52
  def set_android_context(context: Any) -> None:
48
- """Record the current Android Activity/Context for implicit constructor use.
49
-
50
- On Android, Python UI components require a Context to create native views.
51
- We capture it when a Page is constructed from the host Activity so component
52
- constructors can be platform-consistent and avoid explicit context params.
53
- """
54
-
53
+ """Record the current Android Activity/Context for view construction."""
55
54
  global _android_context
56
55
  _android_context = context
57
56
 
58
57
 
59
58
  def set_android_fragment_container(container_view: Any) -> None:
60
- """Record the current Fragment root container ViewGroup for rendering pages.
61
-
62
- The current Page's `set_root_view` will attach its native view to this container.
63
- """
59
+ """Record the current Fragment root container ViewGroup."""
64
60
  global _android_fragment_container
65
61
  _android_fragment_container = container_view
66
62
 
67
63
 
68
64
  def get_android_context() -> Any:
69
- """Return the previously set Android Activity/Context or raise if missing."""
70
-
65
+ """Return the current Android Activity/Context."""
71
66
  if not IS_ANDROID:
72
67
  raise RuntimeError("get_android_context() called on non-Android platform")
73
68
  if _android_context is None:
74
69
  raise RuntimeError(
75
- "Android context is not set. Ensure Page is initialized from an Activity " "before constructing views."
70
+ "Android context not set. Ensure Page is initialized from an Activity before constructing views."
76
71
  )
77
72
  return _android_context
78
73
 
79
74
 
80
75
  def get_android_fragment_container() -> Any:
81
- """Return the previously set Fragment container ViewGroup or raise if missing.
82
-
83
- This is set by the host `PageFragment` when its view is created.
84
- """
76
+ """Return the current Fragment container ViewGroup."""
85
77
  if not IS_ANDROID:
86
78
  raise RuntimeError("get_android_fragment_container() called on non-Android platform")
87
79
  if _android_fragment_container is None:
88
80
  raise RuntimeError(
89
- "Android fragment container is not set. Ensure PageFragment has been created before set_root_view."
81
+ "Android fragment container not set. Ensure PageFragment has been created before set_root_view."
90
82
  )
91
83
  return _android_fragment_container
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -34,12 +34,11 @@ Classifier: Intended Audience :: Developers
34
34
  Classifier: License :: OSI Approved :: MIT License
35
35
  Classifier: Programming Language :: Python :: 3
36
36
  Classifier: Programming Language :: Python :: 3 :: Only
37
- Classifier: Programming Language :: Python :: 3.9
38
37
  Classifier: Programming Language :: Python :: 3.10
39
38
  Classifier: Programming Language :: Python :: 3.11
40
39
  Classifier: Programming Language :: Python :: 3.12
41
40
  Classifier: Topic :: Software Development :: User Interfaces
42
- Requires-Python: >=3.9
41
+ Requires-Python: >=3.10
43
42
  Description-Content-Type: text/markdown
44
43
  License-File: LICENSE
45
44
  Requires-Dist: requests>=2.31.0
@@ -89,18 +88,17 @@ Dynamic: license-file
89
88
 
90
89
  ## Overview
91
90
 
92
- 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.
91
+ 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.
93
92
 
94
93
  ## Features
95
94
 
96
- - **Cross-platform native UI:** Build Android and iOS apps from a single Python codebase with truly native rendering.
95
+ - **Declarative UI:** Describe *what* your UI should look like with element functions (`Text`, `Button`, `Column`, `Row`, etc.). PythonNative creates and updates native views automatically.
96
+ - **Reactive state:** Call `self.set_state(key=value)` and the framework re-renders only what changed — no manual view mutation.
97
+ - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
97
98
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
98
- - **Unified component API:** Components like `Page`, `StackView`, `Label`, `Button`, and `WebView` share a consistent interface across platforms.
99
- - **CLI scaffolding:** `pn init` creates a ready-to-run project structure; `pn run android` and `pn run ios` build and launch your app.
100
- - **Page lifecycle:** Hooks for `on_create`, `on_start`, `on_resume`, `on_pause`, `on_stop`, and `on_destroy`, with state save and restore.
99
+ - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
101
100
  - **Navigation:** Push and pop screens with argument passing for multi-page apps.
102
- - **Rich component set:** Core views (Label, Button, TextField, ImageView, WebView, Switch, DatePicker, and more) plus Material Design variants.
103
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
101
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
104
102
 
105
103
  ## Quick Start
106
104
 
@@ -119,15 +117,18 @@ import pythonnative as pn
119
117
  class MainPage(pn.Page):
120
118
  def __init__(self, native_instance):
121
119
  super().__init__(native_instance)
122
-
123
- def on_create(self):
124
- super().on_create()
125
- stack = pn.StackView()
126
- stack.add_view(pn.Label("Hello from PythonNative!"))
127
- button = pn.Button("Tap me")
128
- button.set_on_click(lambda: print("Button tapped"))
129
- stack.add_view(button)
130
- self.set_root_view(stack)
120
+ self.state = {"count": 0}
121
+
122
+ def render(self):
123
+ return pn.Column(
124
+ pn.Text(f"Count: {self.state['count']}", font_size=24),
125
+ pn.Button(
126
+ "Tap me",
127
+ on_click=lambda: self.set_state(count=self.state["count"] + 1),
128
+ ),
129
+ spacing=12,
130
+ padding=16,
131
+ )
131
132
  ```
132
133
 
133
134
  ## Documentation