| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -33,17 +33,20 @@ jobs: | |||
| 33 | 33 | # ~35th flow. Sharding the run into a few balanced groups — each on | |
| 34 | 34 | # its own freshly booted emulator — keeps every session well under | |
| 35 | 35 | # the limit, and parallelizes the Android pass as a bonus. Groups are | |
| 36 | - # sized so none exceeds ~25 flows; ``components`` is the largest | ||
| 37 | - # single category and runs alone. ``fail-fast: false`` so one shard's | ||
| 38 | - # failure still lets the others report. | ||
| 36 | + # sized so none exceeds ~15 flows; ``components`` (28 flows after the | ||
| 37 | + # gesture/animated additions) crossed the ceiling at ~flow 26, so it | ||
| 38 | + # runs as two halves. ``fail-fast: false`` so one shard's failure | ||
| 39 | + # still lets the others report. | ||
| 39 | 40 | strategy: | |
| 40 | 41 | fail-fast: false | |
| 41 | 42 | matrix: | |
| 42 | 43 | include: | |
| 43 | - - name: components | ||
| 44 | - suites: components | ||
| 45 | - - name: hooks-nav | ||
| 46 | - suites: hooks navigation | ||
| 44 | + - name: components-a | ||
| 45 | + suites: components-a | ||
| 46 | + - name: components-b | ||
| 47 | + suites: components-b | ||
| 48 | + - name: hooks-nav-gestures | ||
| 49 | + suites: hooks navigation gestures | ||
| 47 | 50 | - name: layout-styling-anim-misc | |
| 48 | 51 | suites: layout styling animations misc | |
| 49 | 52 | ||
@@ -85,6 +88,18 @@ jobs: | |||
| 85 | 88 | arch: x86_64 | |
| 86 | 89 | script: bash -lc "./scripts/run-e2e.sh android ${{ matrix.suites }}" | |
| 87 | 90 | ||
| 91 | + # Maestro writes per-flow debug output (command log, view-hierarchy | ||
| 92 | + # dumps, failure screenshots) under ~/.maestro/tests. Surface it when | ||
| 93 | + # a shard fails so CI-only failures are diagnosable without guesswork. | ||
| 94 | + - name: Upload Maestro debug output | ||
| 95 | + if: failure() | ||
| 96 | + uses: actions/upload-artifact@v4 | ||
| 97 | + with: | ||
| 98 | + name: maestro-debug-android-${{ matrix.name }} | ||
| 99 | + path: ~/.maestro/tests/ | ||
| 100 | + if-no-files-found: ignore | ||
| 101 | + retention-days: 7 | ||
| 102 | + | ||
| 88 | 103 | e2e-ios: | |
| 89 | 104 | needs: coverage | |
| 90 | 105 | runs-on: macos-latest | |
@@ -104,8 +119,8 @@ jobs: | |||
| 104 | 119 | include: | |
| 105 | 120 | - name: components | |
| 106 | 121 | suites: components | |
| 107 | - - name: hooks-nav | ||
| 108 | - suites: hooks navigation | ||
| 122 | + - name: hooks-nav-gestures | ||
| 123 | + suites: hooks navigation gestures | ||
| 109 | 124 | - name: layout-styling-anim-misc | |
| 110 | 125 | suites: layout styling animations misc | |
| 111 | 126 | ||
@@ -133,3 +148,15 @@ jobs: | |||
| 133 | 148 | run: ./scripts/run-e2e.sh ios ${{ matrix.suites }} | |
| 134 | 149 | env: | |
| 135 | 150 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| 151 | + | ||
| 152 | + # Maestro writes per-flow debug output (command log, view-hierarchy | ||
| 153 | + # dumps, failure screenshots) under ~/.maestro/tests. Surface it when | ||
| 154 | + # a shard fails so CI-only failures are diagnosable without guesswork. | ||
| 155 | + - name: Upload Maestro debug output | ||
| 156 | + if: failure() | ||
| 157 | + uses: actions/upload-artifact@v4 | ||
| 158 | + with: | ||
| 159 | + name: maestro-debug-ios-${{ matrix.name }} | ||
| 160 | + path: ~/.maestro/tests/ | ||
| 161 | + if-no-files-found: ignore | ||
| 162 | + retention-days: 7 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -104,9 +104,12 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc | |||
| 104 | 104 | - `cli` – CLI tool and `pn` command (`src/pythonnative/cli/`) | |
| 105 | 105 | - `components` – declarative element-creating functions (`components.py`) | |
| 106 | 106 | - `element` – Element descriptor class (`element.py`) | |
| 107 | + - `events` – tag-based event routing between native views and Python callbacks (`events.py`) | ||
| 108 | + - `gestures` – gesture descriptors and the pure-Python recognition arbiter (`gestures.py`) | ||
| 107 | 109 | - `hooks` – function components and hooks (`hooks.py`) | |
| 108 | 110 | - `hot_reload` – file watcher and module reloader (`hot_reload.py`) | |
| 109 | 111 | - `layout` – pure-Python flexbox engine (`layout.py`) | |
| 112 | + - `mutations` – batched mutation ops between reconciler and native backends (`mutations.py`) | ||
| 110 | 113 | - `native_modules` – native API modules for device capabilities (`native_modules/`) | |
| 111 | 114 | - `native_views` – platform-specific native view creation and updates (`native_views/`) | |
| 112 | 115 | - `navigation` – navigation containers and stack/tab/drawer navigators (`navigation.py`) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -36,7 +36,10 @@ 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. 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. | ||
| 39 | + - **Virtual view tree + reconciler:** Element trees are diffed and patched with minimal native mutations, similar to React's reconciliation. Each commit lands as **one batched transaction** of mutation ops, and event callbacks are routed through a tag-based registry so re-renders that only change closures cost zero native calls. 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 | + - **Native-driven animations:** The `Animated` API (timing / spring / decay, awaitable or fire-and-forget) hands animations to Core Animation and `ViewPropertyAnimator` whenever possible, so no Python code runs per frame; a pure-Python ticker covers the rest. | ||
| 41 | + - **Native gesture system:** Attach `Tap`, `LongPress`, `Pan`, `Swipe`, `Pinch`, and `Rotation` recognizers to any view via the `gestures=` prop — backed by `UIGestureRecognizer` on iOS and a unit-testable pure-Python arbiter on Android and desktop. | ||
| 42 | + - **Virtualized lists:** `FlatList` / `SectionList` window their rows in Python over the platform scroll view — uniform, exact, or measured variable heights, grids, headers/footers, infinite scroll, and an imperative scroll controller, identical on every platform. | ||
| 40 | 43 | - **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge. | |
| 41 | 44 | - **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 | 45 | - **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 | |
|---|---|---|---|
@@ -0,0 +1,22 @@ | |||
| 1 | + # Gestures | ||
| 2 | + | ||
| 3 | + Native-backed gesture recognition, attached to any view-like element | ||
| 4 | + via the `gestures=` prop. Descriptors are frozen dataclasses; their | ||
| 5 | + numeric configuration crosses the bridge while callbacks are routed | ||
| 6 | + through the tag-based event channel. See the | ||
| 7 | + [Gestures guide](../guides/gestures.md) for usage patterns. | ||
| 8 | + | ||
| 9 | + ::: pythonnative.gestures | ||
| 10 | + options: | ||
| 11 | + show_root_heading: false | ||
| 12 | + show_root_toc_entry: false | ||
| 13 | + members_order: source | ||
| 14 | + filters: ["!^_"] | ||
| 15 | + show_if_no_docstring: true | ||
| 16 | + | ||
| 17 | + ## See also | ||
| 18 | + | ||
| 19 | + - The [Gestures guide](../guides/gestures.md) walks through taps, | ||
| 20 | + drags, and gesture-driven animations. | ||
| 21 | + - [Animated](animated.md) pairs with `Pan` velocity for springs and | ||
| 22 | + decays. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,7 +1,10 @@ | |||
| 1 | 1 | # Native views | |
| 2 | 2 | ||
| 3 | 3 | The bridge between PythonNative's element tree and concrete native | |
| 4 | - widgets. Every element type maps to a | ||
| 4 | + widgets. Each commit's diff is expressed as a flat list of mutation | ||
| 5 | + ops referencing integer tags, applied through a single | ||
| 6 | + [`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations] | ||
| 7 | + call. Every element type maps to a | ||
| 5 | 8 | [`ViewHandler`][pythonnative.native_views.base.ViewHandler] | |
| 6 | 9 | implementation in the | |
| 7 | 10 | [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry]; | |
@@ -15,6 +18,24 @@ the platform-specific handlers are registered lazily so importing | |||
| 15 | 18 | members_order: source | |
| 16 | 19 | filters: ["!^_"] | |
| 17 | 20 | ||
| 21 | + ## Mutation ops | ||
| 22 | + | ||
| 23 | + ::: pythonnative.mutations | ||
| 24 | + options: | ||
| 25 | + show_root_heading: false | ||
| 26 | + show_root_toc_entry: false | ||
| 27 | + members_order: source | ||
| 28 | + filters: ["!^_"] | ||
| 29 | + | ||
| 30 | + ## Event routing | ||
| 31 | + | ||
| 32 | + ::: pythonnative.events | ||
| 33 | + options: | ||
| 34 | + show_root_heading: false | ||
| 35 | + show_root_toc_entry: false | ||
| 36 | + members_order: source | ||
| 37 | + filters: ["!^_"] | ||
| 38 | + | ||
| 18 | 39 | ## Base classes | |
| 19 | 40 | ||
| 20 | 41 | ::: pythonnative.native_views.base | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -19,8 +19,12 @@ 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 it diffs against the previous tree and applies | ||
| 23 | - the minimal set of native mutations. State-driven renders are | ||
| 22 | + subsequent renders it diffs against the previous tree and emits the | ||
| 23 | + minimal list of mutation ops (create / update / insert / remove / | ||
| 24 | + destroy / set-frame), applied as **one batched transaction** per | ||
| 25 | + commit through | ||
| 26 | + [`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]. | ||
| 27 | + State-driven renders are | ||
| 24 | 28 | **local**: a setter marks only its own component subtree dirty, and | |
| 25 | 29 | [`flush_dirty`][pythonnative.reconciler.Reconciler.flush_dirty] | |
| 26 | 30 | re-runs just those components instead of the whole app from the | |
@@ -95,11 +99,14 @@ and a new one is created. | |||
| 95 | 99 | so [`use_context`][pythonnative.use_context] still resolves. Hooks | |
| 96 | 100 | record state reads, queue effects, and register memos. No native | |
| 97 | 101 | 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 | ||
| 102 | + 3. **Commit phase**: the reconciler turns the diff for each | ||
| 103 | + re-rendered subtree into mutation ops referencing view tags, and | ||
| 104 | + flushes them all in a single `apply_mutations` transaction. Event | ||
| 105 | + callbacks are routed to the Python-side | ||
| 106 | + [`EventRegistry`][pythonnative.events.EventRegistry] instead of | ||
| 107 | + crossing the bridge, so callback-identity churn costs nothing. | ||
| 108 | + 4. **Layout phase**: a layout pass recomputes frames and emits | ||
| 109 | + `SetFrameOp`s only for frames that changed. Leaves whose | ||
| 103 | 110 | `Element` is unchanged reuse a cached intrinsic measurement, so | |
| 104 | 111 | untouched subtrees skip native `measure_intrinsic` calls. | |
| 105 | 112 | 5. **Effect phase**: pending effects are flushed in depth-first order | |
@@ -256,9 +263,18 @@ Each handler class maps an element type name (e.g., `"Text"`, | |||
| 256 | 263 | `"Button"`) to platform-native widget creation, property updates, and | |
| 257 | 264 | child management. The | |
| 258 | 265 | [`NativeViewRegistry`][pythonnative.native_views.NativeViewRegistry] | |
| 266 | + owns the tag-to-view table, applies each commit's mutation list, and | ||
| 259 | 267 | lazily imports only the relevant platform module at runtime, so the | |
| 260 | 268 | package can be imported on any platform for testing. | |
| 261 | 269 | ||
| 270 | + Native events flow back through a single channel: handlers wire their | ||
| 271 | + platform listeners once at view creation and call | ||
| 272 | + [`dispatch_event`][pythonnative.events.dispatch_event] with the view's | ||
| 273 | + tag; the [`EventRegistry`][pythonnative.events.EventRegistry] resolves | ||
| 274 | + the current Python callback. Gestures | ||
| 275 | + ([`pythonnative.gestures`](../api/gestures.md)) and natively-driven | ||
| 276 | + animations (the `Animated` API) ride the same tag infrastructure. | ||
| 277 | + | ||
| 262 | 278 | ## Comparisons | |
| 263 | 279 | ||
| 264 | 280 | !!! note "Versus React Native" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -84,9 +84,9 @@ pn.Column( | |||
| 84 | 84 | **Lists:** | |
| 85 | 85 | ||
| 86 | 86 | - [`FlatList(data, render_item, key_extractor, item_height, ...)`][pythonnative.FlatList]: | |
| 87 | - scrollable data list. Pass `item_height=` to enable native | ||
| 88 | - virtualization (`UITableView` / `RecyclerView`); rows are mounted | ||
| 89 | - lazily as they scroll into view. | ||
| 87 | + virtualized scrollable data list. Rows are mounted lazily as they | ||
| 88 | + scroll into view; pass `item_height=` (or `get_item_height=`) for | ||
| 89 | + exact extents, or let rows be measured on screen. | ||
| 90 | 90 | - [`SectionList(sections, render_item, render_section_header, item_height, ...)`][pythonnative.SectionList]: | |
| 91 | 91 | virtualized list with section headers. | |
| 92 | 92 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments