Hướng Nghiệp Dữ Liệu

Navigation 1.0 vs Navigation 2.0: nên dùng cái nào?

Được viết bởi Admin|0 lượt xem
Navigation 1.0 vs Navigation 2.0: nên dùng cái nào?

Tóm tắt nhanh

So sánh chi tiết Navigation 1.0 và Navigation 2.0 trong Flutter, ưu nhược điểm và khi nào nên dùng giải pháp nào.

Mục lục bài viết (42)

Flutter có 2 cách navigation: Navigation 1.0 (cũ) và Navigation 2.0 (mới). Bài viết này sẽ giúp bạn chọn đúng.

Đặc điểm:

  • ✅ Đơn giản, dễ sử dụng
  • ✅ Phù hợp cho apps đơn giản
  • ✅ Declarative routing
  • ❌ Khó quản lý deep links
  • ❌ Khó quản lý navigation stack

Cách sử dụng:

[object Object]

Named Routes:

[object Object]

Đặc điểm:

  • ✅ URL-based routing
  • ✅ Deep linking dễ dàng
  • ✅ Quản lý navigation stack tốt
  • ✅ Phù hợp cho web
  • ❌ Phức tạp hơn
  • ❌ Learning curve cao

Sử dụng go_router:

[object Object]

So sánh chi tiết

Tiêu chí Navigation 1.0 Navigation 2.0
Simplicity Đơn giản Phức tạp
Deep Links Khó Dễ
URL Support Không Có
Web Support Hạn chế Tốt
Learning Curve Dễ Khó

Khi nào dùng Navigation 1.0?

✅ Nên dùng khi:

  • App đơn giản
  • Không cần deep links
  • Không cần web support
  • Team mới học Flutter

Khi nào dùng Navigation 2.0?

✅ Nên dùng khi:

  • App phức tạp
  • Cần deep links
  • Cần web support
  • Cần URL-based routing

Điểm khác biệt gốc: mệnh lệnh so với khai báo

Hiểu đúng một câu này là hiểu cả hai phiên bản. Navigator 1.0 là cách tiếp cận mệnh lệnh (imperative): bạn ra lệnh "đẩy màn hình này lên", "bỏ màn hình kia đi". Stack nằm trong tay Navigator, và code của bạn là người điều khiển nó.

Navigator 2.0 là cách tiếp cận khai báo (declarative): bạn nói "với trạng thái hiện tại, ứng dụng nên hiển thị những màn hình này", và Flutter tự dựng stack từ danh sách đó.

Điểm mấu chốt về mặt hệ quả: khi URL là nguồn sự thật, việc quay lại bằng nút Back của trình duyệt hay mở app từ một đường dẫn deep link không còn là trường hợp đặc biệt nữa — nó chỉ là một trạng thái khác của cùng một cơ chế.

Bốn thành phần của Router API cần biết, dù bạn gần như không viết tay chúng khi dùng go_router:

  • RouterDelegate: dựng và cập nhật stack màn hình theo trạng thái hiện tại.
  • RouteInformationParser: dịch URL thành đối tượng cấu hình route và ngược lại.
  • RouteInformationProvider: nguồn phát thông tin URL (khởi động app, trình duyệt, deep link).
  • BackButtonDispatcher: nhận sự kiện nút Back của hệ điều hành.

Trên thực tế, gần như toàn bộ dự án dùng go_router để không phải tự viết bốn lớp này.

GoRouter thực chiến: route lồng nhau, ShellRoute và tham số

Ví dụ ở phần trên chỉ là mức tối thiểu. Đây là cấu trúc gần với dự án thật hơn.

[object Object]

Hai điểm đáng nói:

  • Route lồng nhau giữ nguyên stack cha. Khi vào /khoa-hoc/flutter/dang-ky, nút Back sẽ quay về /khoa-hoc/flutter thay vì nhảy về trang chủ.
  • ShellRoute giữ phần khung chung (thanh điều hướng dưới, sidebar) không bị dựng lại khi đổi giữa các tab — đúng cách cho giao diện nhiều tab.

Đọc tham số truy vấn cũng cần chú ý kiểu dữ liệu, vì mọi thứ từ URL đều là chuỗi:

[object Object]

Bảo vệ route: chuyển hướng khi chưa đăng nhập

Đây là lý do phổ biến nhất khiến người mới phải chuyển sang go_router. Bạn cần một chỗ duy nhất quyết định có cho vào màn hình hay không, thay vì rải kiểm tra ở từng nút bấm.

