| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -36,7 +36,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app | |||
| 36 | 36 | - **Hooks and function components:** Manage state with `use_state`, side effects with `use_effect`, and navigation with `use_navigation`, all through one consistent pattern. | |
| 37 | 37 | - **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`. | |
| 38 | 38 | - **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. | |
| 39 | - - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. | ||
| 39 | + - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. State updates re-render **locally** — only the component whose state changed (and its subtree) re-runs, and unchanged leaves reuse cached intrinsic measurements — so deep UIs stay responsive instead of re-rendering the whole app from the root on every tap. | ||
| 40 | 40 | - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge. | |
| 41 | 41 | - **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. | |
| 42 | 42 | - **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,9 +19,14 @@ platform APIs synchronously from Python. | |||
| 19 | 19 | 3. **Reconciler.** On first render, the | |
| 20 | 20 | [`Reconciler`][pythonnative.reconciler.Reconciler] walks the tree | |
| 21 | 21 | and creates real native views via the platform backend. On | |
| 22 | - subsequent renders (triggered by hook state changes), it diffs the | ||
| 23 | - new tree against the previous one and applies the minimal set of | ||
| 24 | - native mutations. | ||
| 22 | + subsequent renders it diffs against the previous tree and applies | ||
| 23 | + the minimal set of native mutations. State-driven renders are | ||
| 24 | + **local**: a setter marks only its own component subtree dirty, and | ||
| 25 | + [`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty] | ||
| 26 | + re-runs just those components instead of the whole app from the | ||
| 27 | + root (the full tree is only rebuilt on mount, navigation, and hot | ||
| 28 | + reload). Sibling and ancestor components whose state did not change | ||
| 29 | + are left untouched. | ||
| 25 | 30 | 4. **Post-render effects.** Effects queued via | |
| 26 | 31 | [`use_effect`][pythonnative.use_effect] are flushed **after** the | |
| 27 | 32 | reconciler commits native mutations, matching React semantics. | |
@@ -78,15 +83,29 @@ and a new one is created. | |||
| 78 | 83 | ||
| 79 | 84 | ### Render lifecycle | |
| 80 | 85 | ||
| 81 | - 1. **Render phase**: component functions execute. Hooks record state | ||
| 82 | - reads, queue effects, and register memos. No native mutations | ||
| 83 | - happen yet. | ||
| 84 | - 2. **Commit phase**: the reconciler applies the diff to native views, | ||
| 85 | - creating, updating, and removing views as needed. | ||
| 86 | - 3. **Effect phase**: pending effects are flushed in depth-first order | ||
| 86 | + 1. **Schedule phase**: a `use_state` / `use_reducer` setter that | ||
| 87 | + actually changes a value marks its owning component dirty (adding | ||
| 88 | + the component's node to the reconciler's dirty set) and asks the | ||
| 89 | + screen host to schedule a flush. Several setters coalesce into one | ||
| 90 | + flush. | ||
| 91 | + 2. **Render phase**: only the dirty components execute, shallowest | ||
| 92 | + first so a dirty ancestor's re-render subsumes any dirty descendant. | ||
| 93 | + Each dirty component re-runs its body against its preserved hook | ||
| 94 | + state, with the context stack of every enclosing `Provider` restored | ||
| 95 | + so [`use_context`][pythonnative.use_context] still resolves. Hooks | ||
| 96 | + record state reads, queue effects, and register memos. No native | ||
| 97 | + mutations happen yet. | ||
| 98 | + 3. **Commit phase**: the reconciler applies the diff for each | ||
| 99 | + re-rendered subtree to native views, creating, updating, and | ||
| 100 | + removing views as needed, and bubbles any swapped subtree-root view | ||
| 101 | + up to its nearest native container. | ||
| 102 | + 4. **Layout phase**: a layout pass recomputes frames. Leaves whose | ||
| 103 | + `Element` is unchanged reuse a cached intrinsic measurement, so | ||
| 104 | + untouched subtrees skip native `measure_intrinsic` calls. | ||
| 105 | + 5. **Effect phase**: pending effects are flushed in depth-first order | ||
| 87 | 106 | (children before parents). Cleanup functions from the previous | |
| 88 | 107 | render run before new effect callbacks. | |
| 89 | - 4. **Drain phase**: if effects set state, a new render pass is | ||
| 108 | + 6. **Drain phase**: if effects set state, another flush is | ||
| 90 | 109 | automatically triggered and the cycle repeats (up to a safety | |
| 91 | 110 | limit to prevent infinite loops). | |
| 92 | 111 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,15 +15,22 @@ A render pass is triggered by: | |||
| 15 | 15 | - A navigation event (`navigate`, `go_back`, `replace`). | |
| 16 | 16 | - A hot-reload module swap (see [Hot reload guide](../guides/hot-reload.md)). | |
| 17 | 17 | ||
| 18 | + A `use_state` / `use_reducer` setter re-renders **locally**: only the | ||
| 19 | + component that owns the changed state (and the subtree it returns) is | ||
| 20 | + re-run, not the whole app. The full tree is rebuilt from the root only | ||
| 21 | + on initial mount, navigation, and hot reload. Sibling and ancestor | ||
| 22 | + components keep their existing native views and hook state untouched. | ||
| 23 | + | ||
| 18 | 24 | The phases: | |
| 19 | 25 | ||
| 20 | - 1. **Render**. Your `@component` function runs. Hooks register state, | ||
| 26 | + 1. **Render**. The affected `@component` function(s) run — for a state | ||
| 27 | + change, just the component whose setter fired. Hooks register state, | ||
| 21 | 28 | queue effects, and capture closures. No native widgets change yet, | |
| 22 | 29 | so this phase is cheap and pure (modulo `use_state` updates). | |
| 23 | 30 | 2. **Commit**. The | |
| 24 | - [`Reconciler`][pythonnative.reconciler.Reconciler] diffs the new | ||
| 25 | - tree against the previous one and applies the smallest set of | ||
| 26 | - native mutations through the registered | ||
| 31 | + [`Reconciler`][pythonnative.reconciler.Reconciler] diffs the | ||
| 32 | + re-rendered subtree against the previous one and applies the | ||
| 33 | + smallest set of native mutations through the registered | ||
| 27 | 34 | [`ViewHandler`][pythonnative.native_views.base.ViewHandler]s. | |
| 28 | 35 | 3. **Effects**. Cleanup callbacks from the *previous* render run | |
| 29 | 36 | first; new [`use_effect`][pythonnative.use_effect] callbacks run | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -76,6 +76,8 @@ class HookState: | |||
| 76 | 76 | "_trigger_render", | |
| 77 | 77 | "_pending_effects", | |
| 78 | 78 | "_dirty", | |
| 79 | + "_vnode", | ||
| 80 | + "_reconciler", | ||
| 79 | 81 | ) | |
| 80 | 82 | ||
| 81 | 83 | def __init__(self) -> None: | |
@@ -95,6 +97,13 @@ def __init__(self) -> None: | |||
| 95 | 97 | # knows that a memoized component still needs to re-render even | |
| 96 | 98 | # when its props didn't change. | |
| 97 | 99 | self._dirty: bool = False | |
| 100 | + # Back-references wired by the reconciler so a state setter can | ||
| 101 | + # mark *its own* component subtree dirty for a local re-render | ||
| 102 | + # (instead of forcing a whole-app re-render from the root). Both | ||
| 103 | + # stay ``None`` until the component is mounted, and are cleared | ||
| 104 | + # again when it unmounts. | ||
| 105 | + self._vnode: Any = None | ||
| 106 | + self._reconciler: Any = None | ||
| 98 | 107 | ||
| 99 | 108 | def reset_index(self) -> None: | |
| 100 | 109 | """Reset every per-hook cursor to ``0``. | |
@@ -182,6 +191,25 @@ def _schedule_trigger(trigger: Callable[[], None]) -> None: | |||
| 182 | 191 | trigger() | |
| 183 | 192 | ||
| 184 | 193 | ||
| 194 | + def _notify_state_changed(ctx: "HookState") -> None: | ||
| 195 | + """Mark ``ctx``'s component dirty and schedule a render after a state change. | ||
| 196 | + | ||
| 197 | + Enqueuing the owning ``VNode`` in the reconciler's dirty set is what | ||
| 198 | + makes the subsequent render *local*: the screen host's trigger calls | ||
| 199 | + ``flush_dirty``, which re-renders only the components marked here | ||
| 200 | + rather than the whole app. The dirty mark is eager (so several | ||
| 201 | + setters coalesce), while the render trigger respects | ||
| 202 | + [`batch_updates`][pythonnative.batch_updates]. | ||
| 203 | + """ | ||
| 204 | + ctx._dirty = True | ||
| 205 | + reconciler = ctx._reconciler | ||
| 206 | + vnode = ctx._vnode | ||
| 207 | + if reconciler is not None and vnode is not None: | ||
| 208 | + reconciler.mark_dirty(vnode) | ||
| 209 | + if ctx._trigger_render: | ||
| 210 | + _schedule_trigger(ctx._trigger_render) | ||
| 211 | + | ||
| 212 | + | ||
| 185 | 213 | @contextmanager | |
| 186 | 214 | def batch_updates() -> Generator[None, None, None]: | |
| 187 | 215 | """Coalesce multiple state updates into a single re-render. | |
@@ -272,9 +300,7 @@ def setter(new_value: Any) -> None: | |||
| 272 | 300 | new_value = new_value(ctx.states[idx]) | |
| 273 | 301 | if ctx.states[idx] is not new_value and ctx.states[idx] != new_value: | |
| 274 | 302 | ctx.states[idx] = new_value | |
| 275 | - ctx._dirty = True | ||
| 276 | - if ctx._trigger_render: | ||
| 277 | - _schedule_trigger(ctx._trigger_render) | ||
| 303 | + _notify_state_changed(ctx) | ||
| 278 | 304 | ||
| 279 | 305 | return current, setter | |
| 280 | 306 | ||
@@ -339,9 +365,7 @@ def dispatch(action: Any) -> None: | |||
| 339 | 365 | new_state = reducer(ctx.states[idx], action) | |
| 340 | 366 | if ctx.states[idx] is not new_state and ctx.states[idx] != new_state: | |
| 341 | 367 | ctx.states[idx] = new_state | |
| 342 | - ctx._dirty = True | ||
| 343 | - if ctx._trigger_render: | ||
| 344 | - _schedule_trigger(ctx._trigger_render) | ||
| 368 | + _notify_state_changed(ctx) | ||
| 345 | 369 | ||
| 346 | 370 | return current, dispatch | |
| 347 | 371 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments