<h1>Android 筆記 -- Navigation (一) 導航組件</h1> >前置閱讀: [Android筆記–Fragment](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_Fragment) <code>Navigation</code> 是用來管理 App 內畫面之間切換與流程控制的元件,常用於處理 Fragment 或 Jetpack Compose 畫面的導覽。它可以統一管理頁面切換、參數傳遞、返回堆疊,以及深層連結(Deep Link)等功能。 相較於傳統 <code>FragmentTransaction</code>、<code>Bundle</code> 與 <code>Back Stack</code> 等做法,<code>Navigation</code> 提供更結構化且易於維護的方式,能降低程式複雜度,提升可讀性與開發效率。 <details style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"><summary><code>Navagation</code> 和 <code>intent</code> 的頁面跳轉差在哪裡?</summary> <div> - <code><b>Navagation</b></code>: ==專為 Fragment 導航 而設計==,通常在單一 Activity 應用中使用。Navigation 透過一個導航圖(Navigation Graph) 集中管理頁面之間的切換,讓導航結構清晰易讀。 - <code><b>Intent</b></code>: 是 Android 原生的頁面跳轉方式,==適用於 Activity 之間 的導航==,且支援跨應用跳轉。主要依賴 Intent 的參數設定和系統的 Activity 管理來執行跳轉。 </div> </details> --- <h2>▼宣告依附元件</h2> --- 到官方網站找適當的依附元件 -- [AndroidDeveloper--Navigation](https://developer.android.com/jetpack/androidx/releases/navigation?hl=zh-tw#kts) ```kotlin= dependencies { val nav_version = "2.8.0" // Jetpack Compose integration implementation("androidx.navigation:navigation-compose:$nav_version") // Views/Fragments integration implementation("androidx.navigation:navigation-fragment:$nav_version") implementation("androidx.navigation:navigation-ui:$nav_version") } ``` <div id="dependNote"> >[!Warning]備註 >如果嘗試不宣告依附就直接新增 <code>NavGraph</code> 的話,系統會跳出警告視窗,選擇確定的話會自動加載相關元件。使用此方法可能會造成 API 等級 錯誤,因此需要調整 API 等級 到指定水平。 >![image](https://hackmd.io/_uploads/S16rrhpZ1l.png =40%x) </div> --- <h2>▼Navigation 的構成: Graph、Host、Controller</h2> --- <h3><font size=4><b>&lt; <u>Navigation Graph(導航圖)</u> &gt;</b></font></h3> 是一個 XML 資源文件,描述應用內所有可導航的頁面(Fragment 或 Activity)以及它們之間的關係。定義了導航的路徑、條件、動作和參數。 <div style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"> <details><summary>新增導航圖</summary> <div> ![image](https://hackmd.io/_uploads/SyZ5KnTWkl.png) ![image](https://hackmd.io/_uploads/rka0F36Zkg.png) </div> </details> <details><summary>基本使用</summary> <div> --- - 1. 在導覽圖頁面中,點選 new Desttination 可以在畫面中新增 Fragment 或 Activity 到頁面中 ![image](https://hackmd.io/_uploads/HyvhjnTWyl.png) - 2. 新增後就可以牽線,牽線會生成 <code>action</code>,這個 <code>action</code> 代表了我們想從 <code>FragmentA</code> 前往 <code>FragmentB</code> 的動作,我們會在 <code>Controller</code> 使用到他 ![1731215029990](https://hackmd.io/_uploads/HkkjibAZJg.gif) </div> </details> <details id="NavigationGraphBasicProperties"><summary>NavigationGraph 基本屬性</summary> <div> --- - <b><code>app:startDestination</code></b>: 定義導航圖的起始目的地,即應用開啟時最先顯示的頁面。 - <b><code>android:name</code></b>: 指定目的地的完整類名(如 Fragment 或 Activity 的類名)。 - <b><code>app:destination</code></b>: 指定導航行為的目的地,即目標頁面的 ID。 - <b><code>android:label</code></b>: 設置目的地的標題,通常用於顯示在工具列或導航欄上。 - <b><code>action 元素</code></b>: 設置從一個 Fragment 到另一個 Fragment 的具體導航邏輯。 - <b><code>deepLink 元素</code></b>: 深層鏈接的作用是讓應用可以直接透過特定的 URL 或 URI 路徑,直接導航到應用中的某個頁面,而不需要用戶逐層點擊進入。例如:通過 Deep Link 將用戶導向應用的特定頁面。 </div> </details> <details><summary>NavigationGraph 進階屬性</summary> <div> --- - <b><code>app:popUpTo</code></b>: 在導航到新目的地之前,清空回退堆疊直到指定的目的地。例如:當完成某些任務後(如登入),不希望用戶返回前一頁時使用。 - <b><code>app:popUpToInclusive</code></b>: 當設為 true 時,會包含 popUpTo 目的地本身一同清除;設為 false 則保留該目的地。通常配合 app:popUpTo 使用,以決定是否保留指定頁面。 - <b><code>app:enterAnim</code>/<code>app:exitAnim</code></b>: 定義進入和退出動畫效果,用於頁面切換時的動畫過渡。 - <b><code>app:popEnterAnim</code>/<code>app:popExitAnim</code></b>: 定義當回退操作(popBackStack)發生時的進入和退出動畫效果。設置自定義的回退動畫,例如回到上一頁時的淡入淡出效果。 - <b><code>argument 元素</code></b>: 在需要傳遞資料(如產品 ID 或用戶資訊)時使用,用於定義從一個頁面傳遞到另一個頁面的參數,支援類型(如 Int、String、Boolean)和預設值。 ```xml= <fragment android:id="@+id/detailsFragment" android:name="com.example.app.DetailsFragment" android:label="Details"> <argument android:name="productId" app:argType="integer" android:defaultValue="0" /> </fragment> ``` - <b><code>app:launchSingleTop</code></b>: 設置為 <code>true</code> 時,若當前目的地已在堆疊頂部則不會重新載入新的目的地,防止重複導航。 - <b><code>app:restoreState</code></b>: 設置為 <code>true</code> 時,當導航至目的地且該目的地在回退堆疊中時會保留其狀態。例如在切換頁面後返回原頁面時希望保持當前狀態(如滾動位置或輸入框內容)。 </div> </details> </br> <details><summary>NavigationGraph 範例</summary> <div> ```xml= <?xml version="1.0" encoding="utf-8"?> <navigation 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:id="@+id/nav_graph" app:startDestination="@id/fragmentA"> <fragment android:id="@+id/fragmentA" android:name="com.example.mypratice_navigation_2.FragmentA" android:label="fragment_a" tools:layout="@layout/fragment_a" > <action android:id="@+id/action_fragmentA_to_fragmentB" app:destination="@id/fragmentB" /> </fragment> <fragment android:id="@+id/fragmentB" android:name="com.example.mypratice_navigation_2.FragmentB" android:label="fragment_b" tools:layout="@layout/fragment_b" > <action android:id="@+id/action_fragmentB_to_fragmentC" app:destination="@id/fragmentC" /> </fragment> <fragment android:id="@+id/fragmentC" android:name="com.example.mypratice_navigation_2.FragmentC" android:label="fragment_c" tools:layout="@layout/fragment_c" /> </navigation> ``` </div> </details> </div> --- <h3><font size=4><b>&lt; <u>Navigation Host(導航容器)</u> &gt;</b></font></h3> 是一個容器,類似於 [FragmentContainer](https://hackmd.io/V5JVVbtSRsysKGD2Cme4Ag?view=&stext=2024%3A22%3A0%3A1731210895%3AGYA68m),用來顯示導航圖中的目的地。 通常是使用 <code>NavHostFragment</code> (<code>NavHostFragment</code> 是 Fragment 的子類別)。它通過 <code>NavController</code> 來控制導航操作。 <div style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"> <details><summary>基本使用</summary> <div> --- 在 Activity 新增 <code>NavHostFragment compoment</code> 並指定要使用的 <code>NavGraph</code> 就好了 ![image](https://hackmd.io/_uploads/rJY7gg1f1e.png) </br> ![image](https://hackmd.io/_uploads/rJjKxxkM1e.png) - <b><code>app:navGraph</code></b>: 指定了 <code>NavHostFragment</code> 使用的導航圖的 ID,該屬性連接導航圖,從而載入導航配置。 - <b><code>app:defaultNavHost</code></b>: 設置為 <code>true</code> 時,<code>NavHostFragment</code> 成為應用的預設導航宿主,處理返回按鈕行為,用於當前 Activity 只有一個 <code>NavHostFragment</code> 時,讓 <code>NavHostFragment</code> 處理返回按鈕。 </div> </details> </div> --- <h3><font size=4><b>&lt; <u>Navigation Controller(導航控制器)</u> &gt;</b></font></h3> 是 <code>Navigation</code> 的控制器,我們可以使用 <code>NavController</code> 來觸發導航操作負責管理 <code>NavHostFragment</code> 的導航圖,處理各個頁面之間的切換和回退邏輯 - 例如: 從一個 Fragment 跳轉到另一個 Fragment。 - 除了頁面跳轉,還可以做到返回堆疊管理、資料傳遞等。 <div style="border-left: 4px solid #1565C0;background-color: #0D1B2A;padding: 12px 16px;margin: 10px 0;border-radius: 4px;font-family: Arial, sans-serif;"> <details><summary>取得 <code>NavController</code> -- 傳統基礎作法</summary> <div> --- - <b>步驟1.</b> 取得 Activity 中的 <code>NavHostFragment</code> ```kotlin= val navHostFragment = requireActivity().supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment //NavHostFragment的是Fragment的子類別,因此可以透過requireActivity().supportFragmentManager.findFragmentById()來找到他 ``` - <b>步驟2.</b> 從 <code>NavHostFragment</code> 取得 <code>NavController</code> ```kotlin= val navController = navHostFragment.navController ``` - <b>步驟3. </b>根據需求使用 <code>NavController</code>,例如 Fragment 跳轉 ```kotlin= binding.button1.setOnClickListener { navController.navigate(R.id.action_fragmentA_to_fragmentB) } ``` </div> </details> <details><summary>取得 <code>NavController</code> -- 使用 <code>fragment-ktx 擴展函數</code> 的作法</summary> <div> --- 優點: 類型安全、編譯時期錯誤檢查、簡潔且易維護、支持 <code>Safe Args</code> 傳遞參數 >如果在宣告附加元件時使用警告視窗的自動加載元件的話,<code>fragment-ktx</code> 也會被加載(也就是說 步驟1 可以省略),[參見](#dependNote) - <b>步驟1.</b> 加載 Fragment 的 <code>Ktx 擴展函數</code> [AndroidDeveloper--Fragment依賴元件](https://developer.android.com/jetpack/androidx/releases/fragment?hl=zh-tw#declaring_dependencies) ```kotlin= implementation ("androidx.fragment:fragment-ktx:1.8.3") ``` - <u><b>步驟2.</b> 直接在需要 <code>NavController</code> 的地方用 <code>findNavController()</code> 方法</u> ```kotlin= class FragmentA : Fragment() { private lateinit var binding: FragmentABinding override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? { binding = FragmentABinding.inflate(inflater, container, false) val navController = findNavController() binding.button1.setOnClickListener { navController.navigate(R.id.action_fragmentA_to_fragmentB) } return binding.root } } ``` </div> </details> <details><summary>在 Activity 中取得 <code>NavController</code></summary> <div> --- 在 Activity 中取得 <code>NavController</code> 的方法和在 Fragment 中略有不同,基本上一樣是從 <code>NavHostFragment</code> 取得,須注意用 <code>ViewBinding</code> 的話,要再加上 <code>getFragment<NavHostFragment>()</code> ```kotlin= //在Activity取得NavController,A方法 val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragmentContainerView) as NavHostFragment val navController = navHostFragment.findNavController() //在Activity取得NavController,B方法 val navHostFragment = binding.fragmentContainerView.getFragment<NavHostFragment>() val navController = navHostFragment.findNavController() ``` </div> </details> </br> <details><summary><code>NavController</code> 中常用的方法</summary> <div> --- - <b><code>navigate(destinationId: Int)</code></b>: 最基本且常見的方法,將使用者導航到指定的目的地。 ```kotlin= navController.navigate(R.id.action_fragmentA_to_fragmentB) ``` - <b><code>navigateUp()</code></b>: 嘗試將使用者導引回上一個目的地。如果無法導航回上一個頁面,它會返回 false。 - <b><code>popBackStack(destinationId: Int, inclusive: Boolean)</code></b>: 從返回堆疊中彈出指定頁面,回到特定的目的地。<code>inclusive</code> 決定是否也彈出 destinationId 自己。 - <b><code>getBackStackEntry(destinationId: Int)</code></b>: 取得指定目的地的返回堆疊條目,用於從不同頁面共用狀態,例如: 共享 <code>ViewModel</code>。 - <b><code>currentBackStackEntry</code></b>: 獲取當前的返回堆疊條目,常用於觀察當前頁面狀態變化,例如: 觀察當前頁面的 <code>LiveData</code>。 ```kotlin= navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")?.observe(viewLifecycleOwner) { value -> // Handle value change } ``` - <b><code>navigate(route: String)</code></b>: 使用字串路由導航(常用於 Compose),比 ID 導航更靈活,可以攜帶動態參數。 ```kotlin= navController.navigate("profile/{userId}") ``` - <b><code>setGraph(navGraphId: Int)</code></b>: 動態設定或重設導航圖,適合需要依照條件載入不同導航圖的情境,例如多入口點應用。 ```kotlin= navController.setGraph(R.navigation.new_nav_graph) ``` - <b><code>restoreState(savedState: Bundle?)</code></b>: 將保存的導航狀態恢復到 NavController,常用於在系統重建或頁面重啟時還原導航狀態。 - <b><code>saveState()</code></b>: 儲存當前的導航狀態,通常與 restoreState 搭配使用來還原頁面和導航狀態。 </div> </details> </div> --- ▼Q&A --- <details><summary>Q: 如果我導航圖中的 <code>popUpTo</code> 中,填入了導航圖本身會怎麼樣?</summary> <div> --- Ans:會跳出當前所在的導航圖 例如: 在 <code>action_fragmentB2_to_fragmentD2</code> 的 <code>popUpTo</code> 屬性上填入導航圖本身(也就是從 <code>nav_graph_a</code> 跳到 <code>nav_graph</code>),如果當前導航圖本身就在最底部了,那會跳出程式。 ![image](https://hackmd.io/_uploads/HyONanCzkx.png) </div> </details> --- ▼GitHub 專案 --- [MyPratice_Navigation_1](https://github.com/PudCheetah/MyPratice_Navigation_1.git) --- 下一篇: [Android 筆記– Navigation (二)使用 Safe Args 傳遞資料](https://hackmd.io/@9YAtszqXS2OLNZOrLY_-Jg/android_Navigation_SafeArgs) --- ▼參考資料: --- - [Jetpack-Navigation](https://cheenjame.medium.com/jetpack-navigation-7ca23543dec3) - [AndroidDeveloper--導航](https://developer.android.com/guide/navigation?hl=zh-tw) - [Day 23 Navigation (一) 介紹與基本使用](https://ithelp.ithome.com.tw/articles/10225937) - [Android Navigation 學習筆記(一) 基礎使用](https://medium.com/@wsrew2000/android-navigation-%E5%AD%B8%E7%BF%92%E7%AD%86%E8%A8%98-%E4%B8%80-%E5%9F%BA%E7%A4%8E%E4%BD%BF%E7%94%A8-3c1607ce4d38) - [Android Kotlin/Java Jetpack Navigation 介紹、入門教學example](https://willy2016.pixnet.net/blog/post/216916713) - [Navigation (1)](https://ithelp.ithome.com.tw/articles/10276248) - [Navigation (2)](https://ithelp.ithome.com.tw/articles/10276786)