[object Object]

Điểm quan trọng: refreshListenable giúp router tự đánh giá lại quy tắc khi trạng thái đăng nhập đổi, nên bạn không phải gọi go() bằng tay sau khi đăng nhập thành công. Luồng xác thực và lưu token được trình bày ở bài xác thực với Firebase.

Các lỗi thường gặp khi chuyển sang go_router

  1. Vẫn dùng Navigator.push lẫn với context.go. Hai cơ chế chạy song song làm stack lệch, và nút Back hoạt động không như mong đợi. Hãy thống nhất một cách, thường là context.go / context.push của go_router.
  2. Nhầm context.go và context.push. go thay vị trí hiện tại trong URL, push chồng thêm một màn hình lên trên. Hiểu sai chỗ này dẫn tới mất nút Back đúng nghĩa.
  3. Dùng context sau await trong redirect hoặc trong trang. Widget có thể đã bị xoá; cần kiểm tra mounted trước khi điều hướng tiếp.
  4. Quên errorBuilder. Deep link sai sẽ cho màn hình trắng, không có cách nào để người dùng thoát ra.
  5. Không gọi GoRouter.dispose() khi vòng đời app kết thúc trong môi trường có nhiều app instance (ví dụ test), gây rò rỉ.
  6. Đặt tên route không có name. Khi dự án lớn dần, chuỗi đường dẫn hard-code rải khắp nơi rất dễ sai sau khi đổi URL; đặt name cho route và điều hướng theo tên.

Có nên chuyển dự án đang dùng Navigation 1.0 sang 2.0?

Câu trả lời thực dụng: không phải lúc nào cũng nên, và không nên làm cùng lúc với việc thêm tính năng mới.

Nên chuyển khi có ít nhất một trong các dấu hiệu: bạn cần deep link dùng được thật, sản phẩm có bản web cần URL riêng cho từng trang, hoặc luồng đăng nhập/phân quyền phải kiểm tra tập trung. Không nên chuyển chỉ vì "2.0 là bản mới": nếu app ba màn hình, không có deep link, không có web, thì đổi sang router chỉ mang thêm chi phí bảo trì.

Nếu đã quyết định chuyển, hãy làm theo thứ tự: dựng router và route cho các màn hình hiện có → thay dần các chỗ điều hướng → xử lý redirect → mới thêm deep link. Đổi từng bước giúp bạn luôn có bản chạy được để so sánh.

Câu hỏi thường gặp

GoRouter có thay thế được Navigator hoàn toàn không?

Trong hầu hết trường hợp, có. Nhưng Navigator vẫn cần cho các trường hợp đặc biệt như mở hộp thoại toàn màn hình, hoặc hiển thị luồng phụ như màn hình chọn ảnh — những thứ không nên trở thành một phần của URL.

Không. Router API chỉ quản lý trạng thái điều hướng, chi phí không đáng kể so với việc dựng giao diện. Vấn đề hiệu năng thường đến từ chỗ khác, ví dụ danh sách dài không dùng builder.

Khi nào cần ShellRoute, khi nào không?

Khi bạn có phần khung cố định dùng chung cho nhiều màn hình — thanh điều hướng dưới, menu bên, thanh phát nhạc. Nếu mỗi màn hình có bố cục riêng hoàn toàn thì không cần.

Có. Bạn cần khai báo trong AndroidManifest.xml và Info.plist, đồng thời cấu hình phía dịch vụ phát link. Chi tiết ở bài deep link và dynamic link.

Bạn gần như sẽ không viết tay phần này trong dự án thật, nhưng hiểu nó giúp bạn debug khi go_router báo lỗi khó hiểu — và là điểm cộng rõ rệt khi phỏng vấn ở vị trí middle trở lên.

Ý tưởng cốt lõi: thay vì giữ một stack trong bộ nhớ, bạn giữ một cấu hình trang (state) và để Flutter dựng màn hình từ cấu hình đó.

[object Object]

Hai điều quan trọng nhất cần thấy trong ví dụ này:

  • Mỗi trang cần một Key ổn định. Flutter dùng key để biết trang nào được giữ lại khi danh sách trang thay đổi. Thiếu key, toàn bộ ngăn xếp có thể bị dựng lại và trạng thái của trang bị mất.
  • Không có Navigator.push. Bạn đổi trạng thái (_selectedId), và danh sách pages được sinh ra từ trạng thái đó. Đây chính là khác biệt gốc giữa hai cách tiếp cận.

