Skip to content

feature: Add pop_until_with_result to Page - pop multiple views and return a result #6326

@brunobrown

Description

@brunobrown

Duplicate Check

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:


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 cleanup

2. 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 ViewPopResultEvent

Before 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

Image

Additional details

I already have a working implementation with unit tests and example app ready to submit as a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature requestSuggestion/Request for additional feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions