🧭 Navigation & Routing
Học cách điều hướng giữa các màn hình trong ứng dụng Flet. ⏱️ 20 phút
🎯 Mục tiêu
Phần tiêu đề “🎯 Mục tiêu”Nắm vững:
- Routes và Navigation
- Views (màn hình)
- AppBar và NavigationBar
- Passing data giữa các màn hình
📍 Routes cơ bản
Phần tiêu đề “📍 Routes cơ bản”Flet sử dụng routes để điều hướng, tương tự web URLs:
import flet as ft
@ft.observableclass RouterState: route: str = "/"
state = RouterState()
@ft.componentdef HomeView(): return ft.View( "/", [ ft.AppBar(title=ft.Text("Home")), ft.Text("Welcome to Home!", size=24), ft.ElevatedButton( "Go to Settings", on_click=lambda _: setattr(state, 'route', '/settings'), ), ], )
@ft.componentdef SettingsView(): return ft.View( "/settings", [ ft.AppBar(title=ft.Text("Settings")), ft.Text("Settings Page", size=24), ft.ElevatedButton( "Back to Home", on_click=lambda _: setattr(state, 'route', '/'), ), ], )
@ft.componentdef Router(): if state.route == "/settings": return SettingsView() return HomeView()
def main(page: ft.Page): page.title = "Navigation Demo" page.add(Router())
ft.run(main)🏗️ Tổ chức code với Components
Phần tiêu đề “🏗️ Tổ chức code với Components”Tách view thành các components riêng:
import flet as ft
@ft.observableclass AppState: current_route: str = "/"
def go(self, route: str): self.current_route = route
state = AppState()
@ft.componentdef HomeView(): return ft.View( "/", [ ft.AppBar(title=ft.Text("Home"), bgcolor=ft.Colors.BLUE), ft.Container( content=ft.Column([ ft.Text("🏠 Home Page", size=28, weight=ft.FontWeight.BOLD), ft.ElevatedButton("Go to Profile", on_click=lambda _: state.go("/profile")), ft.ElevatedButton("Go to Settings", on_click=lambda _: state.go("/settings")), ], spacing=20), padding=30, ), ], )
@ft.componentdef ProfileView(): return ft.View( "/profile", [ ft.AppBar( title=ft.Text("Profile"), bgcolor=ft.Colors.GREEN, leading=ft.IconButton(ft.Icons.ARROW_BACK, on_click=lambda _: state.go("/")), ), ft.Container( content=ft.Column([ ft.Text("👤 Profile Page", size=28, weight=ft.FontWeight.BOLD), ft.Text("User: John Doe"), ], spacing=20), padding=30, ), ], )
@ft.componentdef SettingsView(): return ft.View( "/settings", [ ft.AppBar( title=ft.Text("Settings"), bgcolor=ft.Colors.ORANGE, leading=ft.IconButton(ft.Icons.ARROW_BACK, on_click=lambda _: state.go("/")), ), ft.Container( content=ft.Column([ ft.Text("⚙️ Settings Page", size=28, weight=ft.FontWeight.BOLD), ft.Switch(label="Dark Mode"), ft.Switch(label="Notifications"), ], spacing=20), padding=30, ), ], )
@ft.componentdef AppRouter(): routes = { "/": HomeView, "/profile": ProfileView, "/settings": SettingsView, } ViewComponent = routes.get(state.current_route, HomeView) return ViewComponent()
def main(page: ft.Page): page.title = "Navigation Demo" page.add(AppRouter())
ft.run(main)🔗 Route với Parameters
Phần tiêu đề “🔗 Route với Parameters”Truyền dữ liệu qua state:
import flet as ft
@ft.observableclass ItemsState: items: list = [ {"id": 1, "name": "Item 1", "description": "Description 1"}, {"id": 2, "name": "Item 2", "description": "Description 2"}, {"id": 3, "name": "Item 3", "description": "Description 3"}, ] current_route: str = "/" selected_item_id: int = None
def go_to_detail(self, item_id: int): self.selected_item_id = item_id self.current_route = "/detail"
def go_back(self): self.selected_item_id = None self.current_route = "/"
def get_selected_item(self): return next((i for i in self.items if i["id"] == self.selected_item_id), None)
state = ItemsState()
@ft.componentdef ItemsList(): return ft.View( "/", [ ft.AppBar(title=ft.Text("Items")), ft.Column([ ft.ListTile( title=ft.Text(item["name"]), subtitle=ft.Text(item["description"]), on_click=lambda _, id=item["id"]: state.go_to_detail(id), trailing=ft.Icon(ft.Icons.ARROW_FORWARD_IOS), ) for item in state.items ]), ], )
@ft.componentdef ItemDetail(): item = state.get_selected_item()
if not item: return ft.View("/detail", [ft.Text("Item not found")])
return ft.View( "/detail", [ ft.AppBar( title=ft.Text("Item Detail"), leading=ft.IconButton(ft.Icons.ARROW_BACK, on_click=lambda _: state.go_back()), ), ft.Container( content=ft.Column([ ft.Text(f"ID: {item['id']}", size=16), ft.Text(item["name"], size=28, weight=ft.FontWeight.BOLD), ft.Text(item["description"], size=18), ], spacing=10), padding=30, ), ], )
@ft.componentdef App(): if state.current_route == "/detail": return ItemDetail() return ItemsList()
def main(page: ft.Page): page.title = "Route Params" page.add(App())
ft.run(main)🧭 NavigationBar (Bottom Navigation)
Phần tiêu đề “🧭 NavigationBar (Bottom Navigation)”import flet as ft
@ft.observableclass NavState: selected_index: int = 0
def set_index(self, index: int): self.selected_index = index
state = NavState()
@ft.componentdef ContentArea(): contents = ["🏠 Home Content", "🔍 Search Content", "👤 Profile Content"] return ft.Container( content=ft.Text(contents[state.selected_index], size=24), expand=True, alignment=ft.alignment.center, )
@ft.componentdef BottomNavApp(): return ft.Column([ ContentArea(), ft.NavigationBar( selected_index=state.selected_index, destinations=[ ft.NavigationBarDestination(icon=ft.Icons.HOME, label="Home"), ft.NavigationBarDestination(icon=ft.Icons.SEARCH, label="Search"), ft.NavigationBarDestination(icon=ft.Icons.PERSON, label="Profile"), ], on_change=lambda e: state.set_index(e.control.selected_index), ), ], expand=True)
def main(page: ft.Page): page.title = "Bottom Navigation" page.add(BottomNavApp())
ft.run(main)🚂 NavigationRail (Side Navigation)
Phần tiêu đề “🚂 NavigationRail (Side Navigation)”import flet as ft
@ft.observableclass RailState: selected_index: int = 0
state = RailState()
@ft.componentdef ContentPanel(): labels = ["Home", "Settings", "About"] return ft.Container( content=ft.Text(labels[state.selected_index], size=24), expand=True, alignment=ft.alignment.center, )
@ft.componentdef SideNavApp(): return ft.Row([ ft.NavigationRail( selected_index=state.selected_index, label_type=ft.NavigationRailLabelType.ALL, destinations=[ ft.NavigationRailDestination(icon=ft.Icons.HOME, label="Home"), ft.NavigationRailDestination(icon=ft.Icons.SETTINGS, label="Settings"), ft.NavigationRailDestination(icon=ft.Icons.INFO, label="About"), ], on_change=lambda e: setattr(state, 'selected_index', e.control.selected_index), ), ft.VerticalDivider(width=1), ContentPanel(), ], expand=True)
def main(page: ft.Page): page.title = "Navigation Rail" page.add(SideNavApp())
ft.run(main)📱 Tabs Navigation
Phần tiêu đề “📱 Tabs Navigation”import flet as ft
@ft.observableclass TabsState: selected_index: int = 0
state = TabsState()
@ft.componentdef TabContent(text: str, icon): return ft.Container( content=ft.Column([ ft.Icon(icon, size=48, color=ft.Colors.PRIMARY), ft.Text(text, size=20), ], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=10), padding=40, alignment=ft.alignment.center, )
@ft.componentdef TabsApp(): tabs_data = [ ("Home", ft.Icons.HOME, "Welcome to Home Tab"), ("Settings", ft.Icons.SETTINGS, "Configure your settings"), ("About", ft.Icons.INFO, "About this app"), ]
return ft.Tabs( selected_index=state.selected_index, on_change=lambda e: setattr(state, 'selected_index', e.control.selected_index), tabs=[ ft.Tab( text=name, icon=icon, content=TabContent(content, icon), ) for name, icon, content in tabs_data ], expand=True, )
def main(page: ft.Page): page.title = "Tabs Demo" page.add(TabsApp())
ft.run(main)📋 Tổng kết Navigation Types
Phần tiêu đề “📋 Tổng kết Navigation Types”| Type | Use Case |
|---|---|
| Custom Router + State | Multi-page apps, deep linking |
| NavigationBar | Mobile bottom nav |
| NavigationRail | Desktop side nav |
| Tabs | Content sections |
⏭️ Tiếp theo
Phần tiêu đề “⏭️ Tiếp theo”Tuyệt! Tiếp tục với Theming để học tùy chỉnh giao diện.