- Phân biệt Navigator 1.0 (imperative) vs Navigator 2.0 (declarative).
- Sử dụng
Navigator.push/pop/pushNamed/pushReplacement/pushAndRemoveUntil. - Pass data giữa screen qua constructor (push) và
Navigator.pop(result). - Named routes setup.
go_routerpackage — recommend cho production app.- Deep linking + URL strategies cho Web.
- Bottom navigation, drawer, tab bar.
- Modal: dialog, bottom sheet, snackbar.
1. Navigator 1.0 vs 2.0 — bức tranh chung
| Navigator 1.0 | Navigator 2.0 | |
|---|---|---|
| Style | Imperative — push/pop stack | Declarative — UI = f(URL state) |
| Learning curve | Thấp | Cao (raw); medium qua wrapper |
| Web URL sync | Không | Có |
| Deep link | Custom code | Built-in |
| Recommend | Project nhỏ, prototype | Production app, đặc biệt có Web |
2. Navigator 1.0 — push, pop
Navigator.push(
context,
MaterialPageRoute(
builder: (ctx) => DetailsPage(item: myItem),
),
);
// Bên trong DetailsPage
Navigator.pop(context); // quay lại
Navigator.pop(context, 'result'); // pop kèm value
Caller nhận result:
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (ctx) => const EditPage()),
);
if (result != null) {
print('Got: $result');
}
3. pushReplacement, pushAndRemoveUntil
// Replace top route — pattern sau login
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (ctx) => const HomePage()),
);
// Pop till predicate match, push mới — pattern logout
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (ctx) => const LoginPage()),
(route) => false, // pop tất cả
);
// Pop tới named route
Navigator.popUntil(context, ModalRoute.withName('/home'));
4. Named routes Navigator 1.0
MaterialApp(
initialRoute: '/',
routes: {
'/': (ctx) => const HomePage(),
'/login': (ctx) => const LoginPage(),
'/profile': (ctx) => const ProfilePage(),
},
)
// Navigate
Navigator.pushNamed(context, '/profile');
// Pass data qua arguments
Navigator.pushNamed(context, '/details', arguments: myItem);
// Đọc args trong page
final args = ModalRoute.of(context)!.settings.arguments as Item;
onGenerateRoute — dynamic route
MaterialApp(
onGenerateRoute: (settings) {
final uri = Uri.parse(settings.name ?? '/');
if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'user') {
final id = int.parse(uri.pathSegments[1]);
return MaterialPageRoute(builder: (_) => UserPage(id: id));
}
return null; // fallback xuống onUnknownRoute
},
onUnknownRoute: (settings) => MaterialPageRoute(builder: (_) => const NotFoundPage()),
)
5. Pitfall Navigator 1.0
- Không tự sync URL Web — URL bar không update khi navigate.
- Deep link cần custom logic (handle
FlutterDeepLinkApi). - Tab navigation phức tạp — mỗi tab cần Navigator riêng.
- State recovery (app restore từ background) chỉ work cơ bản.
6. go_router — chuẩn industry
flutter pub add go_router
import 'package:go_router/go_router.dart';
final _router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/user/:id', // path param
builder: (context, state) {
final id = state.pathParameters['id']!;
return UserPage(id: int.parse(id));
},
),
GoRoute(
path: '/search',
builder: (context, state) {
final q = state.uri.queryParameters['q'] ?? '';
return SearchPage(query: q);
},
),
],
errorBuilder: (context, state) => NotFoundPage(error: state.error),
);
MaterialApp.router(
routerConfig: _router,
theme: ThemeData(/*...*/),
)
Navigate:
context.go('/user/42'); // replace stack
context.push('/user/42'); // push lên stack
context.pop();
context.go('/search?q=flutter');
// Type-safe param (cần build_runner — package go_router_builder)
UserRoute(id: 42).go(context);
Redirect — auth flow
GoRouter(
routes: [/*...*/],
redirect: (context, state) {
final loggedIn = AuthService.instance.isLoggedIn;
final goingToLogin = state.matchedLocation == '/login';
if (!loggedIn && !goingToLogin) return '/login';
if (loggedIn && goingToLogin) return '/';
return null; // không redirect
},
)
ShellRoute — persistent shell (vd bottom nav)
GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) => Scaffold(
body: child,
bottomNavigationBar: MyBottomNav(currentIndex: _indexFor(state.uri.path)),
),
routes: [
GoRoute(path: '/home', builder: (_, __) => const HomeTab()),
GoRoute(path: '/profile', builder: (_, __) => const ProfileTab()),
GoRoute(path: '/settings', builder: (_, __) => const SettingsTab()),
],
),
GoRoute(path: '/login', builder: (_, __) => const LoginPage()), // ngoài shell
],
)
7. Web URL strategy
import 'package:flutter_web_plugins/url_strategy.dart';
void main() {
usePathUrlStrategy(); // bỏ # khỏi URL Web
runApp(const MyApp());
}
// URL Web sẽ là /user/42 thay vì /#/user/42
// Yêu cầu server redirect 404 về index.html
8. Bottom navigation
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _index = 0;
final _pages = const [
HomeTab(),
SearchTab(),
ProfileTab(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack( // giữ state mỗi tab
index: _index,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (i) => setState(() => _index = i),
destinations: const [
NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
NavigationDestination(icon: Icon(Icons.search), label: 'Search'),
NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
}
IndexedStack vs swap widget
IndexedStack giữ tất cả tab trong tree, hide non-active. Pros: giữ state, scroll position. Cons: tốn memory hơn.
Swap widget (return _pages[_index]): tab inactive bị dispose, state reset khi switch lại.
9. Drawer & EndDrawer
Scaffold(
appBar: AppBar(title: const Text('My App')),
drawer: Drawer(
child: ListView(
children: [
const DrawerHeader(
decoration: BoxDecoration(color: Colors.blue),
child: Text('Menu', style: TextStyle(color: Colors.white, fontSize: 24)),
),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Home'),
onTap: () {
Navigator.pop(context); // đóng drawer
context.go('/');
},
),
],
),
),
body: const Center(child: Text('Body')),
)
10. TabBar + TabBarView
DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('Tabs'),
bottom: const TabBar(
tabs: [
Tab(icon: Icon(Icons.cloud)),
Tab(icon: Icon(Icons.beach_access)),
Tab(icon: Icon(Icons.brightness_5)),
],
),
),
body: const TabBarView(
children: [CloudTab(), BeachTab(), SunTab()],
),
),
)
11. Modal — Dialog
final result = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Xác nhận'),
content: const Text('Xóa item này?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Hủy'),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Xóa'),
),
],
),
);
if (result == true) {
_delete();
}
12. Modal — Bottom sheet
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) => Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom),
child: Container(
padding: const EdgeInsets.all(16),
child: const Text('Bottom sheet content'),
),
),
);
13. SnackBar
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Saved successfully'),
duration: Duration(seconds: 3),
),
);
// Với action
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('Item deleted'),
action: SnackBarAction(label: 'Undo', onPressed: _undo),
),
);
ScaffoldMessenger chứ không Scaffold.of
Pre-Flutter 1.22 dùng Scaffold.of(context).showSnackBar(...) — context phải descendant của Scaffold.
Modern dùng ScaffoldMessenger.of(context) — ancestor là MaterialApp, không cần Scaffold parent ngay.
14. Bài tập
3-screen app với Navigator 1.0 named routes
Build app 3 screen: Home → List → Detail. Dùng named routes. Pass item từ List → Detail qua arguments.
💡 Gợi ý đáp án
MaterialApp(
initialRoute: '/',
routes: {
'/': (_) => const HomePage(),
'/list': (_) => const ListPage(),
'/detail': (ctx) {
final item = ModalRoute.of(ctx)!.settings.arguments as String;
return DetailPage(item: item);
},
},
)
// List → Detail
Navigator.pushNamed(context, '/detail', arguments: 'item-1');
Migrate sang go_router
Migrate bài 1 sang go_router. Path: /, /list, /list/:id.
URL Web hiển thị đúng /list/42 khi xem item id=42.
💡 Gợi ý đáp án
final router = GoRouter(routes: [
GoRoute(path: '/', builder: (_, __) => const HomePage()),
GoRoute(path: '/list', builder: (_, __) => const ListPage()),
GoRoute(
path: '/list/:id',
builder: (_, state) => DetailPage(id: state.pathParameters['id']!),
),
]);
// Navigate
context.push('/list/42');
ShellRoute bottom nav
Add ShellRoute với bottom nav 3 tab (Home, Profile, Settings). Mỗi tab có nested screen — switch tab giữ history.
💡 Gợi ý đáp án
Tham khảo section 6 (ShellRoute). Để giữ state mỗi tab, dùng StatefulShellRoute thay ShellRoute thường.
Auth redirect flow
Implement: chưa login → redirect /login. Login xong → /home. Logout → clear, redirect /login.
💡 Gợi ý đáp án
Tham khảo section 6 (redirect). Auth state cần ValueNotifier để go_router rebuild khi auth đổi.
Modal bottom sheet form
Show modal bottom sheet với form input (1 TextField + Submit button). Return data via pop. Caller hiển thị data trong snackbar.
💡 Gợi ý đáp án
final result = await showModalBottomSheet<String>(
context: context,
isScrollControlled: true,
builder: (ctx) {
final ctrl = TextEditingController();
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: TextField(controller: ctrl),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, ctrl.text),
child: const Text('Submit'),
),
],
),
);
},
);
if (result != null && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(result)));
}
15. Quiz
Navigator.push return type?
Xem đáp án
Đáp án: Future<T?> — complete khi route pop. T là type result Navigator.pop(result).
Pass data qua named route — dùng property nào?
Xem đáp án
Đáp án: Navigator.pushNamed(context, '/x', arguments: data). Đọc: ModalRoute.of(context)!.settings.arguments.
pushReplacement khác pushAndRemoveUntil?
Xem đáp án
Đáp án: pushReplacement chỉ replace top. pushAndRemoveUntil pop nhiều route cho đến predicate match, sau đó push mới.
go_router path /user/:id — đọc id thế nào?
Xem đáp án
Đáp án: state.pathParameters['id'] trong builder. Query param: state.uri.queryParameters['q'].
usePathUrlStrategy() bỏ gì?
Xem đáp án
Đáp án: Bỏ #/ (hash routing) khỏi URL Web. Yêu cầu server config redirect 404 về index.html.
ShellRoute vs nested GoRoute?
Xem đáp án
Đáp án: ShellRoute = persistent shell widget (vd bottom nav) bao quanh inner route. Nested GoRoute = full screen swap không có persistent shell.
ScaffoldMessenger vs Scaffold.of cũ?
Xem đáp án
Đáp án: ScaffoldMessenger là pattern mới (Flutter 1.22+). Không phải descendant Scaffold ngay; snackbar persist qua navigation.
Modal bottom sheet scrollable — property nào?
Xem đáp án
Đáp án: isScrollControlled: true + dùng DraggableScrollableSheet cho behavior drag.
16. Tổng kết
- ✅ Navigator 1.0: imperative push/pop stack. Đủ cho project nhỏ.
- ✅ Named routes + onGenerateRoute cho dynamic.
- ✅ Navigator 1.0 hạn chế: URL Web, deep link, tab navigation.
- ✅
go_router= chuẩn industry. Declarative, URL-first. - ✅ Redirect — auth flow.
- ✅ ShellRoute — persistent shell (bottom nav).
- ✅ Web URL strategy:
usePathUrlStrategy. - ✅ Bottom nav với
IndexedStackđể giữ state. - ✅ Drawer, TabBar.
- ✅ Modal: dialog, bottom sheet, snackbar (ScaffoldMessenger).
17. Kết nối
- Chương 6+: state management consistent across navigation.
- Chương 11 — Local Storage: persistent auth state.
- Chương 14 — Testing: test navigation flow.