diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9501af990..138a25c99a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* 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.
* 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.
+* Add `Page.pop_views_until()` to pop multiple views and return a result to the destination view ([#6326](https://github.com/flet-dev/flet/issues/6326), [#6347](https://github.com/flet-dev/flet/pull/6347)) by @brunobrown.
### Improvements
diff --git a/client/pubspec.lock b/client/pubspec.lock
index f865fa37eb..6292be3ed7 100644
--- a/client/pubspec.lock
+++ b/client/pubspec.lock
@@ -157,10 +157,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
- version: "1.4.1"
+ version: "1.4.0"
charcode:
dependency: transitive
description:
@@ -359,7 +359,7 @@ packages:
path: "../packages/flet"
relative: true
source: path
- version: "0.85.0"
+ version: "0.82.2"
flet_ads:
dependency: "direct main"
description:
@@ -911,18 +911,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
- version: "0.13.0"
+ version: "0.11.1"
media_kit:
dependency: transitive
description:
@@ -1628,10 +1628,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
- version: "0.7.10"
+ version: "0.7.7"
torch_light:
dependency: transitive
description:
diff --git a/sdk/python/examples/apps/routing_navigation/pop_views_until/main.py b/sdk/python/examples/apps/routing_navigation/pop_views_until/main.py
new file mode 100644
index 0000000000..a4251c14e2
--- /dev/null
+++ b/sdk/python/examples/apps/routing_navigation/pop_views_until/main.py
@@ -0,0 +1,112 @@
+import asyncio
+
+import flet as ft
+
+
+def main(page: ft.Page):
+ page.title = "Routes Example"
+
+ result_text = ft.Text("No result yet", size=18)
+
+ def route_change():
+ page.views.clear()
+
+ # Home View (/)
+ page.views.append(
+ ft.View(
+ route="/",
+ controls=[
+ ft.AppBar(title=ft.Text("Home"), bgcolor=ft.Colors.SURFACE_BRIGHT),
+ result_text,
+ ft.Button(
+ "Start flow",
+ on_click=lambda _: asyncio.create_task(
+ page.push_route("/step1")
+ ),
+ ),
+ ],
+ )
+ )
+
+ if page.route == "/step1" or page.route == "/step2" or page.route == "/step3":
+ page.views.append(
+ ft.View(
+ route="/step1",
+ controls=[
+ ft.AppBar(
+ title=ft.Text("Step 1"),
+ bgcolor=ft.Colors.SURFACE_BRIGHT,
+ ),
+ ft.Text("Step 1 of the flow"),
+ ft.Button(
+ "Go to Step 2",
+ on_click=lambda _: asyncio.create_task(
+ page.push_route("/step2")
+ ),
+ ),
+ ],
+ )
+ )
+
+ if page.route == "/step2" or page.route == "/step3":
+ page.views.append(
+ ft.View(
+ route="/step2",
+ controls=[
+ ft.AppBar(
+ title=ft.Text("Step 2"),
+ bgcolor=ft.Colors.SURFACE_BRIGHT,
+ ),
+ ft.Text("Step 2 of the flow"),
+ ft.Button(
+ "Go to Step 3",
+ on_click=lambda _: asyncio.create_task(
+ page.push_route("/step3")
+ ),
+ ),
+ ],
+ )
+ )
+
+ if page.route == "/step3":
+ page.views.append(
+ ft.View(
+ route="/step3",
+ controls=[
+ ft.AppBar(
+ title=ft.Text("Step 3 (Final)"),
+ bgcolor=ft.Colors.SURFACE_BRIGHT,
+ ),
+ ft.Text("Flow complete!"),
+ ft.Button(
+ "Finish and go Home",
+ on_click=lambda _: asyncio.create_task(
+ page.pop_views_until("/", result="Flow completed!")
+ ),
+ ),
+ ],
+ )
+ )
+
+ page.update()
+
+ def on_pop_result(e: ft.ViewsPopUntilEvent):
+ result_text.value = f"Result: {e.result}"
+ page.show_dialog(ft.SnackBar(ft.Text(f"Got result: {e.result}")))
+ page.update()
+
+ async def view_pop(e: ft.ViewPopEvent):
+ if e.view is not None:
+ page.views.remove(e.view)
+ top_view = page.views[-1]
+ await page.push_route(top_view.route)
+
+ page.on_route_change = route_change
+ page.on_view_pop = view_pop
+ page.on_views_pop_until = on_pop_result
+
+ route_change()
+
+
+if __name__ == "__main__":
+ ft.run(main)
diff --git a/sdk/python/examples/apps/routing_navigation/pop_views_until/pyproject.toml b/sdk/python/examples/apps/routing_navigation/pop_views_until/pyproject.toml
new file mode 100644
index 0000000000..b665a71f29
--- /dev/null
+++ b/sdk/python/examples/apps/routing_navigation/pop_views_until/pyproject.toml
@@ -0,0 +1,26 @@
+[project]
+name = "apps-routing-navigation-pop-views-until"
+version = "1.0.0"
+description = "Pops multiple views from the navigation stack and returns a result to the destination view."
+requires-python = ">=3.10"
+keywords = ["apps", "routing", "navigation", "pop", "views", "result", "async"]
+authors = [{ name = "Flet team", email = "hello@flet.dev" }]
+dependencies = ["flet"]
+
+[dependency-groups]
+dev = ["flet-cli", "flet-desktop", "flet-web"]
+
+[tool.flet.gallery]
+categories = ["Apps/Navigation"]
+
+[tool.flet.metadata]
+title = "Pop views until"
+controls = ["View", "AppBar", "Button", "Text", "SnackBar"]
+layout_pattern = "multi-step-flow"
+complexity = "intermediate"
+features = ["routing", "view stack", "pop views until", "result passing", "async"]
+
+[tool.flet]
+org = "dev.flet"
+company = "Flet"
+copyright = "Copyright (C) 2023-2026 by Flet"
diff --git a/sdk/python/packages/flet/integration_tests/examples/apps/test_routing_navigation.py b/sdk/python/packages/flet/integration_tests/examples/apps/test_routing_navigation.py
index 8857ef302a..824d8394c7 100644
--- a/sdk/python/packages/flet/integration_tests/examples/apps/test_routing_navigation.py
+++ b/sdk/python/packages/flet/integration_tests/examples/apps/test_routing_navigation.py
@@ -6,6 +6,7 @@
from examples.apps.routing_navigation.home_store import main as home_store
from examples.apps.routing_navigation.initial_route import main as initial_route
from examples.apps.routing_navigation.pop_view_confirm import main as pop_view_confirm
+from examples.apps.routing_navigation.pop_views_until import main as pop_views_until
from examples.apps.routing_navigation.route_change_event import (
main as route_change_event,
)
@@ -185,3 +186,59 @@ async def test_drawer_navigation(flet_app_function: ftt.FletTestApp):
# Verify home view
home_text = await flet_app_function.tester.find_by_text("Welcome to Home Page")
assert home_text.count == 1
+
+
+@pytest.mark.parametrize(
+ "flet_app_function",
+ [{"flet_app_main": pop_views_until.main}],
+ indirect=True,
+)
+@pytest.mark.asyncio(loop_scope="function")
+async def test_pop_views_until(flet_app_function: ftt.FletTestApp):
+ # Verify initial view
+ button = await flet_app_function.tester.find_by_text_containing("Start flow")
+ assert button.count == 1
+ result_text = await flet_app_function.tester.find_by_text("No result yet")
+ assert result_text.count == 1
+
+ # Navigate to Step 1
+ await flet_app_function.tester.tap(button)
+ await flet_app_function.tester.pump_and_settle()
+ step1_text = await flet_app_function.tester.find_by_text("Step 1 of the flow")
+ assert step1_text.count == 1
+
+ # Navigate to Step 2
+ step2_button = await flet_app_function.tester.find_by_text_containing(
+ "Go to Step 2"
+ )
+ assert step2_button.count == 1
+ await flet_app_function.tester.tap(step2_button)
+ await flet_app_function.tester.pump_and_settle()
+ step2_text = await flet_app_function.tester.find_by_text("Step 2 of the flow")
+ assert step2_text.count == 1
+
+ # Navigate to Step 3
+ step3_button = await flet_app_function.tester.find_by_text_containing(
+ "Go to Step 3"
+ )
+ assert step3_button.count == 1
+ await flet_app_function.tester.tap(step3_button)
+ await flet_app_function.tester.pump_and_settle()
+ final_text = await flet_app_function.tester.find_by_text("Flow complete!")
+ assert final_text.count == 1
+
+ # Click "Finish and go Home" — triggers pop_views_until
+ finish_button = await flet_app_function.tester.find_by_text_containing(
+ "Finish and go Home"
+ )
+ assert finish_button.count == 1
+ await flet_app_function.tester.tap(finish_button)
+ await flet_app_function.tester.pump_and_settle()
+
+ # Verify back at Home with result
+ result_text = await flet_app_function.tester.find_by_text("Result: Flow completed!")
+ assert result_text.count == 1
+
+ # Verify we can start the flow again
+ button = await flet_app_function.tester.find_by_text_containing("Start flow")
+ assert button.count == 1
diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py
index dbdaee2877..b8c67ccdce 100644
--- a/sdk/python/packages/flet/src/flet/__init__.py
+++ b/sdk/python/packages/flet/src/flet/__init__.py
@@ -410,6 +410,7 @@
PlatformBrightnessChangeEvent,
RouteChangeEvent,
ViewPopEvent,
+ ViewsPopUntilEvent,
)
from flet.controls.painting import (
Paint,
@@ -1073,6 +1074,7 @@
"VerticalDivider",
"View",
"ViewPopEvent",
+ "ViewsPopUntilEvent",
"VisualDensity",
"Wakelock",
"WebBrowserName",
diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py
index 1ca0dcb1c3..c8fe4275b2 100644
--- a/sdk/python/packages/flet/src/flet/controls/page.py
+++ b/sdk/python/packages/flet/src/flet/controls/page.py
@@ -214,6 +214,32 @@ class ViewPopEvent(Event["Page"]):
"""
+@dataclass
+class ViewsPopUntilEvent(Event["Page"]):
+ """
+ Event payload delivered when :meth:`~flet.Page.pop_views_until` completes \
+ navigation.
+
+ Carries the result value back to the destination view.
+ """
+
+ route: str
+ """
+ Route of the destination view that remained on the stack.
+ """
+
+ result: Any = None
+ """
+ The result value passed from the caller of
+ :meth:`~flet.Page.pop_views_until`.
+ """
+
+ view: Optional[View] = None
+ """
+ Matched :class:`~flet.View` instance for `route`, if found on the page.
+ """
+
+
@dataclass
class KeyboardEvent(Event["Page"]):
"""
@@ -506,6 +532,13 @@ class Page(BasePage):
control.
"""
+ on_views_pop_until: Optional[EventHandler[ViewsPopUntilEvent]] = None
+ """
+ Called when :meth:`pop_views_until` reaches the destination view.
+
+ The event carries the result value passed by the caller.
+ """
+
on_keyboard_event: Optional[EventHandler[KeyboardEvent]] = None
"""
Called when a keyboard key is pressed.
@@ -707,7 +740,7 @@ def before_event(self, e: ControlEvent):
self.__last_route = e.route
self.query()
- elif isinstance(e, ViewPopEvent):
+ elif isinstance(e, ViewPopEvent | ViewsPopUntilEvent):
for v in unwrap_component(self.views):
v = unwrap_component(v)
if v.route == e.route:
@@ -897,6 +930,71 @@ async def view_pop(e):
arguments={"route": new_route},
)
+ async def pop_views_until(self, route: str, result: Any = None) -> None:
+ """
+ Pops views from the navigation stack until a view with the given
+ `route` is found, then delivers `result` via the
+ :attr:`on_views_pop_until` event.
+
+ Example:
+ ```python
+ import flet as ft
+
+
+ def main(page: ft.Page):
+ def on_pop_result(e: ft.ViewsPopUntilEvent):
+ page.show_dialog(ft.SnackBar(ft.Text(f"Result: {e.result}")))
+
+ page.on_views_pop_until = on_pop_result
+
+ # ... later, from a deeply nested view:
+ async def go_back(ev):
+ await page.pop_views_until("/", result="Done!")
+ ```
+
+ Args:
+ route: Target route to navigate back to. Must match the `route`
+ of an existing :class:`~flet.View` in :attr:`~flet.Page.views`.
+ result: Optional value delivered to
+ :attr:`on_views_pop_until` on the destination view.
+
+ Raises:
+ ValueError: If no view with the given `route` exists in
+ :attr:`~flet.Page.views`.
+ """
+ views = unwrap_component(self.views)
+
+ # Find the target view (first match from bottom of the stack)
+ target_idx = None
+ for i, v in enumerate(views):
+ v = unwrap_component(v)
+ if v.route == route:
+ target_idx = i
+ break
+
+ if target_idx is None:
+ raise ValueError(f"No view found with route '{route}' in page.views")
+
+ # Remove views above the target
+ del self.views[target_idx + 1 :]
+
+ # Update browser URL
+ await self.push_route(route)
+
+ # Fire on_views_pop_until for the destination view
+ if self.on_views_pop_until:
+ target_view = unwrap_component(views[target_idx])
+ e = ViewsPopUntilEvent(
+ name="views_pop_until",
+ control=self,
+ route=route,
+ result=result,
+ view=target_view,
+ )
+ await self._trigger_event("views_pop_until", event_data=None, e=e)
+
+ self.update()
+
def get_upload_url(self, file_name: str, expires: int) -> str:
"""
Generates presigned upload URL for built-in upload storage:
diff --git a/website/docs/cookbook/navigation-and-routing.md b/website/docs/cookbook/navigation-and-routing.md
index b1e07eb1fa..2b1a406c90 100644
--- a/website/docs/cookbook/navigation-and-routing.md
+++ b/website/docs/cookbook/navigation-and-routing.md
@@ -72,6 +72,15 @@ and confirm manually with [`View.can_pop`](../controls/view.md#flet.View.can_pop
+## Pop multiple views with a result
+
+When a multi-step flow finishes deep in the view stack, use
+[`page.pop_views_until()`](../controls/page.md#flet.Page.pop_views_until)
+to jump back to a target route and deliver a result to the destination view
+via [`page.on_views_pop_until`](../controls/page.md#flet.Page.on_views_pop_until).
+
+
+
## Navigation UI patterns
Routing composes well with navigation controls such as drawer, rail, and tabs.
diff --git a/website/static/docs/assets/navigation-routing/pop-until-with-result-example.gif b/website/static/docs/assets/navigation-routing/pop-until-with-result-example.gif
new file mode 100644
index 0000000000..753a9715ac
Binary files /dev/null and b/website/static/docs/assets/navigation-routing/pop-until-with-result-example.gif differ