主题
导航与路由 (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)
下面的模式是大多数应用的基础实践:
- 清空
page.views。 - 添加根视图(
/)。 - 根据
page.route的值,有条件地追加其他视图。 - 在
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),避免使用手动字符串分割。