Navigation & Routing trong Flutter
1. Navigation & Routing

Navigation & Routing là cơ chế điều hướng giữa các màn hình trong ứng dụng Flutter. Cơ chế này cho phép người dùng di chuyển từ màn hình này sang màn hình khác, quay lại màn hình trước đó, truyền dữ liệu giữa các màn hình và kiểm soát luồng sử dụng ứng dụng.
Trong Flutter, mỗi màn hình thường được xây dựng bằng một Widget, chẳng hạn như HomePage, LoginPage, ProductDetailPage hoặc ProfilePage. Khi người dùng thực hiện một hành động như nhấn nút xem chi tiết sản phẩm, ứng dụng cần chuyển từ màn hình hiện tại sang màn hình tương ứng.
Navigation và Routing có liên quan chặt chẽ với nhau nhưng có ý nghĩa khác nhau:
Navigation (điều hướng) là quá trình chuyển đổi giữa các màn hình, bao gồm mở màn hình mới, quay lại màn hình trước đó hoặc thay thế màn hình hiện tại.
Routing (định tuyến) là cơ chế xác định màn hình tương ứng với một tuyến đường (route) hoặc một đường dẫn (path) trong ứng dụng.
Ví dụ, trong ứng dụng bán hàng, người dùng có thể đi theo luồng:
HomePage → ProductDetailPage → CartPage → CheckoutPage
Mỗi màn hình đảm nhận một chức năng riêng. Navigation quản lý việc chuyển đổi giữa các màn hình, còn Routing giúp xác định màn hình nào cần được mở theo tuyến đường được yêu cầu.
Navigation & Routing là kiến thức quan trọng khi phát triển ứng dụng nhiều màn hình như ứng dụng thương mại điện tử, ứng dụng quản lý phòng gym, ứng dụng ngân hàng hoặc ứng dụng mạng xã hội.
2. Các khái niệm chính
2.1. Route
Route là một tuyến điều hướng đại diện cho một màn hình hoặc một nhánh giao diện trong ứng dụng.
Trong Flutter, route có thể được biểu diễn bằng một đối tượng như MaterialPageRoute, CupertinoPageRoute hoặc bằng một đường dẫn được quản lý bởi hệ thống định tuyến.
Ví dụ:
MaterialPageRoute(
builder: (context) => const ProductDetailPage(),
);
Trong đoạn code trên, MaterialPageRoute tạo một tuyến điều hướng để hiển thị ProductDetailPage.
Route thường chứa giao diện cần hiển thị và có thể cung cấp hiệu ứng chuyển màn hình, quản lý vòng đời của trang cũng như hỗ trợ quay lại màn hình trước đó.
2.2. Navigator
Navigator là Widget quản lý các route theo cấu trúc ngăn xếp (stack). Nó cho phép ứng dụng mở màn hình mới, quay lại màn hình trước đó, thay thế màn hình hiện tại hoặc xóa nhiều màn hình khỏi ngăn xếp.
Ví dụ, khi ứng dụng có luồng điều hướng:
Home → Product Detail → Checkout
Ngăn xếp route có thể được hình dung như sau:
CheckoutPage -> ProductDetailPage -> HomePage
Khi gọi Navigator.pop(context), route ở trên cùng sẽ được lấy ra khỏi ngăn xếp và ứng dụng quay lại route trước đó nếu route đó còn tồn tại.
Navigator là thành phần cốt lõi của hệ thống điều hướng truyền thống trong Flutter.
2.3. BuildContext
BuildContext là đối tượng đại diện cho vị trí của một Widget trong cây Widget của Flutter.
Trong điều hướng, BuildContext thường được sử dụng để truy cập Navigator gần nhất và thực hiện các thao tác chuyển màn hình.
Ví dụ:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProfilePage(),
),
);
Ở đây, context giúp Flutter xác định hệ thống điều hướng cần sử dụng.
Cần lưu ý rằng BuildContext gắn với vị trí của Widget trong cây giao diện. Khi sử dụng context sau một thao tác bất đồng bộ như await, nên kiểm tra context.mounted trước khi thực hiện điều hướng nếu Widget có thể đã bị hủy.
2.4. Route Name và Path
Route Name là tên định danh của một tuyến đường, thường được sử dụng với cơ chế named routes truyền thống.
Ví dụ:
'/home'
'/products'
'/profile'
Path là đường dẫn xác định một vị trí giao diện trong ứng dụng. Các thư viện định tuyến hiện đại thường sử dụng path để hỗ trợ điều hướng rõ ràng hơn.
Ví dụ:
/
/products
/products/101
/cart
/profile
Trong ứng dụng bán hàng, /products/101 có thể đại diện cho trang chi tiết sản phẩm có mã 101.
Route name và path có thể trông giống nhau, nhưng không hoàn toàn đồng nghĩa trong mọi hệ thống định tuyến. Với các thư viện như GoRouter, path có thể chứa tham số động, query parameter và các quy tắc khớp đường dẫn.
3. Các phương thức Navigation cơ bản

Flutter cung cấp các phương thức điều hướng thông qua Navigator. Những phương thức thường dùng gồm push(), pop(), pushReplacement() và pushAndRemoveUntil().
3.1. Navigator.push()
Navigator.push() được sử dụng để mở một màn hình mới và đưa route mới lên đầu ngăn xếp.
Cú pháp:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
),
);
Ví dụ:
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProductDetailPage(),
),
);
},
child: const Text('Xem chi tiết'),
);
Khi người dùng nhấn nút, Flutter tạo route mới và hiển thị ProductDetailPage. Màn hình trước đó vẫn được giữ trong ngăn xếp, vì vậy người dùng có thể quay lại bằng thao tác pop.
Navigator.push() phù hợp khi mở trang chi tiết, trang cài đặt, trang giỏ hàng hoặc bất kỳ màn hình nào cần quay lại màn hình trước đó.
3.2. Navigator.pop()
Navigator.pop() được sử dụng để đóng route hiện tại và quay lại route trước đó trong ngăn xếp.
Ví dụ:
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Quay lại'),
);
Nếu người dùng đang ở ProductDetailPage và gọi Navigator.pop(context), ứng dụng sẽ quay lại màn hình trước đó nếu có route tương ứng.
Ngoài ra, Navigator.pop() có thể trả dữ liệu về màn hình gọi route thông qua đối số result.
Ví dụ:
Navigator.pop(context, 'Sản phẩm đã chọn');
Màn hình trước đó có thể nhận giá trị này từ kết quả của Navigator.push().
Cần tránh gọi pop() khi không có route phù hợp để đóng. Trong các tình huống không chắc chắn, có thể kiểm tra Navigator.canPop(context) trước khi thực hiện.
3.3. Navigator.pushReplacement()
Navigator.pushReplacement() được sử dụng để thay thế route hiện tại bằng một route mới.
Ví dụ:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => const HomePage(),
),
);
Trong trường hợp này, route hiện tại được thay thế bằng HomePage. Người dùng sẽ không quay lại route vừa bị thay thế bằng thao tác pop thông thường.
Phương thức này thường được sử dụng trong các trường hợp như:
-
Chuyển từ màn hình đăng nhập sang màn hình chính sau khi đăng nhập thành công.
-
Chuyển từ màn hình khởi động sang màn hình phù hợp.
-
Thay thế một bước trong quy trình mà người dùng không cần quay lại.
Tuy nhiên, việc sử dụng pushReplacement() không tự động bảo đảm rằng toàn bộ lịch sử điều hướng đã được xóa. Những route nằm bên dưới route hiện tại có thể vẫn tồn tại.
3.4. Navigator.pushAndRemoveUntil()
Navigator.pushAndRemoveUntil() được sử dụng để mở một route mới và loại bỏ những route trước đó cho đến khi đạt điều kiện được chỉ định.
Ví dụ:
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (context) => const HomePage(),
),
(route) => false,
);
Trong ví dụ trên, các route cũ trong ngăn xếp sẽ bị loại bỏ cho đến khi không còn route nào thỏa mãn điều kiện giữ lại. HomePage trở thành màn hình chính của luồng điều hướng mới.
Phương thức này hữu ích khi:
-
Người dùng đăng xuất và cần trở về màn hình đăng nhập.
-
Người dùng hoàn tất một quy trình và không cần quay lại các bước trước.
-
Ứng dụng muốn bắt đầu một luồng điều hướng mới.
-
Cần xóa lịch sử của một nhóm màn hình sau khi hoàn tất thao tác.
Điều kiện truyền vào quyết định route nào được giữ lại. Vì vậy, cần lựa chọn điều kiện phù hợp thay vì mặc định xóa toàn bộ lịch sử trong mọi trường hợp.
3.5. So sánh các phương thức
| Phương thức | Chức năng | Giữ route trước đó? |
|---|---|---|
push() |
Mở màn hình mới | Có |
pop() |
Đóng màn hình hiện tại | Loại bỏ route hiện tại |
pushReplacement() |
Thay thế màn hình hiện tại | Route bị thay thế không còn ở vị trí đó |
pushAndRemoveUntil() |
Mở màn hình mới và xóa route theo điều kiện | Tùy điều kiện |
4. Truyền dữ liệu giữa các màn hình
Trong ứng dụng thực tế, điều hướng không chỉ là chuyển đổi giao diện mà còn cần truyền dữ liệu giữa các màn hình.
Ví dụ, khi người dùng nhấn vào một sản phẩm trong danh sách, ứng dụng cần truyền thông tin sản phẩm sang màn hình chi tiết.
Flutter hỗ trợ truyền dữ liệu thông qua constructor của Widget, đối số route hoặc kết quả trả về từ route.
4.1. Truyền dữ liệu qua constructor
Đây là cách đơn giản và phổ biến khi đã biết dữ liệu cần truyền.
Ví dụ, màn hình chi tiết sản phẩm nhận một mã sản phẩm:
class ProductDetailPage extends StatelessWidget {
final int productId;
const ProductDetailPage({
super.key,
required this.productId,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Chi tiết sản phẩm'),
),
body: Center(
child: Text('Mã sản phẩm: $productId'),
),
);
}
}
Khi điều hướng:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ProductDetailPage(
productId: 101,
),
),
);
Màn hình chi tiết sẽ nhận giá trị 101 và hiển thị mã sản phẩm.
Cách này dễ hiểu, dễ kiểm thử và phù hợp với ứng dụng có cấu trúc màn hình rõ ràng.
4.2. Truyền dữ liệu bằng đối tượng
Thay vì truyền từng thuộc tính riêng lẻ, có thể truyền một đối tượng đại diện cho dữ liệu cần sử dụng.
Ví dụ:
class Product {
final int id;
final String name;
final double price;
const Product({
required this.id,
required this.name,
required this.price,
});
}
Màn hình chi tiết nhận đối tượng:
class ProductDetailPage extends StatelessWidget {
final Product product;
const ProductDetailPage({
super.key,
required this.product,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(product.name),
),
body: Center(
child: Text(
'Giá: ${product.price} VNĐ',
),
),
);
}
}
Khi mở màn hình:
final product = Product(
id: 101,
name: 'Laptop Gaming',
price: 25000000,
);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailPage(
product: product,
),
),
);
Cách này giúp truyền nhiều dữ liệu có liên quan một cách rõ ràng, đặc biệt khi ứng dụng có nhiều màn hình chi tiết.
4.3. Nhận dữ liệu trả về từ màn hình
Một màn hình có thể trả kết quả về màn hình trước đó. Ví dụ, người dùng chọn một phương thức thanh toán hoặc chọn một danh mục sản phẩm.
Màn hình mở route có thể chờ kết quả từ Navigator.push():
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => const SelectionPage(),
),
);
if (!context.mounted) return;
if (result != null) {
debugPrint('Giá trị nhận được: $result');
}
Trong màn hình lựa chọn, trả kết quả bằng:
Navigator.pop(context, 'Thanh toán bằng tiền mặt');
Ở ví dụ này, Navigator.push<String>() khai báo kiểu dữ liệu kết quả là String. Màn hình được mở trả kết quả thông qua Navigator.pop().
Việc sử dụng await giúp chương trình chờ đến khi route được đóng và nhận giá trị trả về.
5. Named Routes trong Flutter
Named Routes là cơ chế định tuyến truyền thống của Flutter, cho phép đặt tên cho từng tuyến đường và điều hướng bằng tên thay vì tạo MaterialPageRoute trực tiếp tại mỗi nơi.
Cách này giúp tập trung danh sách route và làm cho thao tác điều hướng ngắn gọn hơn.
5.1. Khai báo Named Routes
Ví dụ:
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/login': (context) => const LoginPage(),
'/profile': (context) => const ProfilePage(),
},
);
Trong đó:
-
initialRoutexác định tuyến đường khởi đầu. -
routesánh xạ tên route sang hàm tạo màn hình. -
Mỗi hàm nhận
BuildContextvà trả về Widget tương ứng.
5.2. Điều hướng bằng tên route
Sau khi khai báo route, có thể mở màn hình bằng Navigator.pushNamed():
Navigator.pushNamed(
context,
'/profile',
);
Để quay lại màn hình trước:
Navigator.pop(context);
Để thay thế màn hình hiện tại bằng một route có tên:
Navigator.pushReplacementNamed(
context,
'/login',
);
Named Routes giúp giảm việc viết lặp lại mã tạo route ở nhiều nơi.
5.3. Truyền dữ liệu qua Named Routes
Có thể sử dụng đối số arguments để truyền dữ liệu vào route.
Điều hướng:
Navigator.pushNamed(
context,
'/product-detail',
arguments: 101,
);
Nhận dữ liệu tại màn hình:
@override
Widget build(BuildContext context) {
final productId =
ModalRoute.of(context)!.settings.arguments as int;
return Scaffold(
body: Center(
child: Text('Mã sản phẩm: $productId'),
),
);
}
Trong ví dụ này, ModalRoute.of(context) truy cập thông tin route hiện tại, còn settings.arguments chứa dữ liệu được truyền vào.
Khi sử dụng cách này, cần bảo đảm kiểu dữ liệu nhận được đúng với dữ liệu truyền vào để tránh lỗi ép kiểu.
5.4. Hạn chế của Named Routes
Named Routes phù hợp với các ứng dụng nhỏ hoặc luồng điều hướng đơn giản. Tuy nhiên, tài liệu Flutter hiện đại không khuyến nghị sử dụng cơ chế này cho phần lớn ứng dụng có yêu cầu điều hướng phức tạp.
Một số hạn chế thường gặp:
-
Khó xử lý các đường dẫn động như
/products/101. -
Không cung cấp sẵn một hệ thống định tuyến đầy đủ cho deep linking phức tạp.
-
Khó tổ chức các luồng điều hướng lồng nhau trong ứng dụng lớn.
-
Cần thêm logic riêng khi xử lý quyền truy cập, chuyển hướng hoặc trạng thái đăng nhập.
-
Việc quản lý các tham số và kiểu dữ liệu có thể trở nên khó bảo trì khi số lượng màn hình tăng.
Với ứng dụng lớn, có thể cân nhắc thư viện như GoRouter để quản lý route tập trung và hỗ trợ các yêu cầu điều hướng nâng cao.
6. GoRouter trong Flutter
GoRouter là thư viện định tuyến dành cho Flutter, được xây dựng dựa trên cơ chế Router API. Thư viện này hỗ trợ khai báo đường dẫn, điều hướng theo URL, deep linking, route lồng nhau và chuyển hướng dựa trên điều kiện.
GoRouter đặc biệt hữu ích khi xây dựng ứng dụng có nhiều màn hình hoặc có yêu cầu về đường dẫn rõ ràng.
Cài đặt bằng lệnh:
flutter pub add go_router
Sau khi cài đặt, import thư viện:
import 'package:go_router/go_router.dart';
6.1. Khai báo GoRouter
Ví dụ:
final GoRouter router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfilePage(),
),
],
);
Mỗi GoRoute định nghĩa một đường dẫn và Widget tương ứng.
Ví dụ, khi đường dẫn là /profile, GoRouter hiển thị ProfilePage.
6.2. Kết nối GoRouter với MaterialApp.router
Khác với MaterialApp truyền thống, khi sử dụng GoRouter làm hệ thống định tuyến chính, ứng dụng thường sử dụng MaterialApp.router.
MaterialApp.router(
debugShowCheckedModeBanner: false,
routerConfig: router,
);
routerConfig nhận cấu hình GoRouter và sử dụng nó để quản lý các đường dẫn cũng như việc điều hướng.
6.3. Điều hướng bằng GoRouter
GoRouter cung cấp các phương thức thường dùng như go(), push() và pop().
Ví dụ:
context.go('/profile');
go() điều hướng đến một vị trí theo đường dẫn được chỉ định và thiết lập trạng thái điều hướng tương ứng với cấu hình router.
Mở một trang mới trên ngăn xếp điều hướng:
context.push('/profile');
Quay lại:
context.pop();
Điểm khác biệt cần nhớ là go() và push() không hoàn toàn giống nhau. go() phù hợp khi muốn điều hướng đến một vị trí trong ứng dụng, trong khi push() phù hợp với thao tác mở thêm một trang mới trên ngăn xếp điều hướng.
6.4. Route động và Path Parameters
Route động cho phép một đường dẫn đại diện cho nhiều dữ liệu khác nhau.
Ví dụ:
/products/101
/products/102
/products/103
Thay vì khai báo một route riêng cho từng sản phẩm, có thể khai báo:
GoRoute(
path: '/products/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductDetailPage(
productId: int.parse(id),
);
},
);
Trong đó:
-
:idlà tham số động trong đường dẫn. -
state.pathParameters['id']lấy giá trị của tham số. -
int.parse(id)chuyển chuỗi thành số nguyên. -
ProductDetailPagesử dụng mã sản phẩm để hiển thị thông tin tương ứng.
Khi điều hướng đến /products/101, màn hình sẽ nhận mã sản phẩm 101.
Trong dự án thực tế, cần xử lý trường hợp tham số không hợp lệ hoặc dữ liệu không tồn tại thay vì luôn ép kiểu trực tiếp.
6.5. Query Parameters
Query parameters là các tham số được truyền sau dấu ? trong URL. Chúng thường dùng cho tìm kiếm, lọc dữ liệu, phân trang hoặc sắp xếp.
Ví dụ:
/products?category=phone&page=2
Với GoRouter, có thể lấy các giá trị này thông qua state.uri.queryParameters:
GoRoute(
path: '/products',
builder: (context, state) {
final category =
state.uri.queryParameters['category'];
final page =
state.uri.queryParameters['page'];
return ProductListPage(
category: category,
page: int.tryParse(page ?? '') ?? 1,
);
},
);
Query parameters khác path parameters ở vị trí và mục đích sử dụng. Path parameters thường xác định một tài nguyên cụ thể, còn query parameters thường bổ sung điều kiện truy vấn hoặc tùy chọn hiển thị.
6.6. Redirect và kiểm tra đăng nhập
GoRouter hỗ trợ chuyển hướng dựa trên điều kiện thông qua thuộc tính redirect.
Ví dụ minh họa:
final GoRouter router = GoRouter(
initialLocation: '/',
redirect: (context, state) {
const isLoggedIn = false;
final isLoginPage = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginPage) {
return '/login';
}
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
],
);
Trong ví dụ này, biến isLoggedIn được cố định là false để minh họa. Khi chưa đăng nhập, người dùng sẽ được chuyển hướng sang trang đăng nhập.
Trong ứng dụng thật, trạng thái đăng nhập cần được lấy từ cơ chế quản lý phiên hoặc trạng thái xác thực của ứng dụng. Đồng thời, việc bảo vệ dữ liệu vẫn phải được thực hiện ở backend; điều hướng phía giao diện không thay thế cơ chế xác thực và phân quyền của máy chủ.
7. Deep Linking
Deep Linking là cơ chế cho phép mở trực tiếp một màn hình cụ thể trong ứng dụng thông qua một đường dẫn, thay vì bắt buộc người dùng phải mở ứng dụng rồi điều hướng từ màn hình chính.
Ví dụ:
myapp://products/101
Hoặc một liên kết web:
https://example.com/products/101
Khi deep linking được cấu hình phù hợp, người dùng có thể nhấn vào liên kết sản phẩm từ trình duyệt, email hoặc ứng dụng nhắn tin để mở trực tiếp trang chi tiết sản phẩm.
Deep linking thường được sử dụng trong:
-
Ứng dụng thương mại điện tử để mở sản phẩm cụ thể.
-
Ứng dụng mạng xã hội để mở bài viết hoặc hồ sơ.
-
Ứng dụng đặt lịch để mở thông tin lịch hẹn.
-
Ứng dụng quản lý để mở trực tiếp một bản ghi.
-
Chiến dịch quảng cáo hoặc thông báo có liên kết đến nội dung cụ thể.
Flutter hỗ trợ nền tảng cho deep linking, nhưng ứng dụng vẫn cần cấu hình phù hợp cho Android và iOS. Nếu sử dụng liên kết HTTPS, có thể cần cấu hình Android App Links hoặc iOS Universal Links. Với hệ thống điều hướng phức tạp, GoRouter có thể giúp xử lý các đường dẫn và ánh xạ chúng đến màn hình tương ứng.
8. Nested Navigation
Nested Navigation (điều hướng lồng nhau) là kỹ thuật sử dụng nhiều hệ thống điều hướng trong cùng một ứng dụng.
Kỹ thuật này thường được sử dụng khi ứng dụng có nhiều khu vực độc lập, chẳng hạn như Trang chủ, Tìm kiếm, Giỏ hàng và Tài khoản. Mỗi khu vực có thể có lịch sử điều hướng riêng.
Ví dụ, người dùng có thể đi từ Trang chủ đến Chi tiết sản phẩm, sau đó chuyển sang tab Tài khoản. Khi quay lại tab Trang chủ, ứng dụng có thể giữ nguyên vị trí trang Chi tiết sản phẩm thay vì trở về màn hình chính.
Nested Navigation giúp:
-
Tách biệt luồng điều hướng của từng khu vực.
-
Duy trì trạng thái và lịch sử của từng tab.
-
Tổ chức ứng dụng có nhiều màn hình tốt hơn.
-
Quản lý luồng điều hướng độc lập trong các phần của ứng dụng.
Trong Flutter, có thể xây dựng điều hướng lồng nhau bằng nhiều Navigator hoặc sử dụng các tính năng như ShellRoute và StatefulShellRoute của GoRouter.
Đối với ứng dụng lớn, việc tổ chức điều hướng cần được thiết kế từ đầu để tránh tạo ra quá nhiều hệ thống route phức tạp hoặc khó kiểm soát.
9. Quy tắc và nguyên tắc khi thiết kế Navigation & Routing
Khi xây dựng hệ thống điều hướng, cần tuân thủ một số nguyên tắc để bảo đảm tính nhất quán và dễ bảo trì.
Thứ nhất, xác định rõ luồng điều hướng. Mỗi màn hình cần có mục đích cụ thể và xác định được cách người dùng đi đến cũng như rời khỏi màn hình đó.
Thứ hai, lựa chọn phương thức điều hướng phù hợp. Dùng push() khi cần mở thêm màn hình, pop() khi cần quay lại, pushReplacement() khi muốn thay thế route hiện tại và pushAndRemoveUntil() khi cần xóa route theo điều kiện.
Thứ ba, quản lý việc truyền dữ liệu rõ ràng. Ưu tiên constructor hoặc các cơ chế tham số có kiểu dữ liệu rõ ràng. Với các luồng điều hướng phức tạp, cần quy định rõ dữ liệu đầu vào và đầu ra của từng màn hình.
Thứ tư, xử lý trạng thái đăng nhập. Những màn hình yêu cầu xác thực cần được kiểm soát bằng logic điều hướng phù hợp, đồng thời backend phải kiểm tra quyền truy cập thực tế.
Thứ năm, xử lý lỗi và dữ liệu không hợp lệ. Cần có phương án khi người dùng truy cập đường dẫn không tồn tại, truyền tham số sai hoặc yêu cầu một tài nguyên không còn tồn tại.
Thứ sáu, chú ý nút Back. Hành vi quay lại cần nhất quán giữa nút Back trên giao diện, nút Back của hệ điều hành và lịch sử điều hướng của ứng dụng.
Thứ bảy, tổ chức route tập trung khi dự án lớn. Việc tập trung cấu hình route giúp giảm trùng lặp, dễ kiểm thử và thuận tiện khi thêm màn hình mới.
Thứ tám, xem xét deep linking và khả năng mở rộng. Nếu ứng dụng có liên kết ngoài, thông báo đẩy hoặc nhiều luồng điều hướng, cần thiết kế route để hỗ trợ các trường hợp này ngay từ đầu.
10. Các lỗi thường gặp
10.1. Gọi Navigator.pop() khi không có route để quay lại
Nếu không có route phù hợp trong ngăn xếp, việc gọi pop() có thể không tạo ra kết quả như mong muốn.
Có thể kiểm tra trước:
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
10.2. Truyền sai kiểu dữ liệu
Khi sử dụng ModalRoute.of(context)!.settings.arguments, nếu dữ liệu thực tế khác với kiểu được ép, ứng dụng có thể gặp lỗi runtime.
Nên kiểm tra dữ liệu hoặc sử dụng cách truyền qua constructor có kiểu dữ liệu rõ ràng khi phù hợp.
10.3. Sử dụng context sau khi Widget đã bị hủy
Nếu chờ một thao tác bất đồng bộ rồi điều hướng, Widget có thể đã bị loại khỏi cây giao diện.
Ví dụ:
await someAsyncOperation();
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const HomePage(),
),
);
Kiểm tra context.mounted giúp tránh sử dụng BuildContext sau khi Widget không còn tồn tại.
10.4. Nhầm lẫn giữa push() và pushReplacement()
push() giữ route trước đó trong ngăn xếp, trong khi pushReplacement() thay thế route hiện tại.
Sử dụng sai phương thức có thể khiến người dùng quay lại màn hình không mong muốn hoặc không thể quay lại theo luồng dự kiến.
10.5. Không xử lý đường dẫn không tồn tại
Với các ứng dụng sử dụng URL hoặc deep linking, cần có phương án hiển thị màn hình không tìm thấy hoặc chuyển hướng hợp lý khi đường dẫn không khớp route nào.
Điều này giúp ứng dụng tránh hiển thị lỗi khó hiểu khi người dùng mở một liên kết cũ hoặc nhập URL không hợp lệ.
11. Nên sử dụng phương pháp nào trong dự án Flutter
| Loại dự án | Hướng tiếp cận phù hợp |
|---|---|
| Ứng dụng nhỏ, ít màn hình | Navigator.push() và Navigator.pop() |
| Ứng dụng có một số route cố định | Navigator kết hợp cấu hình route đơn giản |
| Ứng dụng nhiều màn hình, nhiều luồng | Cân nhắc GoRouter |
| Ứng dụng cần deep linking | Router API hoặc GoRouter |
| Ứng dụng có nhiều tab và lịch sử riêng | Nested Navigation hoặc StatefulShellRoute |
| Ứng dụng cần truyền dữ liệu đơn giản | Constructor hoặc kết quả trả về từ route |
| Ứng dụng cần điều hướng theo trạng thái đăng nhập | Redirect kết hợp quản lý trạng thái xác thực |
Với dự án Flutter mới có nhiều màn hình, route động hoặc yêu cầu deep linking, GoRouter là lựa chọn đáng cân nhắc. Tuy nhiên, không phải ứng dụng nào cũng cần thư viện định tuyến bổ sung; với ứng dụng nhỏ, Navigator cơ bản vẫn có thể đáp ứng tốt.
12. Kết luận
Navigation & Routing là cơ chế quản lý việc chuyển đổi giữa các màn hình trong Flutter. Navigation tập trung vào thao tác mở, đóng, thay thế và quản lý lịch sử màn hình. Routing tập trung vào việc xác định màn hình tương ứng với route hoặc đường dẫn.
Các thành phần quan trọng gồm Navigator, Route, BuildContext, route name và path. Những phương thức cơ bản là push(), pop(), pushReplacement() và pushAndRemoveUntil().
Flutter còn hỗ trợ truyền dữ liệu giữa màn hình, Named Routes và Router API. Đối với ứng dụng có yêu cầu điều hướng nâng cao, GoRouter cung cấp những tính năng hữu ích như route động, query parameters, redirect, deep linking và điều hướng lồng nhau.
Nắm vững Navigation & Routing giúp lập trình viên xây dựng luồng sử dụng rõ ràng, quản lý lịch sử màn hình chính xác, truyền dữ liệu an toàn hơn và tổ chức ứng dụng Flutter dễ mở rộng, dễ bảo trì.








Khoá học lập trình game con rắn cho trẻ em




