Skip to content

Add declarative Router with view-stack navigation#6406

Open
FeodorFitsner wants to merge 27 commits intorelease/v0.85.0from
router-3
Open

Add declarative Router with view-stack navigation#6406
FeodorFitsner wants to merge 27 commits intorelease/v0.85.0from
router-3

Conversation

@FeodorFitsner
Copy link
Copy Markdown
Contributor

@FeodorFitsner FeodorFitsner commented Apr 10, 2026

Summary

  • Add ft.Router declarative routing component for @ft.component apps with nested routes, layout routes (outlets), dynamic segments, optional segments, splats, custom regex, and data loaders
  • Add hooks: use_route_params, use_route_location, use_route_outlet, use_route_loader_data, is_route_active
  • Add manage_views=True mode for view-stack navigation — enables swipe-back gestures, system back button, and AppBar implicit back button on mobile
  • Add outlet=True flag on Route for shared layouts wrapping child views
  • Add Page.navigate() sync wrapper for Page.push_route() from synchronous callbacks
  • Add visible attribute to Component so functional components can be used as content of controls like SafeArea that require visible content
  • Convert all router docs to docusaurus/crocodocs format with reST cross-references in docstrings
  • Add 14 router example apps including `nested_routes`, `nested_outlet_views`, and `featured_views` (full app with NavigationRail)

Test plan

  • Run `sdk/python/examples/apps/router/basic` — basic flat routing
  • Run `sdk/python/examples/apps/router/nested_routes` — verify back button and swipe-back work between nested route levels
  • Run `sdk/python/examples/apps/router/nested_outlet_views` — verify shared layout wraps each child view
  • Run `sdk/python/examples/apps/router/featured_views` — verify NavigationRail with mixed stacked + tabbed navigation, no transition animation between top-level pages
  • Verify all existing router examples (`active_links`, `auth_dialog`, `auth_page`, `dynamic_segments`, `featured`, `index_routes`, `layout_outlet`, `loaders`, `prefix_routes`, `runtime_routes`, `splats`) still work
  • Verify docs build: `cd website && yarn build`

Summary by Sourcery

Introduce a declarative Router system for component-based Flet apps, including view-stack navigation support, new routing hooks, and updated docs and examples.

New Features:

  • Add Router, Route, LocationInfo, and related hooks for declarative, component-based routing with nested routes, loaders, and active-link detection.
  • Add manage_views support to Router for building stacked View-based navigation with back gestures and AppBar back buttons.
  • Expose a synchronous Page.navigate() helper for routing from non-async callbacks.
  • Allow components to control visibility via a new visible attribute for use inside container-like controls.

Enhancements:

  • Relax Page.render and render_views component type hints to accept any callable, improving flexibility for routed component trees.

Documentation:

  • Document the new Router control, routing hooks, and related types, and add a dedicated Router cookbook section.
  • Register Router and new routing types in the Docusaurus sidebar navigation.
  • Add README and metadata for new router example applications.

Tests:

  • Add a suite of router example apps demonstrating basic routing, nested and prefix routes, loaders, auth guards, dynamic segments, splats, and managed view stacks.

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.
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.
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].
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.
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 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
- 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
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 10, 2026

Deploying flet-website-v2 with  Cloudflare Pages  Cloudflare Pages

Latest commit: a3e2c2e
Status: ✅  Deploy successful!
Preview URL: https://380f9324.flet-website-v2.pages.dev
Branch Preview URL: https://router-3.flet-website-v2.pages.dev

View logs

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've reviewed this pull request using the Sourcery rules engine

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Apr 10, 2026

Deploying flet-examples with  Cloudflare Pages  Cloudflare Pages

Latest commit: a3e2c2e
Status: ✅  Deploy successful!
Preview URL: https://f0f35358.flet-examples.pages.dev
Branch Preview URL: https://router-3.flet-examples.pages.dev

View logs

# Conflicts:
#	sdk/python/packages/flet/src/flet/controls/page.py
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).
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.
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.
- 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
…ts, 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.
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.
…elds

- 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.
- 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.
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant