主题
声明式路由器 (Router)
Router 是 Flet 应用的声明式路由系统,灵感来源于 React Router。它能够自动处理嵌套路由匹配、带出口(outlet)的布局路由、动态路由段、数据加载、视图栈导航,并提供一系列访问路由状态的 Hook。
为什么选择 Router?
如果不使用 Router,多页面 Flet 应用需要手动编写路由匹配逻辑、处理条件渲染以及提取路由参数。Router 则将这一切自动化:
- 嵌套路由 —— 定义父/子路由的树状层级关系。
- 布局路由 —— 使用
use_route_outlet()在多个页面之间共享公共布局(Header、Footer、Sidebar 等)。 - 动态路由段 —— 在路径中使用
:paramName,并通过use_route_params()提取参数。 - 数据加载 —— 在组件渲染前,使用
loader自动获取异步数据。 - 活动链接检测 —— 使用
is_route_active()高亮处于当前活动状态的导航条目。 - 视图栈导航 —— 通过
manage_views=True自动支持移动端的物理返回/手势侧滑返回以及 AppBar 默认的返回按钮。
快速开始 (Quick start)
python
import flet as ft
@ft.component
def Home():
return ft.Text("Welcome home!")
@ft.component
def About():
return ft.Text("About us")
@ft.component
def App():
return ft.Router([
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
])
ft.run(lambda page: page.render(App))定义路由 (Defining routes)
路由是以 Route 对象树的形式进行定义的。
扁平路由 (Flat routes)
所有路由都在顶层定义:
python
ft.Router([
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
ft.Route(path="contact", component=Contact),
])嵌套路由 (Nested routes)
子路由嵌套在父路由的 children 属性下。父路由的路径会自动拼接到子路由路径之前:
python
ft.Route(
path="products",
component=ProductsList,
children=[
ft.Route(path=":pid", component=ProductDetails),
],
)
# /products → 匹配 ProductsList
# /products/42 → 匹配 ProductDetails,动态参数为 {"pid": "42"}索引路由 (Index routes)
当父路由路径完全匹配,且没有提供额外的后续子路径段时,索引路由(index=True)会被触发渲染:
python
ft.Route(path="settings", children=[
ft.Route(index=True, component=SettingsHome), # 匹配路径 /settings
ft.Route(path="profile", component=Profile), # 匹配路径 /settings/profile
])前缀路由 (Prefix routes)
- 无路径布局路由 (Pathless layout routes) —— 将子路由组合在一个共享布局下,而不增加任何路径段:
python
ft.Route(component=AdminLayout, children=[
ft.Route(path="users", component=Users), # 匹配路径 /users
ft.Route(path="settings", component=Settings), # 匹配路径 /settings
])- 仅路径路由 (Path-only routes) —— 添加一个路径前缀,但不在该层级渲染特定组件:
python
ft.Route(path="api", children=[
ft.Route(path="users", component=ApiUsers), # 匹配路径 /api/users
ft.Route(path="products", component=ApiProducts), # 匹配路径 /api/products
])布局路由与 use_route_outlet()
布局路由具有一个 component 和多个 children。布局组件负责渲染共享的 UI(如侧边栏、页眉、页脚),并通过调用 use_route_outlet() 将匹配到的当前子路由组件放置在对应容器中:
python
@ft.component
def AppLayout():
outlet = ft.use_route_outlet()
return ft.Column([
ft.AppBar(title=ft.Text("My App")),
ft.Container(content=outlet, expand=True),
ft.Text("Footer"),
])
ft.Router([
ft.Route(component=AppLayout, children=[
ft.Route(index=True, component=Home),
ft.Route(path="about", component=About),
]),
])布局路由可以嵌套到任意深度。每一层布局都可以调用 use_route_outlet() 来插入其直接匹配到的子组件。查看完整示例。
动态路由段、可选路由段与通配段
路由路径底层的 repath 库支持 Express 风格的模式匹配。
动态段 (Dynamic segments) ——
:paramName匹配单个路径段:pythonft.Route(path="users/:userId", component=UserProfile) # 路径匹配 /users/42 → use_route_params() 会返回 {"userId": "42"}可选段 (Optional segments) ——
:paramName?表示该段可以缺省:pythonft.Route(path="users/:userId?", component=UserProfile) # 匹配 /users → {"userId": None} # 匹配 /users/42 → {"userId": "42"}通配段 (Splats / 捕获全部) ——
:pathName*匹配零个或多个路径段:pythonft.Route(path="files/:path*", component=FileBrowser) # 匹配 /files/a/b/c → {"path": "a/b/c"}自定义正则 (Custom regex) ——
:paramName(\\d+)对匹配格式进行正则限制:pythonft.Route(path="item/:id(\\d+)", component=ItemDetails) # /item/42 → 匹配 # /item/abc → 不匹配 (404)
导航 (Navigation)
在同步回调上下文中,使用 page.navigate():
python
ft.Button("About", on_click=lambda: ft.context.page.navigate("/about"))在异步上下文中,请使用 page.push_route() 配合 await。当路由改变时,Router 会自动更新渲染。
使用 is_route_active() 高亮活动链接
如果给定的路径与当前激活的位置匹配,is_route_active(path) 将返回 True:
python
ft.Button(
"Products",
style=ft.ButtonStyle(
bgcolor=ft.Colors.PRIMARY_CONTAINER
if ft.is_route_active("/products")
else None,
),
on_click=lambda: ft.context.page.navigate("/products"),
)默认情况下,该函数使用前缀匹配 —— 例如 is_route_active("/products") 对 /products/42 也会返回 True。如果需要精确完全匹配,可以传入 exact=True。查看完整示例。
数据预加载 (Data loading)
路由可以关联一个数据加载器(loader)函数。每当该路由被匹配到时,数据加载逻辑会首先执行,其返回的数据可在组件中通过 use_route_loader_data() 获取:
python
def load_user(params):
return db.get_user(params["userId"])
ft.Route(path="users/:userId", component=UserProfile, loader=load_user)
@ft.component
def UserProfile():
user = ft.use_route_loader_data()
return ft.Text(f"Hello, {user.name}")身份验证 (Authentication / 路由守卫)
Flet 不需要专门的路由守卫 API,可通过现有的布局路由灵活地实现权限管控。
页面重定向 (Page redirect) —— 如果未认证则跳转至登录页:
python@ft.component def ProtectedRoute(): outlet = ft.use_route_outlet() if not auth.is_authenticated: ft.context.page.navigate("/login") return ft.Text("Redirecting...") return outlet基于角色的细粒度控制 (Role-based access) —— 嵌套多个权限守卫路由:
pythonft.Router([ ft.Route(component=ProtectedRoute, children=[ ft.Route(index=True, component=Home), ft.Route(component=AdminOnly, children=[ ft.Route(path="admin", component=AdminPanel), ]), ]), ])
404 未找到处理 (404 handling)
在 Router 中可以配置 not_found 组件,在无任何路由匹配时渲染自定义错误提示页面:
python
@ft.component
def NotFound():
location = ft.use_route_location()
return ft.Text(f"Page not found: {location}")
ft.Router(routes, not_found=NotFound)多视图导航 (Multi-view navigation)
默认情况下,Router 在单个 View 中呈现所有路径的内容。如果将 manage_views 设置为 True,Router 会返回一个 View 列表 —— 每个路径层级各对应一个 View,并在应用中自动开启多视图栈。这为你带来:
- 移动端的侧滑返回手势(Swipe-back)
- 操作系统物理返回键支持
- AppBar 内部默认的返回键
- 不同路由层级之间的平滑滑动动画
在多视图栈模式下,请使用 page.render_views() 替代 page.render()。
组件返回 View (Components return Views)
每个层级的组件都返回包含对应 route、appbar 和 controls 属性的 View 实例:
python
@ft.component
def ProductDetails():
params = ft.use_route_params()
return ft.View(
route=f"/products/{params['pid']}",
appbar=ft.AppBar(title=ft.Text(f"Product #{params['pid']}")),
controls=[
ft.Text(f"Details for product #{params['pid']}"),
],
)嵌套路由层级 (Nested route hierarchy)
嵌套多视图路由结构如下:
python
ft.Router([
ft.Route(component=Home, children=[
ft.Route(path="products", component=ProductsList, children=[
ft.Route(path=":pid", component=ProductDetails),
]),
]),
], manage_views=True)视图栈的表现形式:
/—— 栈中包含 1 个视图[Home]。/products—— 栈中包含 2 个视图[Home, Products](带返回 Home 按钮)。/products/1—— 栈中包含 3 个视图[Home, Products, Product Details](带返回 Products 按钮)。
通过 outlet=True 共享布局
在路由上设置 outlet=True,能使其作为一个共享布局,并通过 use_route_outlet() 把它的子路由包装在该布局下。共享布局返回一个 View,子组件返回常规的 controls(不需要返回 View)。
使用 use_view_path() 作为 View.route 的值。它会返回每个视图层级解析后的具体路径,以便 Flutter 的 Navigator 可以在视图栈中分配唯一的 Key:
python
@ft.component
def ProductsLayout():
outlet = ft.use_route_outlet()
return ft.View(
route=ft.use_view_path(),
appbar=ft.AppBar(title=ft.Text("Products")),
controls=[
ft.Container(content=outlet, expand=True),
ft.Text("Footer"),
],
)
ft.Route(path="products", component=ProductsLayout, outlet=True, children=[
ft.Route(component=ProductsList, children=[
ft.Route(path=":pid", component=ProductDetails),
]),
])use_route_location() 与 use_view_path() 的区别
use_route_location() 始终返回完整的当前 URL 地址。而 use_view_path() 只返回当前视图层级的解析路径。当一个单独的布局组件包装了多个子视图时,必须使用 use_view_path() 赋值给 View.route,这样 Flutter Navigator 才能识别并区分它们。
布局的共享 UI 同样会呈现在每个子路由 View 上,但各个子路由仍作为单独的 View 保留在栈中 —— 它们之间的物理/返回导航仍能正常工作。查看完整示例。
避免视图切换的滑动动画 (Avoiding transition animation)
在顶级 Tab 页面(如 NavigationRail 的各个目的地)之间导航时,可能不需要层级间的滑动动画。你可以在根布局的 View 路由属性上绑定一个固定的 route 字符串,以消除切换动画:
python
@ft.component
def RootLayout():
outlet = ft.use_route_outlet()
return ft.View(
route="/", # 固定 Key —— 使顶级页面间无滑动过渡动画
can_pop=False,
controls=[
ft.Row([
ft.NavigationRail(...),
ft.Container(content=outlet, expand=True),
], expand=True),
],
)自定义返回导航 (Back navigation)
当用户 pop 最上层视图时(例如关闭模态弹窗、AppBar 上的返回箭头、iOS 侧滑、系统返回键),Router 默认会退回该页面在路由配置树中的直接父节点 —— 即 chain[-2].resolved_path,而不是 views[-2].route。即使采用了上面“固定 key 消除顶级页面动画”的技巧,深层路由的 pop 仍然准确无误(如根布局强制固定为 / 时,从 /gallery/photo 退出仍能回到 /gallery)。
如果你需要完全控制 pop 逻辑,请在调用 page.render_views(App) 之前挂载你的 page.on_view_pop 处理器。Router 检测到现有处理器时,便会将控制权全权转交,而不再运行默认路由树回溯。
模态路由 (Modal routes)
设置 modal=True 会使得该路由组件渲染的 View 以全屏对话框(Fullscreen dialog)叠加层的方式,覆盖在先前的非模态路由视图栈顶部。关闭模态组件将直接弹出 pop,而不会触发下方视图栈的重新加载,避开了重新构建底层视图造成的闪烁。
模态组件与普通页面相同,都需要返回 View 实例。推荐将 View 的 fullscreen_dialog 设置为 True,以便 Flutter 呈现由下至上的拉起过渡动画,并在 AppBar 上左侧显示关闭 (X) 按钮。
全局模态路由 (Global modals) —— 随时随地跳转
声明在路由树顶层。它的下方视图栈背景将默认拉取前一个路由栈(若首次直接加载该模态,则默认背景为 /):
python
ft.Router([
ft.Route(component=Home),
ft.Route(component=Profile, path="profile"),
ft.Route(component=SettingsModal, path="settings", modal=True),
], manage_views=True)
# 访问 / → 栈为 [Home]
# 访问 /profile → 栈为 [Profile]
# 从 /profile 导航到 /settings → 栈为 [Profile, SettingsModal](Settings 覆盖在 Profile 之上,Profile 不会被卸载)
# 关闭 SettingsModal → 栈恢复 [Profile],无需重新构建 Profile局部模态路由 (Local modals) —— 挂载在特定主页面
声明为某个特定非模态路由的 children。其下方视图栈即为它之上的父路由链,这有利于在 URL 中直接编码它所属的位置,也使得直接深度链接加载该模态非常方便:
python
ft.Router([
ft.Route(component=ItemList, path="items", children=[
ft.Route(component=ItemDetails, path=":id", children=[
ft.Route(component=EditModal, path="edit", modal=True),
]),
]),
], manage_views=True)
# 访问 /items/42 → 栈为 [ItemList, ItemDetails(42)]
# 访问 /items/42/edit → 栈为 [ItemList, ItemDetails(42), EditModal]
# 关闭 EditModal → 返回 /items/42递归路由 (Recursive routes)
声明 recursive=True 可以使一个路由匹配自身作为其后代路径。这允许我们仅声明一次配置,就能匹配无限深度的树状 URL,不需要为每层嵌套写一堆重复的嵌套路由声明:
python
ft.Router([
ft.Route(path="folder", component=Folders, children=[
ft.Route(
path=":name",
component=Folder,
recursive=True,
children=[
ft.Route(path="search", component=Search),
],
),
]),
], manage_views=True)
# 访问 /folder/a/b/c → 栈为 [Folders, Folder(a), Folder(a/b), Folder(a/b/c)]
# 侧滑返回将逐级回退一格路径。在组件中,调用 use_route_params() 只会返回匹配到当前层级的参数(例如在第二层 Folder(a/b) 对应的 Folder 组件里,它返回的值是 {"name": "b"})。若要获取当前视图层级之前的累积完整 URL,请调用 use_view_path()。
兄弟节点优先匹配规则 (Sibling-wins-over-recursion rule)
在任何嵌套深度,普通的非递归子路由比自我递归匹配的优先级更高。在上述例子中,路径 /folder/x/search 将匹配为 [Folders, Folder(x), Search] 结构,即 Search 兄弟组件被优先解析并匹配,而不是匹配成 Folder 的自我递归 Folder("search")。这极大地方便了将特定控制组件插入树状路由的任意位置,而无需在多处定义。查看完整示例。
Hook 参考 (Hooks reference)
| Hook | 返回值 | 描述 |
|---|---|---|
use_route_params() | dict[str, str] | 获取当前匹配的路由链中,所有动态参数段的值。 |
use_route_location() | str | 获取当前 URL 完整路径名(pathname)。 |
use_view_path() | str | 获取当前视图层级解析后的 URL。在 manage_views 多视图模式中每个 View 路由属性中极其有用。 |
use_route_outlet() | component | 匹配到的子路由组件实例,通常在带有子级的共享布局路由中渲染。 |
use_route_loader_data() | Any | 获取当前路由绑定的 loader 加载函数返回的初始数据。 |
is_route_active(path) | bool | 检查指定的路由路径是否与当前活动位置匹配。 |