Skip to content

导航与路由 (Navigation and Routing) ​


导航与路由是构建多页面 Flet 应用的核心。它允许你将 UI 组织成虚拟页面(View 对象),保持 URL/历史记录同步,并支持指向特定应用状态的深度链接(Deep links)。

本文档主要介绍当前的路由模型以及相应的实践示例。


Flet 中的路由模型 (Routing model in Flet) ​

一个 Page 是视图的容器(page.views),其中每个视图(View)代表一个路由级别的屏幕。

  • page.route 是当前的路由字符串(例如 /、/store、/settings/mail)。
  • page.views 是当前处于激活状态的导航栈。
  • page.on_route_change 在路由改变时重新构建视图栈。
  • page.on_view_pop 处理返回导航(包括系统物理返回键、AppBar 返回按钮以及浏览器返回按钮)。

一个可靠的路由配置应使用单一的数据源:根据 page.route 计算并推导出 page.views 栈。


路由基础 (Route basics) ​

当未提供路由时,默认的路由是 /。

提示

你可以在这里在线尝试该示例。

python
import flet as ft


def main(page: ft.Page):
    page.add(ft.SafeArea(content=ft.Text(f"Initial route: {page.route}")))


if __name__ == "__main__":
    ft.run(main)

所有路由都应该以 / 开头,例如 /store、/products/42、/settings/mail。


处理路由改变 (Handling route changes) ​

每当路由发生改变(修改浏览器 URL、点击浏览器前进/后退、或应用内执行导航跳转)时,都会触发 page.on_route_change 事件。应在这个事件中决定当前路由下应该存在哪些视图。

提示

你可以在这里在线尝试该示例。

python
import flet as ft


def main(page: ft.Page):
    route_log = ft.Column(controls=[ft.Text(f"Initial route: {page.route}")])
    page.add(ft.SafeArea(content=route_log))

    def route_change(e):
        route_log.controls.append(ft.Text(f"New route: {e.route}"))
        page.update()

    page.on_route_change = route_change
    page.update()


if __name__ == "__main__":
    ft.run(main)

根据路由构建视图 (Building views from route) ​

下面的模式是大多数应用的基础实践:

  1. 清空 page.views。
  2. 添加根视图(/)。
  3. 根据 page.route 的值,有条件地追加其他视图。
  4. 在 page.on_view_pop 中处理返回操作,并导航到新的栈顶视图。
为什么这个模式如此重要? ​
  • 保持 URL、历史记录栈以及当前呈现的 UI 同步。
  • 自然地支持深度链接和刷新。
  • 使导航行为是确定性的,更易于调试。

提示

你可以在这里在线尝试该示例。

python
import flet as ft


def main(page: ft.Page):
    page.title = "Routes Example"

    print("Initial route:", page.route)

    async def open_mail_settings(e):
        await page.push_route("/settings/mail")

    async def open_settings(e):
        await page.push_route("/settings")

    def route_change():
        print("Route change:", page.route)
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.SafeArea(
                        content=ft.Column(
                            controls=[
                                ft.AppBar(title=ft.Text("Flet app")),
                                ft.Button("Go to settings", on_click=open_settings),
                            ]
                        )
                    )
                ],
            )
        )
        if page.route == "/settings" or page.route == "/settings/mail":
            page.views.append(
                ft.View(
                    route="/settings",
                    controls=[
                        ft.SafeArea(
                            content=ft.Column(
                                controls=[
                                    ft.AppBar(
                                        title=ft.Text("Settings"),
                                        bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                                    ),
                                    ft.Text(
                                        "Settings!",
                                        theme_style=ft.TextThemeStyle.BODY_MEDIUM,
                                    ),
                                    ft.Button(
                                        content="Go to mail settings",
                                        on_click=open_mail_settings,
                                    ),
                                ]
                            )
                        )
                    ],
                )
            )
        if page.route == "/settings/mail":
            page.views.append(
                ft.View(
                    route="/settings/mail",
                    controls=[
                        ft.SafeArea(
                            content=ft.Column(
                                controls=[
                                    ft.AppBar(
                                        title=ft.Text("Mail Settings"),
                                        bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                                    ),
                                    ft.Text("Mail settings!"),
                                ]
                            )
                        )
                    ],
                )
            )
        page.update()

    async def view_pop(e):
        if e.view is not None:
            print("View pop:", e.view)
            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

    route_change()


if __name__ == "__main__":
    ft.run(main)

编程式导航 (Programmatic navigation) ​

在同步回调函数中(如 on_click)可以使用 page.navigate() 进行导航;在异步上下文中,则使用 page.push_route():

