Skip to content

Commit 4a477aa

Browse files
Add declarative Router with view-stack navigation (#6406)
* Add sync navigate wrapper for Page.push_route Introduce Page.navigate(route, **kwargs) as a synchronous convenience wrapper that schedules an async push_route via asyncio.create_task. Useful for synchronous callbacks (e.g. on_click) where awaiting is not possible; forwards route and query parameters to push_route. * Initial router implementation * Use modal AlertDialog for auth guard, fix routing Replace the inline Stack/Card login overlay with a modal AlertDialog using page.show_dialog/pop_dialog in AuthGuard. Add page context, a show_login helper, and a use_effect to open/close the dialog based on auth state. In auth_page, show a ProgressRing when auth is not yet available in Dashboard, and move navigation to /login into a use_effect in ProtectedRoute (also guard against auth being None) to avoid performing navigation as a render side effect. * Add typing hints for router, page, and contexts Export ContextProvider from use_context and add type annotations across router, page, and example files to improve typing/IDE support. Examples (auth_dialog, auth_page, featured) now declare AuthContext with ft.ContextProvider[AuthState | None]. Router typings updated (use_route_outlet and Router now return Control) and TYPE_CHECKING is used to avoid runtime imports. Page.render and Page.render_views signatures were simplified to accept Callable[..., Any]. * Reorganize router examples; add pyproject files Move router example scripts into per-example subfolders (main.py) and add corresponding pyproject.toml metadata for gallery packaging. Wrap example App components with SafeArea and replace unconditional ft.run calls with an if __name__ == "__main__" guard. Remove the old top-level example files and update docs (controls and cookbook) to reference the new example paths and updated navigation API usage. * feat: Add 'visible' attribute to Component * Add manage_views mode to Router for view-stack navigation Router with manage_views=True returns a list of Views (one per path level) enabling swipe-back gestures, system back button, and AppBar implicit back button on mobile. Used with page.render_views(). - Add manage_views parameter to Router - Add outlet flag to Route for shared layouts across child views - Add _split_chain_into_view_levels and _build_view_level helpers - Handle on_view_pop for back navigation - Allow pathless routes with children to match as standalone views - Update nested_routes example to use manage_views - Add nested_outlet_views example with shared layout * Add featured_views example; add READMEs to router examples - Add featured_views example with NavigationRail, Projects (stacked views with back navigation), and Settings (tabbed outlet layout) - Move example descriptions from module docstrings to README.md files for nested_routes, nested_outlet_views, and featured_views * Update router docs to docusaurus/crocodocs format - Convert Python docstrings to reST cross-reference roles - Convert controls/router.md to JSX with ClassSummary, ClassMembers, CodeExample components; add managed views examples - Convert 7 type docs from jinja stubs to JSX with ClassAll - Rewrite cookbook/router.md with short inline snippets, links to full examples, and new sections for manage_views, outlet=True, and NavigationRail patterns - Update navigation-and-routing.md to mention manage_views mode * Regenerate sidebars.js; update pubspec.lock transitive deps * Add Router changelog entry for 0.85.0 * Add Router entries to sidebars.yml * fix: update link to NavigationRail documentation in router.md * Add use_view_path hook and fix LocationInfo.pathname LocationInfo.pathname now holds the actual URL instead of the route template, so is_route_active() and use_route_location() work consistently with dynamic segments in manage_views mode. Add use_view_path() hook that returns the per-view resolved URL (unique per view level), needed for Flutter Navigator keying when a layout wraps multiple child views. Update nested_outlet_views example to use it for View.route. _RouteMatch gains resolved_path populated from regex m.group(0). * Add app_drawer router example Demonstrates a NavigationDrawer with deep-linkable tabs driven by nested routes. Drawer open/close state and selected tab are synced to the URL: /apps/:app_id opens the detail view, and /apps/:app_id/settings/{general,permissions} opens the drawer on the matching tab. Uses outlet=True on the :app_id route, use_route_outlet() to detect drawer state, and show_end_drawer/close_end_drawer in a use_effect to sync the drawer with the URL. * Add integration tests for router examples; fix example layouts New test_router.py covers all 16 router examples (basic, active_links, index_routes, layout_outlet, prefix_routes, dynamic_segments, splats, loaders, runtime_routes, auth_page, auth_dialog, featured, nested_routes, nested_outlet_views, featured_views, app_drawer) following the pattern of test_routing_navigation.py. Fixes to make previously-failing examples render correctly in the Flutter test viewport: - layout_outlet: AppBar was used as a regular Column child (valid only as View.appbar); replaced with a styled Container+Row header. Also removed expand=True on outer Column. - featured: AppLayout used a Row with SPACE_BETWEEN wrapping nested Rows, which Flutter couldn't lay out in bounded viewports. Flattened to a single Row and removed expand=True. LoginPage no longer wraps its Column in an aligned Container. * chore: refresh pubspec.lock transitive deps * Document use_view_path() hook - New types/use_view_path.md auto-generated from the Python docstring - Cookbook: add the hook to the reference table; fix the Shared Layouts outlet example to use use_view_path() for View.route (was previously using use_route_location() which collides when a layout wraps multiple child views). Add a note explaining when to pick each hook. - Sidebar: add use_view_path entry under Methods * chore: regenerate code_editor golden images * Fix Component lifecycle bugs: stale subscriptions, missed mount effects, zombie re-renders Three related bugs in the component reconciliation and effect scheduler: 1. _compare_values: when a scalar dataclass field (e.g. Container.content) swapped between two Components with different `fn`, the diff treated them as in-place updates (same `type()`), copying `_i` via `_migrate_state`. The session then skipped `did_mount` on the new component (same `_i` as the removed one), so `use_effect` mount effects never fired. Added the same `fn`-compatibility check that `_compare_lists` already had. 2. schedule_effect: stored `weakref.ref(hook)` in pending effects, but `_run_unmount_effects` cleared `_state.hooks` synchronously after scheduling. The hook was GC'd before the scheduler could run cleanup. Changed to strong ref so cleanup effects actually execute. 3. update() / _schedule_update(): no guard against unmounted state. Stale observable subscriptions firing after `will_unmount` would re-render zombie components, creating leaked render trees and duplicate dialogs. Added `_state.mounted` checks to both methods. * Fix use_dialog to support multiple sibling hosts When two components each drive their own dialog via use_dialog (e.g. an AlertDialog host and a SnackBar host mounted side-by-side), the previous implementation had several ordering/identity bugs that caused dialogs to land on the wrong host, dismiss animations to be cut short, or the newly shown dialog to never call showDialog on Flutter. - Identity-based dialog lookup. `prev in page._dialogs.controls` relied on dataclass `==`, which matches two similarly-shaped dialogs. Use an `is`-based scan so each host finds its own entry. - Preserve mounted dialog instance on re-render. Keep `prev` alive so Flutter's route/widget state persists while the dialog is open and copy the freshly rendered fields onto it. The previous code swapped the instance each render, which destroyed TextField cursor state and racy dismiss animations. - Fresh `_i` on show-after-dismiss. If a previous dialog is mid-dismiss (open=False but still in the list), append a new entry with a fresh `_i` instead of reusing the old one — Flutter's AlertDialogControl guards against `open == lastOpen` and would silently drop the show. - Batch show/dismiss through `_dialogs.update()`. Routing all dialog mutations through a single scheduled update per tick prevents a sibling host's show from racing another host's dismiss animation. - Recover live controls after stale-index lookups in `dispatch_event` so clicks on freshly-mounted dialog children reach the current control instead of a detached one. Includes dart-side alert_dialog/view changes, a multi_host example, and extensive tests covering the sibling-host scenarios. * Fix declarative dialog regressions and honor `key` on single-child fields - use_dialog: restore frozen-diff instance-swap on re-render so `_migrate_state` runs on nested controls. Preserves Flutter widget identity for children of an open dialog (TextField cursor/focus, dialog route) which the prior merge-into-prev approach destroyed. - use_dialog: patch the dialog directly on dismiss instead of `schedule_update(_dialogs)` — the latter diffs against a stale `_dialogs.__prev_lists` snapshot after the instance swap and drops the `open=False` op entirely. - use_dialog: keep `_dialogs.__prev_lists['controls']` aligned when swapping the instance in place. Otherwise a later `_dialogs.update()` from an unrelated caller (e.g. `page.show_dialog(SnackBar)` from a toast) sees different `id()` at the same position and emits a full REPLACE of the dialog — on Dart that builds a second DialogRoute on top of the first and the dialog can no longer be closed. - alert_dialog.dart: prefer animated `Navigator.pop()` when the route is topmost; keep `removeRoute` as fallback only when a sibling `use_dialog` host has stacked a newer dialog above ours. Restores the closing animation lost in the sibling-host fix. - object_patch: `_compare_values` now respects `key` on single-child dataclass fields — a changed key forces remount (remove + add), matching keyed list reconciliation. Without this, `ft.Container(content=ft.FletApp(key=str(reload_key)))` silently ignored key flips, so state-driven widget remounts did nothing. Tests: updated `test_use_dialog.py` assertions to the restored semantics (frozen-diff, direct-patch dismiss, no full REPLACE after instance swap) and added `test_component_diff.py` coverage for the key-driven remount path on single-child fields. * Convert use_dialog examples to projects and add integration tests - Promote `use_dialog_basic.py`, `use_dialog_chained.py`, and `use_dialog_multiple.py` from single-file snippets to Flet example projects under `sdk/python/examples/apps/declarative/<name>/` with `main.py` and `pyproject.toml`. Git tracks each as a rename so log history follows through. - Extract each example's module-level `ft.run(...)` into a `main(page)` wrapper guarded by `if __name__ == "__main__"` — same pattern as the other declarative projects — so the modules are importable without side effects. - Add `integration_tests/examples/apps/test_use_dialog.py` covering: - basic: open, cancel, confirm → mid-delete "Deleting, please wait..." message, then dismissal after the 2s async. - chained: confirm → async delete → success dialog appears via `on_dismiss` chaining → OK closes → status flips to "File deleted." - multiple: 3 file rows, per-row Rename and Delete dialogs (open, cancel), confirmed delete actually removes the targeted row after the simulated async. * Detect git worktrees in find_repo_root In a worktree, `.git` is a regular file (`gitdir: <main-repo>/.git/worktrees/<name>`) rather than a directory, so `(path / ".git").is_dir()` returned False and find_repo_root walked past the worktree root. That left `from_git()` and the `.fvmrc` lookup in `get_flutter_version()` falling back to defaults, so `flet_version` was stuck at `"0.1.0"` and `flutter_version` at `"0"` for anyone developing Flet from a worktree. Switch to `.exists()` so both clone layouts are accepted; git itself handles the gitdir indirection from there. Add regression tests covering the regular-clone, worktree-file, and not-in-a-repo cases. * Restore synchronous Navigator.pop() in AlertDialog dismiss path The earlier sibling-host fix moved dismissal into a post-frame `Navigator.removeRoute(route)`. That raced with `ViewControl`'s own post-frame `Navigator.pop(context, true)` in the on_confirm_pop flow: both callbacks fired in the same tick, the view-pop callback landed on the dismissing dialog instead of its target view, and `test_pop_view_confirm` regressed. Restore the synchronous `Navigator.of(context).pop()` during build — Flutter schedules it for end-of-frame internally, which lands before any sibling post-frame callbacks, so the confirm-pop handler targets the right route. Keep the StatefulWidget + `_dialogRoute` tracking added by the sibling-host fix so the active-route check still guards the pop. * chore: Update to Flutter 3.41.7 and update dependencies across multiple packages * fix: update macOS runner version to macos-26 in CI workflows * fix: downgrade package_info_plus dependency to ^9.0.0 * fix(docs): sanitize reST xrefs in deprecation tooltips HTML `title=` attributes don't render markdown or reST, so `docs_reason` strings containing `:class:`~flet.Foo`` leaked raw role markup into the built `deprecated` badge tooltip — tripping the CI xref check. - Add `plainifyForTooltip()` in crocodocs utils to flatten reST xrefs and inline backticks for plain-text tooltip use. - Apply it to the Badge `title` in ClassBlock. - Normalize `docs_reason` strings to reST style across drag_target.py and the deprecation tests (previously a mix of mkdocs-style `[text][target]` links and reST). - Fold `check_docs` into `yarn build` via a new `docs:check` script so local builds match CI; drop the redundant check step from ci.yml, docs.yml, and build_docs.sh. * Add width prop to NavigationDrawer * fix: service (un)registration no longer suppresses auto-update ServiceRegistry.register_service and unregister_services called self.update(), which flipped the handler's update-called flag and caused auto-update at the end of the event handler to be skipped — breaking services like FilePicker when constructed inside a handler. Route these internal updates through a private helper that preserves the prior flag state. * Pyodide bootstrap: zip/tar.gz app package + pyproject.toml deps * Give each FletApp a unique backend-channel address Embedded FletApps (Preview's inner Pyodide app) all default to an empty `url`, so their backend channel address collapses to "" and multiple instances collide in the JS-side `_apps` registry used by jsConnect/jsSend/jsDisconnect. When one FletApp disposed, jsDisconnect would terminate whichever worker was last registered under "" — which is a *different* FletApp's worker. Synthesize a stable per-instance address from the control id when `url` is empty. The factory still picks the Pyodide/JavaScript channel under forcePyodide, so there's no routing impact. * fix(docs): drop deprecated-badge tooltip instead of sanitizing it Removing the `title=` tooltip on the deprecated badge avoids the reST markup leak entirely, so the `plainifyForTooltip()` helper added in ce02c8a is no longer needed. This mirrors the approach taken in PR #6421 (commit 2a794af). - Revert drag_target.py docs_reason strings to mkdocs-style links. - Drop `title` from the SummarySection Badge and stop carrying `deprecation` through `memberSummary`. - Remove the now-unused `plainifyForTooltip` from crocodocs utils. * fix(docs): update deprecated documentation links for DragTargetEvent coordinates * feat(FletApp): assets_dir prop for embedded apps Embedded FletApp had no way to point its inner Flet app at an asset location — flet_app_control hardcoded assetsDir to "" and the web asset resolver ignored it anyway, so Image/Lottie/Markdown src values fell back to relative-to-document URLs and 404'd whenever the outer page was deeper than the embedded app's logical root. Add assets_dir to the Python FletApp control, plumb it through flet_app_control.dart into the existing FletBackend.assetsDir field (used by getAssetSource and Markdown), and teach images_web.dart to use assetsDir as a URL prefix when non-empty. Empty assetsDir keeps the old relative-resolution behaviour, so existing apps are unchanged. * gallery: curated catalog at sdk/python/examples/gallery.yaml Seed catalog covering every category currently used by an example. Top-level `categories:` is a map; entries are null (no metadata, no children) or maps where `_`-prefixed keys carry catalog metadata (`_icon`, `_description`) and any other keys are child categories with the same recursive shape. Insertion order = display order, so the file is the single place to curate ordering, naming, and metadata. The flet-app server scanner reads this to build its category tree and drops examples whose pyproject paths don't appear here. * feat(FletApp): on_python_output event for embedded Pyodide stdout/stderr Pyodide stdout/stderr is captured in the worker via loadPyodide({stdout, stderr}) and shipped to the host page as a new control event, so app shells can render embedded Python output instead of losing it to the browser console. - Wire format: MessageAction.pythonOutput(7) on both sides, payload {text, is_stderr}. - Worker bootstrap encodes the frame in Python with msgpack.packb (no hand-rolled wire format on the JS side). The shim is registered after user-app deps install — when msgpack isn't available the redirect silently no-ops and stdout falls through to the dev console. - userAppRunning flag gates the hook so bootstrap diagnostics (archive download, micropip logs) don't pollute the host's console pane. - Inner FletApp bubbles via _parentFletBackend.triggerControlEventById, same shape as errorsHandler; root FletApp falls back to `print` since the cookiecutter main.dart silences debugPrint in release builds. * worker: surface "no Flet UI" via __flet_no_ui__ sentinel When the user script finishes without calling ft.run(...) the worker posts a sentinel-prefixed message instead of letting the bare TypeError on `start_connection` propagate. Host pages can detect the prefix and render a friendly hint plus the captured prints rather than a Script error panel. * worker: treat clean SystemExit as normal script termination exit() / exit(0) used to surface a Pyodide traceback as a Script error. Wrap runpy.run_module in a SystemExit guard: code 0 / None falls through to the no-UI path so the host renders the friendly "script finished" pane with whatever prints landed before the exit. Non-zero exit codes still propagate as errors.
1 parent 055cd09 commit 4a477aa

114 files changed

Lines changed: 6586 additions & 230 deletions

File tree

Some content is hidden

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

.fvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"flutter": "3.41.4"
2+
"flutter": "3.41.7"
33
}

.github/scripts/build_docs.sh

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ for pkg in packages/*; do
1717
done
1818
cd -
1919

20-
# build website
20+
# build website (includes verification checks via `yarn build`)
2121
cd website
2222
yarn install
2323
yarn build
2424
cd -
25-
26-
# run verification checks
27-
bash .github/scripts/check_docs.sh website/build

.github/workflows/ci.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,6 @@ jobs:
122122
yarn install
123123
yarn build
124124
125-
- name: Check for broken images and unresolved xrefs
126-
run: bash .github/scripts/check_docs.sh website/build
127-
128125
# ===========================
129126
# Build Flet Flutter package
130127
# ===========================
@@ -282,7 +279,7 @@ jobs:
282279
# ===========================
283280
build_macos:
284281
name: Build Flet Client for macOS
285-
runs-on: macos-15
282+
runs-on: macos-26
286283
needs:
287284
- python_tests
288285
- build_flet_package

.github/workflows/docs.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,3 @@ jobs:
2727
cd website
2828
yarn install
2929
yarn build
30-
31-
- name: Check for broken images and unresolved xrefs
32-
run: bash .github/scripts/check_docs.sh website/build

.github/workflows/flet-build-test.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ jobs:
6666
needs_linux_deps: true
6767

6868
- name: macos
69-
runner: macos-latest
69+
runner: macos-26
7070
build_cmd: "flet build macos"
7171
artifact_name: macos-build-artifact
7272
artifact_path: build/macos
@@ -88,7 +88,7 @@ jobs:
8888
needs_linux_deps: false
8989

9090
- name: aab-macos
91-
runner: macos-latest
91+
runner: macos-26
9292
build_cmd: "flet build aab"
9393
artifact_name: aab-build-macos-artifact
9494
artifact_path: build/aab
@@ -109,7 +109,7 @@ jobs:
109109
needs_linux_deps: false
110110

111111
- name: apk-macos
112-
runner: macos-latest
112+
runner: macos-26
113113
build_cmd: "flet build apk"
114114
artifact_name: apk-build-macos-artifact
115115
artifact_path: build/apk
@@ -124,14 +124,14 @@ jobs:
124124

125125
# -------- iOS --------
126126
- name: ipa
127-
runner: macos-latest
127+
runner: macos-26
128128
build_cmd: "flet build ipa"
129129
artifact_name: ipa-build-artifact
130130
artifact_path: build/ipa
131131
needs_linux_deps: false
132132

133133
- name: ios-simulator
134-
runner: macos-latest
134+
runner: macos-26
135135
build_cmd: "flet build ios-simulator"
136136
artifact_name: ios-simulator-build-artifact
137137
artifact_path: build/ios-simulator
@@ -146,7 +146,7 @@ jobs:
146146
needs_linux_deps: false
147147

148148
- name: web-macos
149-
runner: macos-latest
149+
runner: macos-26
150150
build_cmd: "flet build web"
151151
artifact_name: web-build-macos-artifact
152152
artifact_path: build/web
@@ -219,7 +219,7 @@ jobs:
219219
runner: ubuntu-latest
220220

221221
- name: macos
222-
runner: macos-latest
222+
runner: macos-26
223223

224224
- name: windows
225225
runner: windows-latest

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### New features
44

5+
* Add declarative `ft.Router` component for `@ft.component` apps with nested routes, layout routes with outlets, dynamic segments, optional segments, splats, custom regex constraints, data loaders, active link detection, authentication patterns, and `manage_views=True` mode for view-stack navigation with swipe-back gestures and `AppBar` back button on mobile ([#6406](https://github.com/flet-dev/flet/pull/6406)) by @FeodorFitsner.
56
* Add `ft.use_dialog()` hook for declarative dialog management from within `@ft.component` functions, with frozen-diff reactive updates and automatic open/close lifecycle ([#6335](https://github.com/flet-dev/flet/pull/6335)) by @FeodorFitsner.
67
* Add `scrollable`, `pin_leading_to_top`, and `pin_trailing_to_bottom` properties to `NavigationRail` for scrollable content with optional pinned leading/trailing controls ([#1923](https://github.com/flet-dev/flet/issues/1923), [#6356](https://github.com/flet-dev/flet/pull/6356)) by @ndonkoHenri.
78
* Add scroll support to `ResponsiveRow` for responsive layouts whose content exceeds the available height ([#2590](https://github.com/flet-dev/flet/issues/2590), [#6417](https://github.com/flet-dev/flet/pull/6417)) by @ndonkoHenri.

client/linux/flutter/generated_plugins.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
1717
)
1818

1919
list(APPEND FLUTTER_FFI_PLUGIN_LIST
20+
jni
2021
)
2122

2223
set(PLUGIN_BUNDLED_LIBRARIES)

0 commit comments

Comments
 (0)