Điều hướng trong Flutter: Navigator cơ bản và go_router

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Ứng dụng nào cũng có nhiều màn hình, và người dùng cần di chuyển qua lại giữa chúng một cách tự nhiên. Flutter cung cấp Navigator sẵn có, còn go_router là gói điều hướng khai báo được nhiều người ưa chuộng. Bài này giúp bạn hiểu cả hai và biết khi nào nên dùng.

Navigator: mô hình ngăn xếp

Navigator quản lý các màn hình như một chồng (stack). Mở màn hình mới là đẩy (push) lên trên cùng, quay lại là lấy (pop) màn hình trên cùng ra. Cách dùng cơ bản:

// Mở màn hình chi tiết
Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => const DetailPage()),
);

// Quay lại
Navigator.pop(context);

Truyền dữ liệu giữa các màn hình

Cách đơn giản nhất là truyền qua constructor của màn hình đích:

class DetailPage extends StatelessWidget {
  final String productId;
  const DetailPage({super.key, required this.productId});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sản phẩm $productId')),
    );
  }
}

Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => const DetailPage(productId: '42')),
);

Để nhận kết quả trả về, màn hình đích gọi Navigator.pop(context, giaTri) và nơi mở chờ bằng await Navigator.push(...).

Named routes và giới hạn

Bạn có thể đặt tên cho route trong MaterialApp rồi gọi bằng Navigator.pushNamed. Cách này gọn với ứng dụng nhỏ, nhưng khi cần đường dẫn có tham số, kiểm tra đăng nhập hoặc liên kết sâu từ bên ngoài thì phần cấu hình trở nên rườm rà. Đó là lý do nhiều dự án chuyển sang go_router.

go_router: điều hướng theo đường dẫn

go_router cho phép bạn khai báo toàn bộ tuyến đường tập trung, dùng đường dẫn giống URL. Cài đặt bằng flutter pub add go_router.

final _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomePage(),
      routes: [
        GoRoute(
          path: 'product/:id',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return DetailPage(productId: id);
          },
        ),
      ],
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) => const LoginPage(),
    ),
  ],
);

// Trong MaterialApp
MaterialApp.router(routerConfig: _router);

// Điều hướng
context.go('/product/42');
context.push('/login');

Khác biệt cần nhớ: go thay thế vị trí hiện tại theo đường dẫn mới, còn push thêm một màn hình lên ngăn xếp giống Navigator truyền thống.

Chuyển hướng theo trạng thái đăng nhập

Một tình huống rất phổ biến là chặn người chưa đăng nhập. Với go_router bạn dùng tham số redirect:

GoRouter(
  redirect: (context, state) {
    final loggedIn = authState.isLoggedIn;
    final goingToLogin = state.matchedLocation == '/login';
    if (!loggedIn && !goingToLogin) return '/login';
    if (loggedIn && goingToLogin) return '/';
    return null;
  },
  routes: [ /* ... */ ],
);

Trả về null nghĩa là cho phép đi tiếp đến đích ban đầu.

Khi nào chọn cái nào?

  • Navigator trực tiếp: ứng dụng nhỏ, ít màn hình, bài tập học tập.
  • go_router: ứng dụng có nhiều tuyến, cần đường dẫn có tham số, liên kết sâu, kiểm tra quyền hoặc bản web.

Lỗi thường gặp

  1. Dùng context đã hết hiệu lực sau await. Hãy kiểm tra mounted trước khi điều hướng.
  2. Lồng quá nhiều Navigator khiến nút Back hoạt động khó đoán.
  3. Quên xử lý trường hợp đường dẫn không tồn tại; nên cấu hình trang báo lỗi riêng.

Kết luận

Bắt đầu bằng Navigator để hiểu nguyên lý ngăn xếp, sau đó chuyển sang go_router khi dự án cần cấu trúc rõ hơn. Điều quan trọng là giữ cách điều hướng nhất quán trong toàn ứng dụng.

Điều hướng lồng nhau với thanh tab

Ứng dụng thật thường có thanh tab ở dưới, mỗi tab giữ ngăn xếp màn hình riêng. Với go_router, dùng StatefulShellRoute để làm điều này mà vẫn giữ trạng thái từng tab khi chuyển qua lại:

StatefulShellRoute.indexedStack(
  builder: (context, state, navigationShell) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        selectedIndex: navigationShell.currentIndex,
        onDestinationSelected: (i) => navigationShell.goBranch(i),
        destinations: const [
          NavigationDestination(icon: Icon(Icons.home), label: 'Trang chủ'),
          NavigationDestination(icon: Icon(Icons.person), label: 'Cá nhân'),
        ],
      ),
    );
  },
  branches: [
    StatefulShellBranch(routes: [
      GoRoute(path: '/', builder: (_, __) => const HomePage()),
    ]),
    StatefulShellBranch(routes: [
      GoRoute(path: '/profile', builder: (_, __) => const ProfilePage()),
    ]),
  ],
)

Mỗi nhánh (branch) có ngăn xếp độc lập, nên người dùng đang xem sâu trong tab Trang chủ, chuyển sang tab Cá nhân rồi quay lại vẫn thấy đúng vị trí cũ. Cú pháp của gói có thể thay đổi giữa các phiên bản, vì vậy hãy đối chiếu với tài liệu của phiên bản bạn dùng.

Hiệu ứng chuyển trang và nút Back

Mặc định Flutter dùng hiệu ứng chuyển trang theo nền tảng: trượt sang ngang trên iOS, mờ và trượt lên trên Android. Bạn có thể tùy biến bằng PageRouteBuilder hoặc thuộc tính pageBuilder của go_router, nhưng hãy cân nhắc vì người dùng quen với hành vi hệ điều hành của họ. Hiệu ứng tùy biến chỉ nên dùng khi có lý do rõ ràng.

Về nút Back, Android có cử chỉ và nút quay lại của hệ thống. Nếu màn hình có dữ liệu nhập dở, hãy dùng PopScope để hỏi người dùng có muốn thoát không thay vì mất dữ liệu im lặng. Đồng thời kiểm tra luồng đăng nhập: sau khi đăng nhập xong nên thay thế màn hình đăng nhập chứ không đẩy thêm, để bấm Back không quay về form đăng nhập.

Muốn thực hành điều hướng trong một dự án đầy đủ chức năng, bạn có thể tìm khóa học phù hợp trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment