pythonnative 0.6.0__py3-none-any.whl → 0.7.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.
pythonnative/page.py CHANGED
@@ -1,161 +1,107 @@
1
- """Page — the root component that bridges native lifecycle and declarative UI.
1
+ """Page host — the bridge between native lifecycle and function components.
2
2
 
3
- A ``Page`` subclass is the entry point for each screen. It owns a
4
- :class:`~pythonnative.reconciler.Reconciler` and automatically mounts /
5
- re-renders the element tree returned by :meth:`render` whenever state
6
- changes.
3
+ Users no longer subclass ``Page``. Instead they write ``@component``
4
+ functions and the native template calls :func:`create_page` to obtain
5
+ an :class:`_AppHost` that manages the reconciler and lifecycle.
7
6
 
8
- Usage::
7
+ Usage (user code)::
9
8
 
10
9
  import pythonnative as pn
11
10
 
12
- class MainPage(pn.Page):
13
- def __init__(self, native_instance):
14
- super().__init__(native_instance)
15
- self.state = {"count": 0}
16
-
17
- def increment(self):
18
- self.set_state(count=self.state["count"] + 1)
19
-
20
- def render(self):
21
- return pn.Column(
22
- pn.Text(f"Count: {self.state['count']}", font_size=24),
23
- pn.Button("Increment", on_click=self.increment),
24
- spacing=12,
25
- padding=16,
26
- )
11
+ @pn.component
12
+ def MainPage():
13
+ count, set_count = pn.use_state(0)
14
+ return pn.Column(
15
+ pn.Text(f"Count: {count}", style={"font_size": 24}),
16
+ pn.Button("Tap me", on_click=lambda: set_count(count + 1)),
17
+ style={"spacing": 12, "padding": 16},
18
+ )
19
+
20
+ The native template calls::
21
+
22
+ host = pythonnative.page.create_page("app.main_page.MainPage", native_instance)
23
+ host.on_create()
27
24
  """
28
25
 
26
+ import importlib
29
27
  import json
30
- from abc import ABC, abstractmethod
31
- from typing import Any, Optional, Union
28
+ from typing import Any, Dict, Optional
32
29
 
33
30
  from .utils import IS_ANDROID, set_android_context
34
31
 
35
32
  # ======================================================================
36
- # Base class (platform-independent)
33
+ # Component path resolution
37
34
  # ======================================================================
38
35
 
39
36
 
40
- class PageBase(ABC):
41
- """Abstract base defining the Page interface."""
42
-
43
- @abstractmethod
44
- def __init__(self) -> None:
45
- super().__init__()
46
-
47
- @abstractmethod
48
- def render(self) -> Any:
49
- """Return an Element tree describing this page's UI."""
50
-
51
- def set_state(self, **updates: Any) -> None:
52
- """Merge *updates* into ``self.state`` and trigger a re-render."""
53
-
54
- def on_create(self) -> None:
55
- """Called when the page is first created. Triggers initial render."""
56
-
57
- def on_start(self) -> None:
58
- pass
59
-
60
- def on_resume(self) -> None:
61
- pass
62
-
63
- def on_pause(self) -> None:
64
- pass
65
-
66
- def on_stop(self) -> None:
67
- pass
68
-
69
- def on_destroy(self) -> None:
70
- pass
71
-
72
- def on_restart(self) -> None:
73
- pass
74
-
75
- def on_save_instance_state(self) -> None:
76
- pass
77
-
78
- def on_restore_instance_state(self) -> None:
79
- pass
80
-
81
- @abstractmethod
82
- def set_args(self, args: Optional[dict]) -> None:
83
- pass
84
-
85
- @abstractmethod
86
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
87
- pass
88
-
89
- @abstractmethod
90
- def pop(self) -> None:
91
- pass
37
+ def _resolve_component_path(page_ref: Any) -> str:
38
+ """Resolve a component function to a ``module.name`` path string."""
39
+ if isinstance(page_ref, str):
40
+ return page_ref
41
+ func = getattr(page_ref, "__wrapped__", page_ref)
42
+ module = getattr(func, "__module__", None)
43
+ name = getattr(func, "__name__", None)
44
+ if module and name:
45
+ return f"{module}.{name}"
46
+ raise ValueError(f"Cannot resolve component path for {page_ref!r}")
92
47
 
93
- def get_args(self) -> dict:
94
- """Return navigation arguments (empty dict if none)."""
95
- return getattr(self, "_args", {})
96
48
 
97
- def navigate_to(self, page: Any) -> None:
98
- self.push(page)
49
+ def _import_component(component_path: str) -> Any:
50
+ """Import and return the component function from a dotted path."""
51
+ module_path, component_name = component_path.rsplit(".", 1)
52
+ module = importlib.import_module(module_path)
53
+ return getattr(module, component_name)
99
54
 
100
55
 
101
56
  # ======================================================================
102
- # Shared declarative rendering helpers
57
+ # Shared helpers
103
58
  # ======================================================================
104
59
 
105
60
 
106
- def _init_page_common(page: Any) -> None:
107
- """Common initialisation shared by both platform Page classes."""
108
- page.state = {}
109
- page._args = {}
110
- page._reconciler = None
111
- page._root_native_view = None
61
+ def _init_host_common(host: Any) -> None:
62
+ host._args = {}
63
+ host._reconciler = None
64
+ host._root_native_view = None
112
65
 
113
66
 
114
- def _set_state(page: Any, **updates: Any) -> None:
115
- page.state.update(updates)
116
- if page._reconciler is not None:
117
- _re_render(page)
118
-
119
-
120
- def _on_create(page: Any) -> None:
67
+ def _on_create(host: Any) -> None:
68
+ from .hooks import NavigationHandle, Provider, _NavigationContext
121
69
  from .native_views import get_registry
122
70
  from .reconciler import Reconciler
123
71
 
124
- page._reconciler = Reconciler(get_registry())
125
- page._reconciler._page_re_render = lambda: _re_render(page)
126
- element = page.render()
127
- page._root_native_view = page._reconciler.mount(element)
128
- page._attach_root(page._root_native_view)
72
+ host._reconciler = Reconciler(get_registry())
73
+ host._reconciler._page_re_render = lambda: _re_render(host)
129
74
 
75
+ nav_handle = NavigationHandle(host)
76
+ app_element = host._component()
77
+ provider_element = Provider(_NavigationContext, nav_handle, app_element)
130
78
 
131
- def _re_render(page: Any) -> None:
132
- element = page.render()
133
- new_root = page._reconciler.reconcile(element)
134
- if new_root is not page._root_native_view:
135
- page._detach_root(page._root_native_view)
136
- page._root_native_view = new_root
137
- page._attach_root(new_root)
79
+ host._root_native_view = host._reconciler.mount(provider_element)
80
+ host._attach_root(host._root_native_view)
138
81
 
139
82
 
140
- def _resolve_page_path(page_ref: Union[str, Any]) -> str:
141
- if isinstance(page_ref, str):
142
- return page_ref
143
- module = getattr(page_ref, "__module__", None)
144
- name = getattr(page_ref, "__name__", None)
145
- if module and name:
146
- return f"{module}.{name}"
147
- cls = page_ref.__class__
148
- return f"{cls.__module__}.{cls.__name__}"
83
+ def _re_render(host: Any) -> None:
84
+ from .hooks import NavigationHandle, Provider, _NavigationContext
85
+
86
+ nav_handle = NavigationHandle(host)
87
+ app_element = host._component()
88
+ provider_element = Provider(_NavigationContext, nav_handle, app_element)
149
89
 
90
+ new_root = host._reconciler.reconcile(provider_element)
91
+ if new_root is not host._root_native_view:
92
+ host._detach_root(host._root_native_view)
93
+ host._root_native_view = new_root
94
+ host._attach_root(new_root)
150
95
 
151
- def _set_args(page: Any, args: Optional[dict]) -> None:
96
+
97
+ def _set_args(host: Any, args: Any) -> None:
152
98
  if isinstance(args, str):
153
99
  try:
154
- page._args = json.loads(args) or {}
100
+ host._args = json.loads(args) or {}
155
101
  except Exception:
156
- page._args = {}
102
+ host._args = {}
157
103
  return
158
- page._args = args or {}
104
+ host._args = args if isinstance(args, dict) else {}
159
105
 
160
106
 
161
107
  # ======================================================================
@@ -165,21 +111,14 @@ def _set_args(page: Any, args: Optional[dict]) -> None:
165
111
  if IS_ANDROID:
166
112
  from java import jclass
167
113
 
168
- class Page(PageBase):
169
- """Android Page backed by an Activity and Fragment navigation."""
114
+ class _AppHost:
115
+ """Android host backed by an Activity and Fragment navigation."""
170
116
 
171
- def __init__(self, native_instance: Any) -> None:
172
- super().__init__()
173
- self.native_class = jclass("android.app.Activity")
117
+ def __init__(self, native_instance: Any, component_func: Any) -> None:
174
118
  self.native_instance = native_instance
119
+ self._component = component_func
175
120
  set_android_context(native_instance)
176
- _init_page_common(self)
177
-
178
- def render(self) -> Any:
179
- raise NotImplementedError("Page subclass must implement render()")
180
-
181
- def set_state(self, **updates: Any) -> None:
182
- _set_state(self, **updates)
121
+ _init_host_common(self)
183
122
 
184
123
  def on_create(self) -> None:
185
124
  _on_create(self)
@@ -208,16 +147,19 @@ if IS_ANDROID:
208
147
  def on_restore_instance_state(self) -> None:
209
148
  pass
210
149
 
211
- def set_args(self, args: Optional[dict]) -> None:
150
+ def set_args(self, args: Any) -> None:
212
151
  _set_args(self, args)
213
152
 
214
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
215
- page_path = _resolve_page_path(page)
153
+ def _get_nav_args(self) -> Dict[str, Any]:
154
+ return self._args
155
+
156
+ def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
157
+ page_path = _resolve_component_path(page)
216
158
  Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
217
159
  args_json = json.dumps(args) if args else None
218
160
  Navigator.push(self.native_instance, page_path, args_json)
219
161
 
220
- def pop(self) -> None:
162
+ def _pop(self) -> None:
221
163
  try:
222
164
  Navigator = jclass(f"{self.native_instance.getPackageName()}.Navigator")
223
165
  Navigator.pop(self.native_instance)
@@ -265,10 +207,10 @@ else:
265
207
 
266
208
  _IOS_PAGE_REGISTRY: _Dict[int, Any] = {}
267
209
 
268
- def _ios_register_page(vc_instance: Any, page_obj: Any) -> None:
210
+ def _ios_register_page(vc_instance: Any, host_obj: Any) -> None:
269
211
  try:
270
212
  ptr = int(vc_instance.ptr)
271
- _IOS_PAGE_REGISTRY[ptr] = page_obj
213
+ _IOS_PAGE_REGISTRY[ptr] = host_obj
272
214
  except Exception:
273
215
  pass
274
216
 
@@ -280,38 +222,31 @@ else:
280
222
  pass
281
223
 
282
224
  def forward_lifecycle(native_addr: int, event: str) -> None:
283
- """Forward a lifecycle event from Swift ViewController to the registered Page."""
284
- page = _IOS_PAGE_REGISTRY.get(int(native_addr))
285
- if page is None:
225
+ """Forward a lifecycle event from Swift ViewController to the registered host."""
226
+ host = _IOS_PAGE_REGISTRY.get(int(native_addr))
227
+ if host is None:
286
228
  return
287
- handler = getattr(page, event, None)
229
+ handler = getattr(host, event, None)
288
230
  if handler:
289
231
  handler()
290
232
 
291
233
  if _rubicon_available:
292
234
 
293
- class Page(PageBase):
294
- """iOS Page backed by a UIViewController."""
235
+ class _AppHost:
236
+ """iOS host backed by a UIViewController."""
295
237
 
296
- def __init__(self, native_instance: Any) -> None:
297
- super().__init__()
298
- self.native_class = ObjCClass("UIViewController")
238
+ def __init__(self, native_instance: Any, component_func: Any) -> None:
299
239
  if isinstance(native_instance, int):
300
240
  try:
301
241
  native_instance = ObjCInstance(native_instance)
302
242
  except Exception:
303
243
  native_instance = None
304
244
  self.native_instance = native_instance
305
- _init_page_common(self)
245
+ self._component = component_func
246
+ _init_host_common(self)
306
247
  if self.native_instance is not None:
307
248
  _ios_register_page(self.native_instance, self)
308
249
 
309
- def render(self) -> Any:
310
- raise NotImplementedError("Page subclass must implement render()")
311
-
312
- def set_state(self, **updates: Any) -> None:
313
- _set_state(self, **updates)
314
-
315
250
  def on_create(self) -> None:
316
251
  _on_create(self)
317
252
 
@@ -340,11 +275,14 @@ else:
340
275
  def on_restore_instance_state(self) -> None:
341
276
  pass
342
277
 
343
- def set_args(self, args: Optional[dict]) -> None:
278
+ def set_args(self, args: Any) -> None:
344
279
  _set_args(self, args)
345
280
 
346
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
347
- page_path = _resolve_page_path(page)
281
+ def _get_nav_args(self) -> Dict[str, Any]:
282
+ return self._args
283
+
284
+ def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
285
+ page_path = _resolve_component_path(page)
348
286
  ViewController = None
349
287
  try:
350
288
  ViewController = ObjCClass("ViewController")
@@ -373,11 +311,11 @@ else:
373
311
  nav = getattr(self.native_instance, "navigationController", None)
374
312
  if nav is None:
375
313
  raise RuntimeError(
376
- "No UINavigationController available; ensure template embeds root in navigation controller"
314
+ "No UINavigationController available; " "ensure template embeds root in navigation controller"
377
315
  )
378
316
  nav.pushViewController_animated_(next_vc, True)
379
317
 
380
- def pop(self) -> None:
318
+ def _pop(self) -> None:
381
319
  nav = getattr(self.native_instance, "navigationController", None)
382
320
  if nav is not None:
383
321
  nav.popViewControllerAnimated_(True)
@@ -408,23 +346,17 @@ else:
408
346
 
409
347
  else:
410
348
 
411
- class Page(PageBase):
349
+ class _AppHost:
412
350
  """Desktop stub — no native runtime available.
413
351
 
414
352
  Fully functional for testing with a mock backend via
415
353
  ``native_views.set_registry()``.
416
354
  """
417
355
 
418
- def __init__(self, native_instance: Any = None) -> None:
419
- super().__init__()
356
+ def __init__(self, native_instance: Any = None, component_func: Any = None) -> None:
420
357
  self.native_instance = native_instance
421
- _init_page_common(self)
422
-
423
- def render(self) -> Any:
424
- raise NotImplementedError("Page subclass must implement render()")
425
-
426
- def set_state(self, **updates: Any) -> None:
427
- _set_state(self, **updates)
358
+ self._component = component_func
359
+ _init_host_common(self)
428
360
 
429
361
  def on_create(self) -> None:
430
362
  _on_create(self)
@@ -453,13 +385,16 @@ else:
453
385
  def on_restore_instance_state(self) -> None:
454
386
  pass
455
387
 
456
- def set_args(self, args: Optional[dict]) -> None:
388
+ def set_args(self, args: Any) -> None:
457
389
  _set_args(self, args)
458
390
 
459
- def push(self, page: Union[str, Any], args: Optional[dict] = None) -> None:
391
+ def _get_nav_args(self) -> Dict[str, Any]:
392
+ return self._args
393
+
394
+ def _push(self, page: Any, args: Optional[Dict[str, Any]] = None) -> None:
460
395
  raise RuntimeError("push() requires a native runtime (iOS or Android)")
461
396
 
462
- def pop(self) -> None:
397
+ def _pop(self) -> None:
463
398
  raise RuntimeError("pop() requires a native runtime (iOS or Android)")
464
399
 
465
400
  def _attach_root(self, native_view: Any) -> None:
