Skip to content

声明式路由器 (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 匹配单个路径段:

    python
    ft.Route(path="users/:userId", component=UserProfile)
    # 路径匹配 /users/42 → use_route_params() 会返回 {"userId": "42"}
  • 可选段 (Optional segments) —— :paramName? 表示该段可以缺省:

    python
    ft.Route(path="users/:userId?", component=UserProfile)
    # 匹配 /users → {"userId": None}
    # 匹配 /users/42 → {"userId": "42"}
  • 通配段 (Splats / 捕获全部) —— :pathName* 匹配零个或多个路径段:

    python
    ft.Route(path="files/:path*", component=FileBrowser)
    # 匹配 /files/a/b/c → {"path": "a/b/c"}
  • 自定义正则 (Custom regex) —— :paramName(\\d+) 对匹配格式进行正则限制:

    python
    ft.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) —— 嵌套多个权限守卫路由:

    python
    ft.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检查指定的路由路径是否与当前活动位置匹配。

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