Khi màn hình được dựng từ URL (mở app bằng deep link), bạn cũng cần một bộ phân tích để dịch URL thành SimplePageConfig và ngược lại. Đó là việc của RouteInformationParser. Trong dự án thật, go_router làm hết phần này cho bạn.

Route theo tên và typed routes

Chuỗi đường dẫn viết trực tiếp trong code rất dễ sai sau khi đổi URL, và trình biên dịch không giúp bạn phát hiện. Hai cách cải thiện, dùng tăng dần:

Cách 1 — đặt tên cho route:

[object Object]

Cách 2 — typed routes (sinh code): khai báo route bằng class có kèm chú thích, trình sinh code sẽ tạo lớp truy cập có kiểu. Khi đó CourseDetailRoute(slug: 'flutter-co-ban').go(context) được kiểm tra kiểu ngay lúc biên dịch, và đổi tham số sẽ làm code sai hiện lỗi thay vì chạy tới màn hình trắng.

Với dự án từ khoảng 10 màn hình trở lên, cách 2 tiết kiệm đáng kể thời gian sửa lỗi. Với dự án nhỏ, cách 1 là đủ.

Truyền dữ liệu giữa các màn hình: ba kênh và một cạm bẫy lớn

Có ba cách truyền dữ liệu, và chọn sai cách là nguồn gốc của lỗi chỉ xuất hiện khi mở app từ deep link.

  • extra — truyền cả object trong bộ nhớ, nhanh và tiện.
  • pathParameters — nằm trong đường dẫn, ví dụ /khoa-hoc/flutter-co-ban.
  • queryParameters — nằm sau dấu hỏi, phù hợp cho trạng thái có thể chia sẻ, ví dụ /tim-kiem?q=flutter&page=2.
[object Object]

Cạm bẫy cần nhớ: extra chỉ tồn tại trong phiên chạy hiện tại. Khi người dùng mở link từ Zalo/Facebook, hoặc khi hệ điều hành thu hồi bộ nhớ rồi mở lại app, extra sẽ là null. Vì vậy màn hình chi tiết luôn phải lấy được dữ liệu từ định danh trong URL. Quy tắc thực dụng: dùng extra để hiển thị ngay cho nhanh, nhưng vẫn phải có đường lấy dữ liệu từ slug.

Với trạng thái có thể chia sẻ (từ khoá tìm kiếm, bộ lọc, số trang), hãy đưa vào queryParameters thay vì giữ trong biến cục bộ. Nhờ vậy người dùng gửi link cho đồng nghiệp sẽ mở ra đúng màn hình đã lọc, và nút Back trên trình duyệt hoạt động đúng.

StatefulShellRoute: giữ trạng thái từng tab

ShellRoute giữ khung chung, nhưng khi chuyển tab, widget của tab cũ vẫn có thể bị xoá. Với ứng dụng nhiều tab (trang chủ, khoá học, thông báo, cá nhân), bạn cần mỗi tab giữ ngăn xếp điều hướng và vị trí cuộn riêng.

[object Object]

Với cấu trúc này, người dùng có thể mở khoá học, vào chi tiết, chuyển sang tab cá nhân rồi quay lại tab khoá học — và vẫn thấy đúng màn hình chi tiết đang mở. Đây là hành vi mà người dùng mong đợi, nhưng phải khai báo đúng mới có.

Hiệu ứng chuyển trang tuỳ chỉnh

Mặc định, mỗi nền tảng dùng hiệu ứng khác nhau. Nếu thiết kế yêu cầu một hiệu ứng thống nhất, hãy đặt ở cấp router hoặc ở từng route:

[object Object]

Hai lưu ý: state.pageKey nên được truyền vào để Flutter phân biệt các trang, và nên giữ hiệu ứng ngắn (khoảng 150–300ms). Hiệu ứng dài làm app cảm giác chậm dù máy không hề chậm.

Dialog, bottom sheet và những thứ không nên nằm trong URL

Không phải mọi thứ hiển thị trên màn hình đều nên trở thành một route có URL. Hộp thoại xác nhận xoá, bottom sheet chọn bộ lọc, snackbar thông báo — những thứ này gắn với hành động tức thời và không cần chia sẻ được.