@@ -467,3 +402,34 @@ else:
467
402
 
468
403
  def _detach_root(self, native_view: Any) -> None:
469
404
  pass
405
+
406
+
407
+ # ======================================================================
408
+ # Public factory
409
+ # ======================================================================
410
+
411
+
412
+ def create_page(
413
+ component_path: str,
414
+ native_instance: Any = None,
415
+ args_json: Optional[str] = None,
416
+ ) -> _AppHost:
417
+ """Create a page host for a function component.
418
+
419
+ Called by native templates (PageFragment.kt / ViewController.swift)
420
+ to bridge the native lifecycle to a ``@component`` function.
421
+
422
+ Parameters
423
+ ----------
424
+ component_path:
425
+ Dotted Python path to the component, e.g. ``"app.main_page.MainPage"``.
426
+ native_instance:
427
+ The native Activity (Android) or ViewController pointer (iOS).
428
+ args_json:
429
+ Optional JSON string of navigation arguments.
430
+ """
431
+ component_func = _import_component(component_path)
432
+ host = _AppHost(native_instance, component_func)
433
+ if args_json:
434
+ _set_args(host, args_json)
435
+ return host
pythonnative/style.py CHANGED
@@ -1,7 +1,8 @@
1
- """StyleSheet and theming support.
1
+ """StyleSheet, style resolution, and theming support.
2
2
 
3
3
  Provides a :class:`StyleSheet` helper for creating and composing
4
- reusable style dictionaries, plus a built-in theme context.
4
+ reusable style dictionaries, a :func:`resolve_style` utility for
5
+ flattening the ``style`` prop, and built-in theme contexts.
5
6
 
6
7
  Usage::
7
8
 
@@ -12,20 +13,39 @@ Usage::
12
13
  container={"padding": 16, "spacing": 12},
13
14
  )
14
15
 
