Commit 4a477aa
authored
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
- .github
- scripts
- workflows
- client
- linux/flutter
- web
- windows/flutter
- packages/flet/lib/src
- controls
- protocol
- utils
- sdk/python
- examples
- apps
- declarative
- use_dialog_basic
- use_dialog_chained
- use_dialog_multiple
- dialogs/multi_host
- router
- active_links
- app_drawer
- auth_dialog
- auth_page
- basic
- dynamic_segments
- featured_views
- featured
- index_routes
- layout_outlet
- loaders
- nested_outlet_views
- nested_routes
- prefix_routes
- runtime_routes
- splats
- packages
- flet-ads/src/flutter/flet_ads
- flet-audio-recorder/src/flutter/flet_audio_recorder
- flet-audio/src/flutter/flet_audio
- flet-camera/src/flutter/flet_camera
- flet-charts/src/flutter/flet_charts
- flet-datatable2/src/flutter/flet_datatable2
- flet-map/src/flutter/flet_map
- flet
- integration_tests/examples
- apps
- extensions/code_editor/golden/macos/code_editor
- src/flet
- components
- hooks
- controls
- core
- material
- messaging
- tests
- templates/build/{{cookiecutter.out_dir}}/web
- website
- docs
- controls
- cookbook
- publish
- types
- src/components/crocodocs
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
25 | | - | |
26 | | - | |
27 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
125 | | - | |
126 | | - | |
127 | | - | |
128 | 125 | | |
129 | 126 | | |
130 | 127 | | |
| |||
282 | 279 | | |
283 | 280 | | |
284 | 281 | | |
285 | | - | |
| 282 | + | |
286 | 283 | | |
287 | 284 | | |
288 | 285 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
30 | | - | |
31 | | - | |
32 | | - | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
69 | | - | |
| 69 | + | |
70 | 70 | | |
71 | 71 | | |
72 | 72 | | |
| |||
88 | 88 | | |
89 | 89 | | |
90 | 90 | | |
91 | | - | |
| 91 | + | |
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| |||
109 | 109 | | |
110 | 110 | | |
111 | 111 | | |
112 | | - | |
| 112 | + | |
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| |||
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
127 | | - | |
| 127 | + | |
128 | 128 | | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
133 | 133 | | |
134 | | - | |
| 134 | + | |
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
| |||
146 | 146 | | |
147 | 147 | | |
148 | 148 | | |
149 | | - | |
| 149 | + | |
150 | 150 | | |
151 | 151 | | |
152 | 152 | | |
| |||
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
222 | | - | |
| 222 | + | |
223 | 223 | | |
224 | 224 | | |
225 | 225 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
0 commit comments