Quy tắc phân chia:

  • Có URL: bất cứ thứ gì người dùng muốn mở lại, chia sẻ link, hoặc quay lại bằng nút Back — danh sách, chi tiết, bước trong quy trình nhiều bước.
  • Không cần URL: hộp thoại, bottom sheet, menu ngữ cảnh, màn hình chọn ảnh của hệ thống.

Trong cả hai trường hợp, vẫn dùng Navigator.push cho lớp phụ chồng lên trên. Việc trộn hai cơ chế là hợp lệ miễn là bạn phân biệt rõ lớp nào thuộc URL, lớp nào không.

Nút Back trên Android và xác nhận khi có dữ liệu chưa lưu

Với form nhập liệu dài, việc người dùng lỡ tay bấm Back và mất toàn bộ nội dung là lỗi trải nghiệm nghiêm trọng. Cách xử lý hiện nay là PopScope:

[object Object]

Điểm cần chú ý: kiểm tra context.mounted sau await trước khi điều hướng, và đặt canPop đúng để cử chỉ vuốt Back trên iOS cũng được xử lý, không chỉ nút Back của Android.

Trên web, người dùng có những hành vi không tồn tại trên mobile: dán link vào tab mới, bấm F5, dùng nút Back của trình duyệt. Router khai báo đúng sẽ xử lý được cả ba, nhưng cần lưu ý vài điểm:

  • Mọi trạng thái quan trọng nên nằm trên URL, nếu không F5 sẽ đưa người dùng về trạng thái ban đầu và họ mất ngữ cảnh.
  • Dùng initialLocation hợp lý để khi mở đường dẫn không khớp, người dùng về trang chủ thay vì màn hình lỗi.
  • Với ứng dụng cần máy tìm kiếm đọc được, hãy nhớ rằng Flutter web vẽ giao diện bằng canvas; nội dung không tự động có trong HTML. Nếu SEO là mục tiêu, cần cân nhắc cách hiển thị phù hợp cho các trang quan trọng.

Test điều hướng và logic chuyển hướng

Phần redirect là logic nghiệp vụ, và logic nghiệp vụ cần test. Hai trường hợp đáng test nhất:

[object Object]

Để test được như trên, hãy tách hàm buildRouter ra khỏi widget gốc và cho nó nhận lớp xác thực qua tham số. Cách này giống nguyên tắc truyền phụ thuộc từ ngoài vào đã dùng ở bài hướng dẫn Provider.

Lỗi điều hướng khó tìm và cách khoanh vùng

  1. "No GoRouter found in context". Thường vì MaterialApp.router chưa được bọc bằng InheritedGoRouter, hoặc vì bạn gọi context.go từ một widget được tạo trong cây con của widget khác (ví dụ showDialog với useRootNavigator đúng nhưng context lấy từ nhánh không có router).
  2. Màn hình trắng khi mở deep link. Nhiều khả năng: đường dẫn không khớp route nào và thiếu errorBuilder, hoặc màn hình chi tiết phụ thuộc extra đã bị mất.
  3. Nút Back đóng app thay vì quay lại. Xảy ra khi bạn dùng go thay vì push cho màn hình thứ hai — go thay thế vị trí hiện tại nên không còn gì để quay lại.
  4. Trạng thái tab bị mất khi chuyển tab. Nguyên nhân là dùng ShellRoute thay vì StatefulShellRoute, hoặc thiếu IndexedStack.
  5. Điều hướng chạy hai lần. Thường do redirect trả về chuỗi và router đánh giá lại, kết hợp với việc gọi go() ngay sau khi đăng nhập trong khi refreshListenable cũng đang kích hoạt. Hãy để một nguồn sự thật duy nhất quyết định.

Câu hỏi thường gặp (tiếp)

Có cần dùng go_router ở dự án nhỏ không?

Không bắt buộc. Với ba màn hình và không có deep link, Navigator.push là lựa chọn đơn giản và ít code bảo trì hơn. Tuy nhiên nếu bạn chắc chắn sản phẩm sẽ mở rộng hoặc cần link chia sẻ được, dựng router từ đầu sẽ tiết kiệm hơn nhiều so với việc chuyển đổi sau này.

ShellRoute và StatefulShellRoute khác nhau ở đâu?

Cả hai giữ khung giao diện chung. StatefulShellRoute giữ thêm ngăn xếp điều hướng riêng cho từng nhánh, nên trạng thái của tab không bị mất khi đổi tab. Nếu ứng dụng có thanh điều hướng dưới với nhiều nhánh, gần như luôn nên dùng bản Stateful.

Deep link thường vào thẳng một đường dẫn sâu (ví dụ /khoa-hoc/flutter-co-ban), còn điều hướng thường sẽ đi qua danh sách trước. Cách kiểm tra đáng tin là khởi động app bằng flutter run kèm đường dẫn, hoặc cấu hình thiết bị rồi mở link thật — đừng chỉ dựa vào việc bấm nút trong app.

Có nên đặt router trong một provider?

Không nên. Router có vòng đời riêng và cần được tạo một lần cho toàn ứng dụng. Hãy tạo ở lớp ngoài cùng và dùng refreshListenable để nó tự đánh giá lại khi trạng thái liên quan (ví dụ đăng nhập) thay đổi.

Làm sao xử lý khi người dùng bấm Back hai lần để thoát app?

Hành vi này thường thấy ở màn hình chính. Trên Android, cách làm đúng là xử lý ở nút Back với thông báo "Bấm lần nữa để thoát" trong khoảng vài giây. Với PopScope, đặt canPop: false ở màn hình chính, và khi người dùng bấm lần hai trong thời hạn đó thì gọi thoát app bằng API của hệ thống. Lưu ý không nên áp dụng cho mọi màn hình — chỉ màn hình gốc, vì ở màn hình khác người dùng thực sự muốn quay lại.

Có nên truyền BuildContext của router xuống tầng service không?

Không. Tầng service nên nhận dữ liệu thuần và trả về kết quả, không nên biết gì về điều hướng. Nếu bạn cần điều hướng sau khi một tác vụ hoàn thành, hãy để tầng giao diện lắng nghe kết quả rồi mới gọi context.go. Cách này cũng tránh lỗi dùng context sau await — nguyên nhân phổ biến của các lỗi khó tái hiện.

Router có hoạt động khác nhau giữa Android, iOS và web không?

Có. Ba điểm khác biệt thường gặp: cử chỉ vuốt Back trên iOS cần canPop đúng để không bị bỏ qua; nút Back của Android có thể đóng app nếu stack rỗng; và trên web, mỗi thay đổi trạng thái nên phản ánh lên URL để người dùng tải lại trang vẫn giữ đúng ngữ cảnh. Hãy kiểm tra cả ba nền tảng trước khi coi phần điều hướng đã xong.

Checklist điều hướng trước khi release

Điều hướng là phần ít được kiểm tra kỹ nhất nhưng lại gây lỗi khó tái hiện nhất. Danh sách dưới đây dùng để rà nhanh:

  • Mọi màn hình quan trọng đều có đường dẫn riêng, mở lại được sau khi tải lại app.
  • errorBuilder đã được khai báo, thử bằng một đường dẫn sai để xác nhận.
  • Đã thử mở app bằng deep link cho ít nhất ba màn hình khác nhau.
  • Màn hình chi tiết lấy được dữ liệu từ định danh trong URL, không phụ thuộc extra.
  • Luồng đăng nhập có kiểm tra tập trung, và đã thử đăng xuất giữa các màn hình.
  • Nút Back ở màn hình chính có thông báo xác nhận, không đóng app ngay.
  • Form có dữ liệu chưa lưu đã có xác nhận khi rời màn hình.
  • Chuyển tab rồi quay lại vẫn giữ đúng màn hình đang xem của tab đó.
  • Trạng thái quan trọng (từ khoá, bộ lọc, số trang) nằm trên URL.
  • Đã thử cả ba nền tảng: Android, iOS và web.

Kết luận

Chọn navigation phù hợp:

  • ✅ Navigation 1.0: Đơn giản, cho apps cơ bản
  • ✅ Navigation 2.0: Mạnh mẽ, cho apps phức tạp

Tóm tắt:

  1. Navigation 1.0: Đơn giản, dễ học
  2. Navigation 2.0: Mạnh mẽ, phù hợp web
  3. Choose wisely: Chọn theo nhu cầu project

Chọn đúng navigation sẽ giúp app của bạn dễ maintain hơn! 🚀


Học Flutter bài bản theo lộ trình

Nếu bạn muốn học có người sửa bài thay vì tự ghép tài liệu, Flutter Hướng Nghiệp đào tạo theo 3 cấp:

Lớp online trực tiếp qua Zoom/Google Meet, có bản ghi lại — không bắt buộc đến cơ sở.
Xem lịch khai giảng hoặc so sánh 5 cách học lập trình mobile nếu còn phân vân.