15
- pn.Text("Hello", **styles["title"])
16
- pn.Column(..., **styles["container"])
16
+ pn.Text("Hello", style=styles["title"])
17
+ pn.Column(..., style=styles["container"])
17
18
  """
18
19
 
19
- from typing import Any, Dict
20
+ from typing import Any, Dict, List, Optional, Union
20
21
 
21
22
  from .hooks import Context, create_context
22
23
 
24
+ _StyleDict = Dict[str, Any]
25
+ StyleValue = Union[None, _StyleDict, List[Optional[_StyleDict]]]
26
+
27
+
28
+ def resolve_style(style: StyleValue) -> _StyleDict:
29
+ """Flatten a ``style`` prop into a single dict.
30
+
31
+ Accepts ``None``, a single dict, or a list of dicts (later entries
32
+ override earlier ones, mirroring React Native's array style pattern).
33
+ """
34
+ if style is None:
35
+ return {}
36
+ if isinstance(style, dict):
37
+ return dict(style)
38
+ result: _StyleDict = {}
39
+ for entry in style:
40
+ if entry:
41
+ result.update(entry)
42
+ return result
43
+
44
+
23
45
  # ======================================================================
24
46
  # StyleSheet
25
47
  # ======================================================================
26
48
 
27
- _StyleDict = Dict[str, Any]
28
-
29
49
 
30
50
  class StyleSheet:
31
51
  """Utility for creating and composing style dictionaries."""
@@ -25,15 +25,8 @@ class PageFragment : Fragment() {
25
25
  val py = Python.getInstance()
26
26
  val pagePath = arguments?.getString("page_path") ?: "app.main_page.MainPage"
27
27
  val argsJson = arguments?.getString("args_json")
28
- val moduleName = pagePath.substringBeforeLast('.')
29
- val className = pagePath.substringAfterLast('.')
30
- val pyModule = py.getModule(moduleName)
31
- val pageClass = pyModule.get(className)
32
- // Pass the hosting Activity as native_instance for context
33
- page = pageClass?.call(requireActivity())
34
- if (!argsJson.isNullOrEmpty()) {
35
- page?.callAttr("set_args", argsJson)
36
- }
28
+ val pnPage = py.getModule("pythonnative.page")
29
+ page = pnPage.callAttr("create_page", pagePath, requireActivity(), argsJson)
37
30
  } catch (e: Exception) {
38
31
  Log.e(TAG, "Failed to instantiate page", e)
39
32
  }
@@ -85,28 +85,15 @@ class ViewController: UIViewController {
85
85
  // Determine which Python page to load
86
86
  let pagePath: String = requestedPagePath ?? "app.main_page.MainPage"
87
87
  do {
88
- let moduleName = String(pagePath.split(separator: ".").dropLast().joined(separator: "."))
89
- let className = String(pagePath.split(separator: ".").last ?? "MainPage")
90
- let pyModule = try Python.attemptImport(moduleName)
91
- // Resolve class by name via builtins.getattr to avoid subscripting issues
92
- let builtins = Python.import("builtins")
93
- let getattrFn = builtins.getattr
94
- let pageClass = try getattrFn.throwing.dynamicallyCall(withArguments: [pyModule, className])
95
- // Pass native pointer so Python Page can wrap via rubicon.objc
88
+ let pnPage = try Python.attemptImport("pythonnative.page")
96
89
  let ptr = Unmanaged.passUnretained(self).toOpaque()
97
90
  let addr = UInt(bitPattern: ptr)
98
- let page = try pageClass.throwing.dynamicallyCall(withArguments: [addr])
99
- // If args provided, pass into Page via set_args(dict)
100
- if let jsonStr = requestedPageArgsJSON {
101
- let json = Python.import("json")
102
- do {
103
- let args = try json.loads.throwing.dynamicallyCall(withArguments: [jsonStr])
104
- _ = try page.set_args.throwing.dynamicallyCall(withArguments: [args])
105
- } catch {
106
- NSLog("[PN] Failed to decode requestedPageArgsJSON: \(error)")
107
- }
108
- }
109
- // Call on_create immediately so Python can insert its root view
91
+ let argsJson: PythonObject = (requestedPageArgsJSON != nil)
92
+ ? PythonObject(requestedPageArgsJSON!)
93
+ : Python.None
94
+ let page = try pnPage.create_page.throwing.dynamicallyCall(
95
+ withArguments: [pagePath, addr, argsJson]
96
+ )
110
97
  _ = try page.on_create.throwing.dynamicallyCall(withArguments: [])
111
98
  return
112
99
  } catch {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pythonnative
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Cross-platform native UI toolkit for Android and iOS
5
5
  Author: Owen Carey
6
6
  License: MIT License
@@ -88,17 +88,18 @@ Dynamic: license-file
88
88
 
89
89
  ## Overview
90
90
 
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.
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 hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
92
92
 
93
93
  ## Features
94
94
 
95
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.
96
+ - **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern.
97
+ - **`style` prop:** Pass all visual and layout properties through a single `style` dict, composable via `StyleSheet`.
97
98
  - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation.
98
99
  - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
99
100
  - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
100
- - **Navigation:** Push and pop screens with argument passing for multi-page apps.
101
- - **Bundled templates:** Android Gradle and iOS Xcode templates are included scaffolding requires no network access.
101
+ - **Navigation:** Push and pop screens with argument passing via the `use_navigation()` hook.
102
+ - **Bundled templates:** Android Gradle and iOS Xcode templates are included, so scaffolding requires no network access.
102
103
 
103
104
  ## Quick Start
104
105
 
@@ -114,21 +115,17 @@ pip install pythonnative
114
115
  import pythonnative as pn
115
116
 
116
117
 
117
- class MainPage(pn.Page):
118
- def __init__(self, native_instance):
119
- super().__init__(native_instance)
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
- )
118
+ @pn.component
119
+ def MainPage():
120
+ count, set_count = pn.use_state(0)
121
+ return pn.Column(
122
+ pn.Text(f"Count: {count}", style={"font_size": 24}),
123
+ pn.Button(
124
+ "Tap me",
125
+ on_click=lambda: set_count(count + 1),
126
+ ),
127
+ style={"spacing": 12, "padding": 16},
128
+ )
132
129
  ```
133
130
 
134
131
  ## Documentation