Chào mừng các bạn quay trở lại với series “Android Developer Roadmap“! Sau khi đã cùng nhau tìm hiểu về cấu trúc ứng dụng, các thành phần cơ bản như Activity, Fragment, Layout, và cách tạo ứng dụng “Hello World” đầu tiên, đã đến lúc chúng ta đi sâu vào một khía cạnh cực kỳ quan trọng của bất kỳ ứng dụng di động nào: Điều hướng (Navigation).
Trong quá khứ, việc quản lý các màn hình (Fragment hoặc Activity) và luồng điều hướng giữa chúng có thể khá phức tạp. Bạn phải tự tay quản lý Backstack của Fragment Manager, xử lý các trường hợp xoay màn hình, cấu hình Intents phức tạp (đôi khi kết hợp với Intent Filter), và đảm bảo trải nghiệm người dùng nhất quán khi nhấn nút Back hoặc Up. Đây là một nguồn gốc phổ biến của các lỗi không mong muốn.
May mắn thay, thư viện Navigation Component của Android Jetpack đã ra đời để giải quyết vấn đề này. Nó cung cấp một framework mạnh mẽ, linh hoạt và có cấu trúc để quản lý điều hướng trong ứng dụng của bạn, từ các trường hợp đơn giản nhất đến phức tạp nhất. Sử dụng Navigation Component không chỉ giúp mã nguồn của bạn sạch sẽ hơn, dễ bảo trì hơn mà còn cung cấp các tính năng sẵn có như xử lý Backstack, truyền dữ liệu an toàn, và tích hợp dễ dàng với các thành phần UI phổ biến.
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu Navigation Component từ A đến Z, thông qua một hướng dẫn từng bước chi tiết. Bắt đầu nào!
Mục lục
Các Khái niệm Cốt lõi của Navigation Component
Trước khi đi vào thực hành, hãy làm quen với các thành phần chính tạo nên Navigation Component:
- Navigation Graph (nav_graph.xml): Đây là một tài nguyên XML (hoặc được tạo thông qua trình chỉnh sửa đồ họa trong Android Studio) định nghĩa tất cả các màn hình (được gọi là destinations) trong ứng dụng của bạn và cách người dùng có thể di chuyển giữa chúng (được gọi là actions). Nó cung cấp một cái nhìn trực quan về luồng điều hướng của ứng dụng.
- NavHostFragment: Đây là một Fragment đặc biệt hoạt động như một “máy chủ” hoặc “vùng chứa” trong layout Activity của bạn. Khi bạn điều hướng, NavHostFragment sẽ hiển thị destination tiếp theo trong vùng chứa của nó. Thông thường, NavHostFragment sẽ được đặt trong Activity chính của bạn.
- NavController: Đây là đối tượng mà bạn sẽ sử dụng trong mã nguồn (ví dụ: trong Fragment) để thực hiện các thao tác điều hướng. NavController biết NavHostFragment của bạn và có thể thực hiện các hành động điều hướng được định nghĩa trong Navigation Graph.
- Destinations: Đây là các màn hình hoặc điểm dừng trong ứng dụng của bạn. Thông thường, chúng là các Fragment, nhưng cũng có thể là Activity hoặc thậm chí là các destination tùy chỉnh.
- Actions: Đây là các kết nối giữa các destinations trong Navigation Graph, biểu thị một đường dẫn mà người dùng có thể đi theo. Actions có thể mang theo dữ liệu và có các tùy chọn cấu hình như animation chuyển cảnh hoặc xử lý Backstack.
Việc nắm vững các khái niệm này là bước đầu tiên để sử dụng Navigation Component hiệu quả.
Thiết lập Môi trường và Dependencies
Để sử dụng Navigation Component, bạn cần thêm các thư viện tương ứng vào file build.gradle (app)
của module ứng dụng. Đồng thời, để sử dụng tính năng Safe Args (sẽ giải thích sau), bạn cần thêm plugin vào cả file build.gradle (project)
và build.gradle (app)
. Nếu bạn chưa quen với Gradle, hãy xem lại bài viết “Gradle là gì? Cách sử dụng trong Phát triển Android“.
Thêm dòng sau vào file build.gradle (project)
, bên trong block plugins
:
plugins {
// ... các plugin khác
id("androidx.navigation.safeargs.kotlin") version "2.7.7" apply false // Sử dụng version mới nhất
}
Thêm các dòng sau vào file build.gradle (app)
:
plugins {
// ... các plugin khác
id("androidx.navigation.safeargs.kotlin")
}
android {
// ...
}
dependencies {
// ... các dependencies khác
// Navigation Component
val nav_version = "2.7.7" // Sử dụng version mới nhất
// Kotlin
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version")
implementation("androidx.navigation:navigation-ui-ktx:$nav_version")
// Nếu cần Navigation Dynamic Features Module
// implementation("androidx.navigation:navigation-dynamic-features-fragment:$nav_version")
// Nếu cần Navigation Testing
// androidTestImplementation("androidx.navigation:navigation-testing:$nav_version")
// Nếu dùng Jetpack Compose
// implementation("androidx.navigation:navigation-compose:$nav_version")
}
Sau khi thêm hoặc cập nhật dependencies, bạn cần Sync lại project bằng cách nhấn nút “Sync Now” xuất hiện trên thanh thông báo của Android Studio hoặc chọn “File” -> “Sync Project with Gradle Files”.
Hướng dẫn Từng bước Sử dụng Navigation Component
Bây giờ, chúng ta sẽ đi vào phần chính: xây dựng điều hướng bằng Navigation Component.
Bước 1: Tạo Navigation Graph
Navigation Graph là trung tâm của hệ thống điều hướng. Để tạo nó:
- Trong cửa sổ Project pane, click chuột phải vào thư mục
res
. - Chọn New > Android Resource File.
- Trong dialog “New Resource File”, điền các thông tin sau:
- File name: Đặt tên cho graph của bạn, ví dụ:
nav_graph
. - Resource type: Chọn
navigation
.
- File name: Đặt tên cho graph của bạn, ví dụ:
- Nhấn OK.
Android Studio sẽ tạo một thư mục navigation
trong res
và mở file nav_graph.xml
trong trình chỉnh sửa đồ họa (Design view).
Bước 2: Thêm Destinations (Fragments)
Bạn có thể thêm các Fragment (hoặc Activity) hiện có vào graph hoặc tạo Fragment mới trực tiếp từ trình chỉnh sửa graph.
- Trong trình chỉnh sửa graph (Design view), nhấn vào biểu tượng “+” (New Destination) ở góc trên bên trái.
- Chọn “Create new destination” để tạo Fragment mới hoặc chọn Fragment/Activity hiện có trong danh sách.
- Lặp lại bước này cho tất cả các màn hình mà bạn muốn quản lý bằng Navigation Component.
Mỗi destination sẽ xuất hiện như một hộp trong graph.
Bước 3: Định nghĩa Start Destination
Start destination là màn hình đầu tiên mà người dùng nhìn thấy khi ứng dụng khởi chạy (hoặc khi đi vào luồng điều hướng cụ thể này). Để thiết lập start destination:
- Click vào destination mà bạn muốn đặt làm màn hình bắt đầu.
- Trong thuộc tính Attributes (nếu không thấy, mở View > Tool Windows > Attributes), click vào biểu tượng ngôi nhà màu xanh lá cây (Set as Start Destination).
Biểu tượng ngôi nhà sẽ xuất hiện trên destination đó trong graph.
Bước 4: Thêm Actions để Nối Các Destinations
Actions xác định các đường dẫn di chuyển giữa các destinations.
- Trong trình chỉnh sửa graph, di chuột qua cạnh phải của destination nguồn (ví dụ: Fragment A).
- Một hình tròn sẽ xuất hiện. Click và kéo hình tròn này đến destination đích (ví dụ: Fragment B).
- Một mũi tên (action) sẽ xuất hiện nối Fragment A với Fragment B.
- Click vào mũi tên này để chọn và cấu hình các thuộc tính của action trong cửa sổ Attributes, ví dụ:
- Type: Xác định loại destination đích.
- ID: Một ID duy nhất cho action (được tạo tự động).
- Arguments: Thêm các đối số cần truyền đi.
- Options: Cấu hình animation chuyển cảnh (xem thêm về Animation), cách xử lý Backstack (
popUpTo
,inclusive
).
Lặp lại bước này cho tất cả các luồng điều hướng trong ứng dụng của bạn.
Bước 5: Thêm NavHostFragment vào Layout của Activity
NavHostFragment là nơi các destinations sẽ được hiển thị. Bạn cần thêm nó vào layout XML của Activity chính của bạn (thường là activity_main.xml
).
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- Đây là NavHostFragment -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:defaultNavHost="true" // Đảm bảo nó intercept nút Back của hệ thống
app:navGraph="@navigation/nav_graph" /> <!-- Liên kết với graph đã tạo -->
</androidx.constraintlayout.widget.ConstraintLayout>
Trong ví dụ trên, chúng ta sử dụng FragmentContainerView
(được khuyến khích thay cho FragmentContainer
hoặc tag <fragment>
cũ) và đặt android:name
là androidx.navigation.fragment.NavHostFragment
. Thuộc tính app:navGraph="@navigation/nav_graph"
liên kết NavHostFragment này với Navigation Graph mà chúng ta đã tạo. Thuộc tính app:defaultNavHost="true"
đảm bảo rằng NavHostFragment của bạn xử lý nút Back của hệ thống.
Nếu bạn muốn tìm hiểu thêm về các loại Layout, hãy tham khảo bài viết “Khám phá các Layout trong Android: Frame, Linear, Relative và Constraint“.
Bước 6: Điều hướng giữa các Destinations với NavController
Bây giờ bạn đã có NavHostFragment trong Activity và các actions trong Navigation Graph, bạn có thể kích hoạt điều hướng từ bất kỳ Fragment nào bên trong NavHostFragment đó bằng cách sử dụng NavController.
Trong Fragment nguồn (ví dụ: trong một listener của Button), bạn có thể lấy NavController và gọi phương thức navigate()
:
class FragmentA : Fragment(R.layout.fragment_a) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_go_to_b).setOnClickListener {
// Tìm NavController
val navController = findNavController()
// Thực hiện điều hướng bằng ID của action trong nav_graph
// R.id.action_fragmentA_to_fragmentB là ID của action bạn đã tạo ở Bước 4
navController.navigate(R.id.action_fragmentA_to_fragmentB)
// Hoặc, nếu bạn sử dụng Safe Args (rất khuyến khích!)
// val action = FragmentADirections.actionFragmentAToFragmentB()
// navController.navigate(action)
}
}
}
Phương thức findNavController()
là một extension function trong KTX, giúp bạn dễ dàng lấy NavController liên kết với Fragment hoặc View hiện tại.
Bước 7: Truyền Dữ liệu giữa các Destinations (Sử dụng Safe Args)
Việc truyền dữ liệu giữa các màn hình là một yêu cầu phổ biến. Navigation Component, đặc biệt là với plugin Safe Args, cung cấp một cách an toàn và có kiểu dữ liệu để làm điều này.
Safe Args là một plugin Gradle tạo ra các lớp đơn giản và đối tượng builder để điều hướng an toàn về kiểu dữ liệu. Khi sử dụng Safe Args, bạn không cần phải lo lắng về việc sai tên key khi đặt hoặc lấy dữ liệu từ Bundle.
Cách sử dụng Safe Args:
- Thêm plugin Safe Args như đã hướng dẫn ở phần Thiết lập Môi trường.
- Thêm đối số (Arguments) vào destination đích trong Navigation Graph (sử dụng trình chỉnh sửa đồ họa hoặc chỉnh sửa XML trực tiếp). Chọn destination đích (ví dụ: Fragment B), trong cửa sổ Attributes, tìm mục Arguments và nhấn “+”.
- Name: Tên của đối số (ví dụ:
userId
). - Type: Kiểu dữ liệu (String, int, boolean, custom Parcelable/Serializable, v.v.).
- Nullable: Cho phép giá trị null hay không.
- Default Value: Giá trị mặc định (tùy chọn).
- Name: Tên của đối số (ví dụ:
- Sync lại project sau khi thêm arguments. Gradle sẽ tạo ra các lớp Safe Args.
- Truyền dữ liệu từ destination nguồn: Sử dụng lớp Directions được tạo bởi Safe Args.
class FragmentA : Fragment(R.layout.fragment_a) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
view.findViewById<Button>(R.id.button_go_to_b).setOnClickListener {
val userId = 123
val userName = "Alice"
// Sử dụng lớp Directions được tạo bởi Safe Args
val action = FragmentADirections.actionFragmentAToFragmentB(userId, userName)
findNavController().navigate(action)
}
}
}
- Nhận dữ liệu tại destination đích: Sử dụng lớp Args được tạo bởi Safe Args.
class FragmentB : Fragment(R.layout.fragment_b) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Sử dụng lớp Args được tạo bởi Safe Args
val args: FragmentBArgs = FragmentBArgs.fromBundle(requireArguments())
val userId = args.userId
val userName = args.userName
// Sử dụng userId và userName để hiển thị thông tin hoặc tải dữ liệu
view.findViewById<TextView>(R.id.text_user_info).text = "User ID: $userId, Name: $userName"
}
}
Việc sử dụng Safe Args giúp giảm thiểu lỗi runtime do sai key hoặc sai kiểu dữ liệu, làm cho mã nguồn của bạn an toàn và dễ đọc hơn.
Bước 8: Xử lý Điều hướng Lên (Up) và Quay lại (Back)
Navigation Component tự động xử lý nút Back của hệ thống dựa trên Backstack được quản lý bởi NavController. Khi người dùng nhấn nút Back, NavController sẽ tự động pop destination hiện tại khỏi Backstack và hiển thị destination trước đó.
Đối với nút Up (mũi tên quay lại trên ActionBar), bạn cần kết nối NavController với ActionBar (hoặc Toolbar). Thư viện NavigationUI cung cấp các helper method để làm điều này.
Trong MainActivity.kt
(hoặc Activity chứa NavHostFragment):
class MainActivity : AppCompatActivity() {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Tìm NavController của NavHostFragment
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
// Kết nối NavController với ActionBar để xử lý nút Up
setupActionBarWithNavController(navController)
}
// Override phương thức này để NavController xử lý sự kiện nút Up
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
Hàm setupActionBarWithNavController(navController)
sẽ hiển thị tiêu đề của destination hiện tại trên ActionBar và tự động hiển thị nút Up khi không ở start destination. Phương thức onSupportNavigateUp()
cần được override để chuyển tiếp sự kiện nút Up cho NavController xử lý.
Bước 9: Tích hợp với các UI Khác (Tóm tắt)
Navigation Component được thiết kế để tích hợp dễ dàng với các thành phần UI phổ biến như:
- BottomNavigationView: Bạn có thể liên kết các menu item của BottomNavigationView với các destinations trong Navigation Graph. Khi người dùng chọn một item, NavController sẽ tự động điều hướng đến destination tương ứng.
- NavigationView (cho DrawerLayout): Tương tự, bạn có thể liên kết các menu item trong Drawer Navigation với các destinations.
Việc tích hợp này thường chỉ yêu cầu một dòng code duy nhất sử dụng các extension function từ thư viện NavigationUI KTX, ví dụ:
// Trong Activity sau khi setup NavController
val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav_view)
bottomNavigationView.setupWithNavController(navController)
// Hoặc cho NavigationView trong Drawer
val navView = findViewById<NavigationView>(R.id.nav_view)
navView.setupWithNavController(navController)
Điều này giúp bạn quản lý điều hướng từ các thành phần UI phức tạp một cách cực kỳ đơn giản. Để tìm hiểu thêm về các thành phần UI này, bạn có thể xem lại bài viết “Sử dụng Fragments, Dialogs, Bottom Sheets và Drawers – Xây dựng Giao diện Linh hoạt và Tương tác“.
Lợi ích của việc Sử dụng Navigation Component
Sau khi đã đi qua các bước cơ bản, bạn có thể thấy Navigation Component mang lại rất nhiều lợi ích so với các phương pháp quản lý điều hướng truyền thống:
- Đơn giản hóa: Quản lý tập trung luồng điều hướng trong một file duy nhất (Navigation Graph).
- Trực quan: Trình chỉnh sửa đồ họa giúp dễ dàng hình dung luồng điều hướng.
- Xử lý Backstack tự động: NavController quản lý Backstack một cách chính xác, xử lý các trường hợp phức tạp như deeplinks và cấu hình lại.
- Truyền dữ liệu an toàn: Safe Args loại bỏ lỗi thời gian chạy liên quan đến Bundle.
- Hỗ trợ Deep Linking: Dễ dàng cấu hình và xử lý Deep Links.
- Tích hợp UI: Dễ dàng kết nối với BottomNavigationView, DrawerLayout, Toolbar/ActionBar.
- Animation & Transitions: Hỗ trợ tích hợp các animation chuyển cảnh giữa các màn hình (tìm hiểu thêm về Animation).
- Consistency: Cung cấp trải nghiệm điều hướng nhất quán trên toàn ứng dụng.
So sánh: Navigation Component vs. Điều hướng Truyền thống
Để làm rõ hơn lợi ích, hãy xem bảng so sánh sau:
Tính năng | Navigation Component | Phương pháp Truyền thống (Manual Fragment Transactions, Intents) |
---|---|---|
Thiết lập | Tập trung trong Navigation Graph, cấu hình dễ dàng qua GUI. | Phân tán trong mã nguồn (Activity/Fragment), cần viết nhiều code boiler-plate. |
Quản lý Backstack | Tự động và chính xác. | Cần quản lý thủ công với FragmentManager (addToBackStack() , popBackStack() ) hoặc cờ Intent. Dễ xảy ra lỗi. |
Truyền dữ liệu | Safe Args cung cấp cách an toàn, có kiểu dữ liệu. | Sử dụng Bundle với key-value String. Dễ sai key hoặc sai kiểu dữ liệu (runtime crash). |
Deep Linking | Hỗ trợ tích hợp sẵn và dễ cấu hình trong graph. | Cần cấu hình Intent Filter phức tạp trong Manifest và xử lý parsing dữ liệu thủ công. |
Tích hợp UI | Cung cấp các helper functions (NavigationUI) để tích hợp với BottomNavigationView, DrawerLayout, Toolbar/ActionBar chỉ với 1 dòng code. | Cần viết code listener riêng cho từng thành phần UI và tự gọi phương thức điều hướng. |
Độ phức tạp | Giảm thiểu mã nguồn liên quan đến điều hướng, tăng khả năng đọc và bảo trì. | Mã nguồn điều hướng phức tạp, phân tán, khó debug và bảo trì, đặc biệt với các luồng phức tạp. |
Các Chủ đề Nâng cao (Giới thiệu)
Bài viết này đã giới thiệu các bước cơ bản để bắt đầu với Navigation Component. Tuy nhiên, thư viện này còn nhiều tính năng mạnh mẽ khác mà bạn có thể khám phá khi dự án phức tạp hơn:
- Deep Linking: Cho phép người dùng mở trực tiếp một màn hình cụ thể trong ứng dụng của bạn từ một URL hoặc thông báo.
- Conditional Navigation & Pop Up: Kiểm soát chi tiết cách Backstack hoạt động khi điều hướng (ví dụ: xóa các màn hình trung gian).
- Nested Graphs: Tổ chức các luồng điều hướng phức tạp thành các graph con.
- Global Actions: Actions có thể truy cập từ bất kỳ destination nào trong graph.
- Navigation in Jetpack Compose: Navigation Component cũng có hỗ trợ riêng cho Jetpack Compose thông qua thư viện
navigation-compose
.
Kết luận
Navigation Component là một công cụ không thể thiếu trong bộ Android Jetpack hiện đại. Nó giúp chúng ta giải quyết bài toán điều hướng vốn phức tạp một cách thanh lịch, an toàn và có cấu trúc. Bằng việc áp dụng Navigation Component, bạn sẽ tiết kiệm được rất nhiều thời gian và công sức trong việc phát triển và bảo trì ứng dụng Android của mình.
Việc làm quen và sử dụng thành thạo Navigation Component là một bước tiến quan trọng trên Android Developer Roadmap của bạn. Hãy dành thời gian thực hành bằng cách áp dụng nó vào các project thử nghiệm hoặc ngay trong project thực tế của bạn.
Nếu bạn có bất kỳ câu hỏi nào hoặc gặp khó khăn trong quá trình thực hiện, đừng ngần ngại để lại bình luận bên dưới. Hẹn gặp lại các bạn trong các bài viết tiếp theo của series!