-
Notifications
You must be signed in to change notification settings - Fork 640
Description
Duplicate Check
- I have searched the opened issues and there are no duplicates
Describe the requested feature
Currently, Flet has no way to pop multiple views from the navigation stack and return a result to the destination view. Developers who need to navigate back through several pages (e.g., from a multi-step flow) and pass data back to the originating view must resort to shared application state or manual workarounds.
Flutter recently added Navigator.popUntilWithResult, which solves this exact problem on the imperative side. Flet should expose an equivalent in its declarative navigation model.
References:
- Flutter PR #169341
- Flutter Issue #30112 — 79+ reactions, open for 6 years (2019–2025)
- API Docs
- Short Video
Why this matters — the problem in Flutter and Flet
Flutter context
Flutter's issue #30112 was opened in March 2019 and received 79+ reactions. It took 6 years to be resolved (merged October 2025, shipped in Flutter 3.41.0).
The problem: Navigator.pop() accepts an optional result, but Navigator.popUntil() — which pops multiple routes — did not. Developers were forced into workarounds:
| Workaround | Problem |
|---|---|
Chained .pop()..pop()..pop(true) |
Brittle; requires knowing exact stack depth |
| Provider / Global State | Overkill for simple navigation results |
pushNamedAndRemoveUntil with arguments |
Wrong animation direction (push instead of pop) |
shared_preferences |
Extremely heavy; race conditions |
Completer<T> passed through stack |
Complex plumbing; easy to leak |
Developer quotes from the issue:
"Very ugly for something that should've been provided out of the box by Flutter." — radiKal07
"I have no clue why is this not implemented yet." — topnax
The same problem in Flet
The problem repeats in Flet, and is arguably worse because Flet's declarative model makes the workarounds more verbose.
The existing ViewPopEvent has no result field:
class ViewPopEvent(Event["Page"]):
route: str
view: Optional[View] = None
# No result!Workarounds Flet developers must use today:
1. Global shared state (most common)
result_holder = {"value": None}
async def finish_flow(e):
result_holder["value"] = "completed" # write to global
while len(page.views) > 1:
page.views.pop()
await page.push_route("/")
def route_change():
# ... rebuild views ...
if result_holder["value"]: # check global manually
show_result(result_holder["value"])
result_holder["value"] = None # manual cleanup2. Session store
page.session.store.set("flow_result", "completed")
# ... later ...
result = page.session.store.get("flow_result")3. Manual view stack loop
while len(page.views) > 1 and page.views[-1].route != "/":
page.views.pop()
await page.push_route("/")
# Result? Still need global state.All of these suffer from: no type-safety, manual cleanup, race conditions, tight coupling between views, and excessive boilerplate.
Describe the solution you'd like
A new method on Page:
await page.pop_until_with_result(route: str, result: Any = None)Along with a new event:
page.on_view_pop_result = handler # receives ViewPopResultEventBefore vs After
BEFORE (workaround with global state):
result_holder = {"value": None}
def main(page: ft.Page):
def route_change():
page.views.clear()
page.views.append(ft.View(route="/", controls=[
ft.Text(f"Result: {result_holder['value']}"),
ft.Button("Start", on_click=lambda _:
asyncio.create_task(page.push_route("/step1"))),
]))
if page.route == "/step2":
page.views.append(ft.View(route="/step2", controls=[
ft.Button("Finish", on_click=lambda _: finish()),
]))
if result_holder["value"]:
page.show_dialog(ft.SnackBar(ft.Text(result_holder["value"])))
result_holder["value"] = None
page.update()
async def finish():
result_holder["value"] = "Flow completed!"
while len(page.views) > 1:
page.views.pop()
await page.push_route("/")AFTER (with pop_until_with_result):
def main(page: ft.Page):
def route_change():
page.views.clear()
page.views.append(ft.View(route="/", controls=[...]))
if page.route == "/step2":
page.views.append(ft.View(route="/step2", controls=[
ft.Button("Finish", on_click=lambda _:
asyncio.create_task(
page.pop_until_with_result("/", result="Flow completed!")
)),
]))
page.update()
def on_pop_result(e: ft.ViewPopResultEvent):
page.show_dialog(ft.SnackBar(ft.Text(f"Result: {e.result}")))
page.update()
page.on_route_change = route_change
page.on_view_pop_result = on_pop_result| Aspect | Before | After |
|---|---|---|
| Result passing | Global state or closure | result in event |
| Data cleanup | Manual | Automatic |
| Type-safety | None | ViewPopResultEvent.result |
| View removal | Manual loop | One method call |
| Route update | Manual (push_route + update) |
Automatic |
| Lines of code | ~15-20 for the workaround | 1 call + 1 handler |
| Bug risk | High (race conditions, missing view) | Low (clear ValueError) |
Implementation details
Architecture context
Flet uses a declarative Navigator (based on pages: [...]), not imperative push/pop. The page.views list in Python is the source of truth and is synced to the Dart client's Navigator. Because of this, Flutter's Navigator.popUntilWithResult cannot be called directly — the equivalent semantics are implemented within Flet's own navigation model.
No Dart-side changes are needed. The Python-side page.update() syncs the updated view list to the Dart client, and the Flutter Navigator rebuilds with correct transition animations automatically.
Edge cases
| Scenario | Expected behavior |
|---|---|
Target route not found in page.views |
Raise ValueError |
| Target route is already the top view | No views popped; only on_view_pop_result fires |
| Only one view in stack (and it's the target) | Only on_view_pop_result fires |
| Multiple views with the same route | First match from the bottom (like Flutter's ModalRoute.withName) |
Suggest a solution
No response
Screenshots
Additional details
I already have a working implementation with unit tests and example app ready to submit as a PR.