python
# 同步调用 (on_click 等)
page.navigate("/search", q="flet", page=2)

# 异步调用
await page.push_route("/search", q="flet", page=2)

返回导航与 Pop 确认 (Back navigation and pop confirmation) ​

当用户返回时,Flet 会触发 page.on_view_pop 事件。对于需要用户确认的交互流程(例如,当前表单有未保存的更改),可以禁用自动 Pop,并通过 View.can_pop 和 View.on_confirm_pop 进行手动确认。

提示

你可以在这里在线尝试该示例。

python
import flet as ft


def main(page: ft.Page):
    page.title = "Routes Example"

    def route_change():
        page.views.clear()
        page.views.append(MainView("/"))
        if page.route == "/store":
            page.views.append(PermissionView("/store"))
        page.update()

    async def view_pop(e: ft.ViewPopEvent):
        if e.view is not None:
            print("View pop:", e.view)
            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

    route_change()


class MainView(ft.View):
    def __init__(self, path):
        super().__init__(
            route=path,
            controls=[
                ft.SafeArea(
                    content=ft.Column(
                        controls=[
                            ft.AppBar(title=ft.Text("Flet app")),
                            ft.Button("Go to store", on_click=self.open_store),
                        ]
                    )
                )
            ],
        )

    async def open_store(self, e):
        await self.page.push_route("/store")


class PermissionView(ft.View):
    def __init__(self, path):
        super().__init__(
            route=path,
            controls=[
                ft.SafeArea(
                    content=ft.Column(
                        controls=[ft.AppBar(title=ft.Text(f"{path} View"))]
                    )
                )
            ],
            can_pop=False,
            on_confirm_pop=self.ask_pop_permission,
        )

    async def ask_pop_permission(self, e):
        async def on_dlg_yes(e):
            self.page.pop_dialog()
            await self.confirm_pop(True)

        async def on_dlg_no(e):
            self.page.pop_dialog()
            await self.confirm_pop(False)

        dlg_modal = ft.AlertDialog(
            title=ft.Text("Please confirm"),
            content=ft.Text("Go home?"),
            actions=[
                ft.TextButton(
                    "Yes",
                    on_click=on_dlg_yes,
                ),
                ft.TextButton(
                    "No",
                    on_click=on_dlg_no,
                ),
            ],
            actions_alignment=ft.MainAxisAlignment.END,
            on_dismiss=lambda e: print("Modal dialog dismissed!"),
        )

        self.page.show_dialog(dlg_modal)


if __name__ == "__main__":
    ft.run(main)

携带结果返回多个视图 (Pop multiple views with a result) ​

当一个多步骤的流程在视图栈的深处结束时,可以使用 page.pop_views_until() 直接跳转回目标路由,并通过 page.on_views_pop_until 将结果传递给目标视图。

提示

你可以在这里在线尝试该示例。

python
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()

        # 主视图 (/)
        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)

导航 UI 模式 (Navigation UI patterns) ​

路由功能可以与导航控件(例如 NavigationDrawer、NavigationRail、Tabs)良好地结合使用。以下示例展示了一个由路由驱动的多入口侧边栏(NavigationDrawer)导航:

提示

你可以在这里在线尝试该示例。

python
import asyncio

import flet as ft


def main(page: ft.Page):
    page.title = "Drawer navigation"

    async def handle_change(e):
        if e.control.selected_index == 0:
            await page.push_route("/")
        elif e.control.selected_index == 1:
            await page.push_route("/store")
        elif e.control.selected_index == 2:
            await page.push_route("/about")

    def create_drawer(selected_index=0):
        return ft.NavigationDrawer(
            selected_index=selected_index,
            on_change=handle_change,
            controls=[
                ft.Container(height=12),
                ft.NavigationDrawerDestination(
                    label="Home",
                    icon=ft.Icons.HOME_OUTLINED,
                    selected_icon=ft.Icon(ft.Icons.HOME),
                ),
                ft.Divider(thickness=2),
                ft.NavigationDrawerDestination(
                    label="Store",
                    icon=ft.Icon(ft.Icons.STORE_OUTLINED),
                    selected_icon=ft.Icon(ft.Icons.STORE),
                ),
                ft.NavigationDrawerDestination(
                    label="About",
                    icon=ft.Icon(ft.Icons.PHONE_OUTLINED),
                    selected_icon=ft.Icons.PHONE,
                ),
            ],
        )

    async def show_drawer():
        await page.show_drawer()

    def route_change(route):
        page.views.clear()
        page.views.append(
            ft.View(
                route="/",
                controls=[
                    ft.SafeArea(
                        content=ft.Column(
                            controls=[
                                ft.AppBar(
                                    title=ft.Text("Home", expand=True),
                                    bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                                    leading=ft.IconButton(
                                        ft.Icons.MENU, on_click=show_drawer
                                    ),
                                ),
                                ft.Text("Welcome to Home Page"),
                            ]
                        )
                    )
                ],
                drawer=create_drawer(selected_index=0) if page.route == "/" else None,
            )
        )

        if page.route == "/store":
            page.views.append(
                ft.View(
                    route="/store",
                    controls=[
                        ft.SafeArea(
                            content=ft.Column(
                                controls=[
                                    ft.AppBar(
                                        title=ft.Text("Store", expand=True),
                                        bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                                        leading=ft.IconButton(
                                            ft.Icons.MENU, on_click=show_drawer
                                        ),
                                        automatically_imply_leading=False,
                                    ),
                                    ft.Text("Welcome to Store Page"),
                                    ft.Button(
                                        "Go About",
                                        on_click=lambda _: asyncio.create_task(
                                            page.push_route("/about")
                                        ),
                                    ),
                                ]
                            )
                        )
                    ],
                    drawer=create_drawer(selected_index=1),
                )
            )

        if page.route == "/about":
            page.views.append(
                ft.View(
                    route="/about",
                    controls=[
                        ft.SafeArea(
                            content=ft.Column(
                                controls=[
                                    ft.AppBar(
                                        title=ft.Text("About", expand=True),
                                        bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
                                        leading=ft.IconButton(
                                            ft.Icons.MENU, on_click=show_drawer
                                        ),
                                        automatically_imply_leading=False,
                                    ),
                                    ft.Text("Welcome to About Page"),
                                    ft.Button(
                                        "Go Store",
                                        on_click=lambda _: asyncio.create_task(
                                            page.push_route("/store")
                                        ),
                                    ),
                                ]
                            )
                        )
                    ],
                    drawer=create_drawer(selected_index=2),
                )
            )
        page.update()

    async def view_pop(view):
        page.views.pop()
        top_view = page.views[-1]
        await page.push_route(top_view.route)

    page.on_route_change = route_change
    page.on_view_pop = view_pop
    route_change(page.route)


if __name__ == "__main__":
    ft.run(main)

路由模板(参数化路由) (Route templates) ​

使用 TemplateRoute 可以非常方便地匹配和解析路由参数(例如 /books/:id)。模板的匹配语法基于 repath 库。

python
import flet as ft

troute = ft.TemplateRoute(page.route)

if troute.match("/books/:id"):
    print("Book ID:", troute.id)
elif troute.match("/account/:account_id/orders/:order_id"):
    print("Account:", troute.account_id, "Order:", troute.order_id)
else:
    print("Unknown route")

Web 端 URL 策略 (Web URL strategy) ​

Flet Web 应用支持两种 URL 策略:

  • "path"(默认):URL 形式如 https://myapp.dev/store
  • "hash":URL 形式如 https://myapp.dev/#/store

可以通过 ft.run() 函数中的 route_url_strategy 属性来进行配置:

python
import flet as ft
ft.run(main, route_url_strategy="hash")

对于部署的 Flet 服务器,你也可以通过设置 FLET_ROUTE_URL_STRATEGY 环境变量来配置此选项。


声明式路由器 (Declarative Router) ​

对于使用 @component 的声明式应用,Flet 提供了一个灵感来源于 React Router 的声明式路由器(Router)系统。它能够自动处理嵌套路由、带 <Outlet> 的布局路由、动态路由段、数据加载器(data loaders)以及活动链接检测,完全省去了手动进行路由匹配的麻烦。

在将 manage_views 设置为 True 时,声明式路由器还可以直接托管视图栈(View stack),自动启用移动端的滑动返回手势及 AppBar 的返回按钮。路由组件可以直接返回包含自身 AppBar 的 View 对象,导航到更深层次的路由时会自动向栈中压入新的视图。

有关完整配置指南,请查阅“声明式路由器(Router)食谱”章节。


实践建议 (Practical recommendations) ​

  • 始终在 page.views 视图栈的底部保留根 / 视图。
  • 保持路由处理逻辑完全集中在 page.on_route_change 回调中,避免在多处散乱地直接修改 page.views。
  • 添加新路由时,注意测试以下场景:直接访问深度链接、浏览器前进/后退、应用层返回按钮、以及刷新页面。
  • 处理动态参数路由段时,请使用路由模板(TemplateRoute),避免使用手动字符串分割。

关注我,学习大模型、大数据、Python等热门技术。