片段和導覽元件

1. 事前準備

在「活動與意圖」程式碼研究室中,您在 Words 應用程式新增了在兩項活動之間導覽的意圖。雖然這是實用的導覽模式,但僅是為應用程式撰寫動態使用者介面的一部分。許多 Android 應用程式不需個別為每個畫面設定活動。事實上,許多常見的使用者介面模式 (如分頁標籤) 均存在單一活動內,並使用名為「片段」的組成部分。

64100b59bb487856.png

片段是可重複使用的使用者介面,並可嵌入一或多個活動中。在上方的螢幕截圖中,輕觸分頁標籤並不會觸發顯示下一個畫面的意圖。而是,切換分頁標籤僅僅是在先前片段與原先片段之間調換。這些事項都不需要啟動其他活動。

您甚至可以在單一畫面上一次顯示多個片段,例如平板電腦裝置的主控制項詳細資料版面配置。在以下範例中,左側的導覽 UI 和右側的內容都可以包含在不同的片段中。兩個片段在同一個活動中並存。

b5711344c5795d55.png

如您所見,片段是建構高品質應用程式的關鍵要素。在本程式碼研究室中,您將瞭解片段的基本概念,並轉換 Word 應用程式來使用片段。也會瞭解如何使用 Jetpack 導覽元件,以及使用名為導覽圖的新資源檔案,以在同一主機活動中導覽不同片段。完成本程式碼研究室後,您將獲得在下一個應用程式中導入片段的基本技能。

必要條件

在完成本程式碼研究室之前,請務必瞭解

  • 如何將資源 XML 檔案和 Kotlin 檔案新增至 Android Studio 專案。
  • 活動生命週期在高層級的運作方式。
  • 如何覆寫並導入現有類別的方法。
  • 如何建立 Kotlin 類別、存取類別屬性和呼叫方法的執行個體。
  • 對可為空值的值和不可為空值的值都有基本瞭解,並瞭解如何安全地處理空值的值。

課程內容

  • 片段生命週期與活動生命週期的差異。
  • 如何將現有活動轉換成片段。
  • 如何在導覽圖中新增目的地,以及在使用 Safe Args 外掛程式時在片段之間傳遞資料。

建構項目

  • 您必須修改 Word 應用程式以使用單一活動和多個片段,並在含導覽元件的不同片段之間切換。

需求條件

  • 已安裝 Android Studio 的電腦。
  • 活動與意圖程式碼研究室提供的 Word 應用程式解決方案程式碼

2. 範例程式碼

在本程式碼研究室中,在活動和意圖程式碼研究室的結束時,您就能使用 Word 應用程式接續先前的進度。如果您已完成「活動與意圖」程式碼研究室,可自由選擇從您的程式碼開始操作。或者,直到此刻,您也可以從 GitHub 下載程式碼。

下載本程式碼研究室的範例程式碼

本程式碼研究室提供範例程式碼,可延伸至本程式碼研究室所教授的功能。範例程式碼可能包含先前介紹過的程式碼。也可能含有您不熟悉的程式碼,您可以在後續的程式碼研究室中學習。

如果您使用 GitHub 中的範例程式碼,請注意資料夾名稱是 android-basics-kotlin-words-app-activities。在 Android Studio 中開啟專案時,請選取這個資料夾。

  1. 前往專案指定的 GitHub 存放區頁面。
  2. 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」

1e4c0d2c081a8fd2.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open」

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。

3. 片段與片段生命週期

片段僅僅是一段可重複使用的應用程式使用者介面 如同活動,片段具有生命週期且可回應使用者輸入。在畫面上顯示活動的檢視區塊階層內始終包含片段。由於片段強調可重用性和模組化,甚至可以由單一活動同時代管多個片段,所以每個片段都有各自的生命週期。

片段生命週期

如同活動,您也可以從記憶體初始化及移除片段,並且在片段存在期間,會在螢幕上顯示、消失和重新顯示。此外,就像活動一樣,片段的生命週期有多種狀態,片段也提供多個覆寫方法,用來回應片段之間的轉換。片段生命週期為五個狀態,以 Lifecycle.State 列舉表示。

  • INITIALIZED (已初始化):片段的新執行個體已執行個體化。
  • CREATED (已建立):呼叫第一個片段生命週期方法。在此狀態下,系統也會建立與片段相關聯的檢視畫面。
  • STARTED (已啟動):片段可在畫面上看見,但沒有「焦點」,因此無法回應使用者輸入。
  • RESUMED (已重新啟用):片段可在畫面上看見且有焦點。
  • DESTROYED (已刪除):片段物件已解除執行個體化。

也類似於活動,Fragment 類別提供多種方法,讓您可回應於生命週期事件進行覆寫。

  • onCreate():片段已執行個體化,並處於 CREATED 狀態。不過,尚未建立對應的檢視畫面。
  • onCreateView():這個方法是加載版面配置之處。片段已進入 CREATED 狀態。
  • onViewCreated():會在建立檢視畫面後呼叫。在此方法中,您通常會呼叫 findViewById() 來繫結特定檢視畫面與屬性。
  • onStart():片段已進入 STARTED 狀態。
  • onResume():片段已進入 RESUMED 狀態且現在已聚焦 (可回應使用者輸入)。
  • onPause():片段已重新進入 STARTED 狀態。使用者可看得到使用者介面
  • onStop():片段已重新進入 CREATED 狀態。物件已執行個體化,但不再顯示在畫面上。
  • onDestroyView():在片段正好進入 DESTROYED 狀態時呼叫。檢視畫面已從記憶體中移除,但片段物件仍然存在。
  • onDestroy():片段進入 DESTROYED 狀態。

下圖概述各種片段生命週期,以及各狀態之間的轉換。

8dc30a4c12ab71b.png

生命週期狀態和回呼方法非常類似於活動中使用的方法。不過,請記住與 onCreate() 方法的差異。配合活動,使用此方法加載版面配置並繫結檢視。不過,在片段生命週期內,系統會在建立檢視畫面之前呼叫 onCreate(),因此您無法在這裡加載版面配置。請改為在 onCreateView() 中執行這項操作。建立檢視畫面之後,系統會呼叫 onViewCreated() 方法,然後將屬性繫結至特定檢視畫面。

儘管似乎包含很多理論,但您現在已瞭解片段的基本運作方式,以及片段與活動的異同之處。在本程式碼研究室的其餘部分,您將充分學以致用。首先,您必須遷移先前使用 Words 應用程式至使用片段為基礎的版面配置。接下來,導入在單一活動中不同片段之間的導覽功能。

4. 建立片段和版面配置檔案

如同活動,您新增的每個片段都包含兩個檔案:一個檔案是版面配置的 XML 檔案,另一個檔案則是顯示資料和處理使用者互動的 Kotlin 類別。您必須新增字母清單和字詞清單的片段。

  1. 在「Project Navigator」(專案導覽器) 中選取「app」(應用程式),並加入下列片段 (「File」(檔案) >「New」(新增) >「Fragment」(片段) >「Fragment (Blank)」(片段 (空白))),應該會產生各片段的類別和版面配置檔案。
  • 將第一個片段的「Fragment Name」(片段名稱) 設定為 LetterListFragment。「Fragment Layout Name」(片段版面配置名稱) 應填入 fragment_letter_list

4a1729f01d62e65e.png

  • 將第二個片段的「Fragment Name」(片段名稱) 設定為 WordListFragment。「Fragment Layout Name」(片段版面配置名稱) 應填入 fragment_word_list.xml

5b86ff3a94833b5a.png

  1. 為這兩個片段產生的 Kotlin 類別都包含許多樣板程式碼,通常用於實作片段。不過,由於您是第一次使用片段,請刪除這兩個檔案中的所有內容,只保留 LetterListFragmentWordListFragment 的類別宣告。我們會引導您您從頭開始逐步實作片段,使您瞭解所有程式碼的運作方式。刪除樣板程式碼後,Kotlin 檔案應像如下所示。

LetterListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class LetterListFragment : Fragment() {

}

WordListFragment.kt

package com.example.wordsapp

import androidx.fragment.app.Fragment

class WordListFragment : Fragment() {

}
  1. activity_main.xml 的內容複製到 fragment_letter_list.xml,並將 activity_detail.xml 的內容複製到 fragment_word_list.xml。將 fragment_letter_list.xml 中的 tools:context 更新為 .LetterListFragment,並將 fragment_word_list.xml 中的 tools:context 更新為 .WordListFragment

變更後,片段版面配置檔案應像如下所示。

fragment_letter_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".LetterListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp" />

</FrameLayout>

fragment_word_list.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".WordListFragment">

   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/recycler_view"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:padding="16dp"
       tools:listitem="@layout/item_view" />

</FrameLayout>

5. 導入 LetterListFragment

如同活動,您需要加載版面配置並繫結個別檢視畫面。使用片段生命週期時,還是有些許差異。我們會引導您逐步完成 LetterListFragment 的設定程序,讓您有機會為 WordListFragment 進行相同設定。

若要在 LetterListFragment 中導入檢視畫面繫結,您必須先取得 FragmentLetterListBinding 可為空值的參照。在 build.gradle 檔案的 buildFeatures 區段下啟用 viewBinding 屬性時,Android Studio 會為每個版面配置檔案產生與此類似的繫結類別。您只需要為 FragmentLetterListBinding 中的每個檢視畫面指派片段類別中的屬性。

型別應為 FragmentLetterListBinding?,且初始值應為 null。為什麼要使其可為空值?因為除非呼叫 onCreateView(),否則您無法加載版面配置。建立 LetterListFragment 的例項 (生命週期從 onCreate() 開始) 後,要等待一段時間才能實際使用此屬性。也請注意,您可以在片段的整個生命週期內多次建立和刪除片段的檢視畫面。因此,您還必須重設在另一個生命週期方法 (onDestroyView()) 中的值。

  1. LetterListFragment.kt 中,會先取得 FragmentLetterListBinding 的參照,並將參照命名為 _binding
private var _binding: FragmentLetterListBinding? = null

這是可為空值,因此每次存取 _binding 的屬性 (例如 _binding?.someView) 時,您都必須納入 ? 來提供空值安全。然而,這並不意謂您會因為一個空值,而捨棄有問號的程式碼。如果您確定某值在存取時不會為空值,則可以在類型名稱中附加 !!。於是您就不需使用 ? 運算子,也能和任何其他屬性一樣進行存取。

  1. 建立名為 binding 的新屬性 (不含底線),並將其設為等於 _binding!!
private val binding get() = _binding!!

此處,get() 意指屬性是「get-only」。這意指您可以「get」(取得) 這個值,但一旦指派 (如此處),就無法指派後給其他。

  1. 如要顯示選項選單,請覆寫 onCreate()。在 onCreate() 內呼叫 setHasOptionsMenu() 並傳入 true
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   setHasOptionsMenu(true)
}
  1. 請記住,使用片段時,系統會在 onCreateView() 中加載版面配置。加載檢視畫面、設定_binding 的值,並傳回根層級檢視畫面,就可導入 onCreateView()
override fun onCreateView(
   inflater: LayoutInflater, container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentLetterListBinding.inflate(inflater, container, false)
   val view = binding.root
   return view
}
  1. binding 屬性下方,建立回收器檢視畫面的屬性。
private lateinit var recyclerView: RecyclerView
  1. 然後在 onViewCreated() 中設定 recyclerView 屬性的值,並呼叫 chooseLayout(),就像您在 MainActivity 中的做法一樣。您很快就會將 chooseLayout() 方法移到 LetterListFragment,所以不需擔心有錯誤。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   chooseLayout()
}

請注意,繫結類別已經建立 recyclerView 的屬性,因此您不需要針對每個檢視畫面呼叫 findViewById()

  1. 最後在 onDestroyView() 中,將 _binding 屬性重設為 null,因為檢視畫面已不存在。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. 另外要注意的唯一事項是,使用片段時,onCreateOptionsMenu() 方法有些微的差異。雖然 Activity 類別具有名為 menuInflater 的全域屬性,但 Fragment 並未提供這項屬性,而是將選單 inflater 傳遞至 onCreateOptionsMenu() 中。另請注意,搭配片段使用的 onCreateOptionsMenu() 方法不需要回傳敘述。實作方法如下所示:
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.layout_menu, menu)

   val layoutButton = menu.findItem(R.id.action_switch_layout)
   setIcon(layoutButton)
}
  1. MainActivity 中按原樣移動 chooseLayout()setIcon()onOptionsItemSelected() 的其餘程式碼。應注意的唯一差別在於,與活動不同,片段不是 Context。您無法傳入 this (指片段物件) 做為版面配置管理工具的內容。但是,片段會提供 context 屬性,您可以改用該屬性。程式碼的其餘部分與 MainActivity 相同。
private fun chooseLayout() {
   when (isLinearLayoutManager) {
       true -> {
           recyclerView.layoutManager = LinearLayoutManager(context)
           recyclerView.adapter = LetterAdapter()
       }
       false -> {
           recyclerView.layoutManager = GridLayoutManager(context, 4)
           recyclerView.adapter = LetterAdapter()
       }
   }
}

private fun setIcon(menuItem: MenuItem?) {
   if (menuItem == null)
       return

   menuItem.icon =
       if (isLinearLayoutManager)
           ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_grid_layout)
       else ContextCompat.getDrawable(this.requireContext(), R.drawable.ic_linear_layout)
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           isLinearLayoutManager = !isLinearLayoutManager
           chooseLayout()
           setIcon(item)

           return true
       }

       else -> super.onOptionsItemSelected(item)
   }
}
  1. 最後,複製 MainActivityisLinearLayoutManager 屬性。將此屬性放在 recyclerView 屬性的宣告正下方。
private var isLinearLayoutManager = true
  1. 現在所有功能都已經移至 LetterListFragmentMainActivity 類別所需要做的只是加載版面配置,使得片段顯示在檢視畫面中。繼續從 MainActivity.刪除所有內容,惟 onCreate() 除外。變更後,MainActivity 只能包含下列項目。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

換您囉

就是將 MainActivity 遷移至 LettersListFragmentDetailActivity 的遷移作業幾乎相同。請執行下列步驟,將程式碼遷移至 WordListFragment

  1. 將夥伴模式物件從 DetailActivity 複製到 WordListFragment。確保 WordAdapter 中的 SEARCH_PREFIX 參照已更新為參照 WordListFragment
  2. 新增 _binding 變數。變數應可為空值,並將 null 作為初始值。
  3. 新增名為 binding 的 get-only 變數 (等於 _binding 變數)。
  4. onCreateView() 中加載版面配置、設定 _binding 的值並傳回根層級檢視畫面。
  5. 完成 onViewCreated() 中的任何其餘設定:取得回收業者檢視畫面的參照、設定版面配置管理員和轉接程式,以及新增其項目裝飾。您必須從意圖中取得字母。由於片段沒有 intent 屬性,因此通常不應存取父項活動的意圖。目前您參照 activity.intent (而非 DetailActivity 中的 intent),以獲得額外項目。
  6. onDestroyView 中將 _binding 重設為空值。
  7. 刪除 DetailActivity 中的其餘程式碼,只保留 onCreate() 方法。

在繼續之前,請嘗試自己完成這些步驟。下一個步驟將列出詳細的逐步操作說明。

6. 將 DetailActivity 轉換為 WordListFragment

希望您很喜歡有機會 DetailActivity 遷移至 WordListFragment。這幾乎與將 MainActivity 遷移至 LetterListFragment 的方法相同。如果您在任何時候遇到困難,這些步驟總結如下。

  1. 首先,請將夥伴模式物件複製到 WordListFragment
companion object {
   val LETTER = "letter"
   val SEARCH_PREFIX = "https://www.google.com/search?q="
}
  1. 然後在 LetterAdapter 中,在執行意圖的 onClickListener() 中,您必須將呼叫更新為 putExtra(),將 DetailActivity.LETTER 替換為 WordListFragment.LETTER
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
  1. 同樣地,在 WordAdapter 中,在您前往字詞的搜尋結果時必須更新出現 onClickListener(),並將 DetailActivity.SEARCH_PREFIX 替換為 WordListFragment.SEARCH_PREFIX
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
  1. 返回 WordListFragment,新增 FragmentWordListBinding? 型別的繫結變數。
private var _binding: FragmentWordListBinding? = null
  1. 然後建立 get-only 變數,這樣無需使用 ? 就能參照檢視畫面。
private val binding get() = _binding!!
  1. 接著,請調整版面配置,指派 _binding 變數並傳回根層級檢視畫面。請記得,對於片段,您在 onCreateView() 中執行此作業,而不是 onCreate()
override fun onCreateView(
   inflater: LayoutInflater,
   container: ViewGroup?,
   savedInstanceState: Bundle?
): View? {
   _binding = FragmentWordListBinding.inflate(inflater, container, false)
   return binding.root
}
  1. 接下來,您要導入 onViewCreated()。這幾乎和在 DetailActivity 中的 onCreate() 中設定 recyclerView 相同。不過,由於片段無法直接存取 intent,因此您必須使用 activity.intent 進行參照。但是,您必須在 onViewCreated() 中執行此作業,因為無法保證在生命週期早期已存在活動。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   val recyclerView = binding.recyclerView
   recyclerView.layoutManager = LinearLayoutManager(requireContext())
   recyclerView.adapter = WordAdapter(activity?.intent?.extras?.getString(LETTER).toString(), requireContext())

   recyclerView.addItemDecoration(
       DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
   )
}
  1. 最後,您可以在 onDestroyView() 中重設 _binding 變數。
override fun onDestroyView() {
   super.onDestroyView()
   _binding = null
}
  1. 這些功能全都移至 WordListFragment 後,您就可以從 DetailActivity 刪除程式碼。您只須保留 onCreate() 方法。
override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)

   val binding = ActivityDetailBinding.inflate(layoutInflater)
   setContentView(binding.root)
}

移除 DetailActivity

現在,您已順利將 DetailActivity 的功能遷移至 WordListFragment,所以不再需要 DetailActivity。您可以繼續刪除 DetailActivity.ktactivity_detail.xml,也可以對資訊清單進行小幅變更。

  1. 首先,請刪除 DetailActivity.kt

2b13b08ac9442ae5.png

  1. 確認已取消勾選「Safe Delete」,然後按一下「OK」。

239f048d945ab1f9.png

  1. 接著刪除 activity_detail.xml。再次確認已取消勾選「Safe Delete」(安全刪除)。

774c8b152c5bff6b.png

  1. 最後,由於 DetailActivity 已不存在,請將下列項目從 AndroidManifest.xml 中移除。
<activity
   android:name=".DetailActivity"
   android:parentActivityName=".MainActivity" />

刪除詳細活動後,剩下兩個片段 (LetterListFragment 和 WordListFragment) 和單一活動 (MainActivity)。下一節將介紹 Jetpack Navigation 元件並編輯 activity_main.xml,讓該元件可顯示片段並在片段之間導覽,而不是代管靜態版面配置。

7. Jetpack 導覽元件

Android Jetpack 提供導覽元件,可協助您在應用程式中處理任何簡易或複雜的導覽實作。導覽元件包含三個主要部分,可供您在 Words 應用程式中導入導覽功能。

  • 導覽圖:導覽圖是 XML 檔案,能以圖表呈現應用程式中的導覽功能。此檔案包含與個別活動和片段對應的「目的地」,以及片段之間的動作。在程式碼中,動作可用來執行目的地之間的導覽。就如同版面配置檔案,Android Studio 提供視覺編輯器,可用於在導覽圖中加入目的地和動作。
  • NavHostNavHost 是用來顯示在活動內來自導覽圖的目的地。當您在片段之間導覽時,NavHost 中顯示的目的地也會隨之更新。您需要在 MainActivity 中使用內建的實作,名為 NavHostFragment
  • NavControllerNavController 物件可讓您控制 NavHost 中顯示的目的地之間的導覽動作。使用意圖時,您必須呼叫 startActivity 才能前往新的畫面。您可以使用 Navigation 元件呼叫 NavControllernavigate() 方法,調換所顯示的片段。NavController 也有助於您處理一般工作,例如回應系統「向上」按鈕,即可返回先前顯示的片段。
  1. 在專案層級的 build.gradle 檔案中,於 buildscript > ext material_version 下方,將 nav_version 設為等於 2.5.2
buildscript {
    ext {
        appcompat_version = "1.5.1"
        constraintlayout_version = "2.1.4"
        core_ktx_version = "1.9.0"
        kotlin_version = "1.7.10"
        material_version = "1.7.0-alpha2"
        nav_version = "2.5.2"
    }

    ...
}
  1. 在應用程式層級的 build.gradle 檔案中,將以下內容加入依附元件群組:
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

Safe Args 外掛程式

初次在 Words 應用程式中導入導覽時,您使用這兩種活動之間的明確意圖。若要在兩個活動之間傳遞資料,您必須呼叫 putExtra() 方法,並傳入所選字母。

將導覽元件導入至 Words 應用程式之前,建議您也新增名為 Safe Args 的 Gradle 外掛程式,協助您在片段之間傳遞資料時確保型別安全。

請執行下列步驟,將 SafeArgs 整合至您的專案。

  1. 在頂層 build.gradle 檔案中,於 buildscript > dependencies 新增下列類別路徑。
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
  1. 在應用程式層級的 build.gradle 檔案中,在 plugins 內的頂端新增 androidx.navigation.safeargs.kotlin
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'androidx.navigation.safeargs.kotlin'
}
  1. 編輯 Gradle 檔案後,頁面頂端會顯示黃色橫幅,要求您同步處理專案。按一下「Sync Now」,等待一兩分鐘讓 Gradle 更新專案的依附元件,反映所做變更。

854d44a6f7c4c080.png

同步完成後,您就可以進行下一步來新增導覽圖。

8. 使用導覽圖

現在您對片段和生命週期都有基本瞭解,接著就要開始更有趣的事情。下一步是納入導覽元件。導覽元件只是一系列導入尤其在片段之間導覽的工具集合。您將使用新的視覺編輯器協助導入片段之間的導覽;導覽圖 (簡稱 NavGraph)。

什麼是導覽圖?

導覽圖 (簡稱 NavGraph) 是應用程式導覽的虛擬對應 在這種情況下,每個螢幕或片段都變成一個可以前往的可能「目的地」。NavGraph 能以 XML 檔案表示,表明每個目的地彼此之間有何關聯。

這項功能會在幕後建立 NavGraph 類別的新執行個體。不過,FragmentContainerView 會向使用者顯示導覽圖中的目的地。您只需建立 XML 檔案並定義可能的目的地即可。然後使用產生的程式碼以在片段之間導覽。

在 MainActivity 中使用 FragmentContainerView

由於版面配置已包含在 fragment_letter_list.xmlfragment_word_list.xml 中,因此 activity_main.xml 檔案不再需要包含應用程式中第一個畫面的版面配置。而是重複利用 MainActivity 以包含 FragmentContainerView 來作為片段的 NavHost。從現在開始,應用程式中的所有導覽動作都會發生在 FragmentContainerView 中。

  1. activity_main.xml (即 androidx.recyclerview.widget.RecyclerView) 中的 FrameLayout 內容替換為 FragmentContainerView。將此 ID 設定為 nav_host_fragment,並將高度和寬度設定為 match_parent,即可填滿整個頁框版面配置。

替換為:

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        ...
        android:padding="16dp" />

使用:

<androidx.fragment.app.FragmentContainerView
   android:id="@+id/nav_host_fragment"
   android:layout_width="match_parent"
   android:layout_height="match_parent" />
  1. 在 ID 屬性下方新增 name 屬性,並設定為 androidx.navigation.fragment.NavHostFragment。雖然您可以針對此屬性指定特定片段,但設定為 NavHostFragment 即可讓 FragmentContainerView 在片段之間導覽。
android:name="androidx.navigation.fragment.NavHostFragment"
  1. 在 layout_height 和 layout_width 屬性下方新增 app:defaultNavHost 屬性,並設定為等於 "true"。如此一來,片段容器就可以與導覽階層互動。舉例來說,按下系統返回按鈕後,容器就會回到先前顯示的片段,就像顯示新活動時的情況一樣。
app:defaultNavHost="true"
  1. 新增名為 app:navGraph 的屬性,並將該屬性設定為等於 "@navigation/nav_graph"。這會指向一個 XML 檔案,用於定義應用程式片段之間的導覽方式。Android Studio 暫時會顯示尚未解決的符號錯誤。您會在接下來的工作中解決這個問題。
app:navGraph="@navigation/nav_graph"
  1. 最後,由於您使用應用程式命名空間新增了兩項屬性,因此務必在 FrameLayout 中加入 xmlns:app 屬性。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:tools="http://schemas.android.com/tools"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

這些就是 activity_main.xml 中的所有變更。接下來,您將建立 nav_graph 檔案。

設定導覽圖

新增導覽圖檔案 (「File」(檔案)>「New」(新增) >「Android Resource File」(Android 資源檔案)),並按照以下方式填寫各欄位。

  • 檔案名稱:nav_graph.xml. 這個名稱與您為 app:navGraph 屬性設定的名稱相同。
  • 資源類型:「Navigation」。系統隨即會將「Directory Name」自動變更為 navigation然後建立名為「navigation」的新資源資料夾。

6812c83aa1e9cea6.png

建立 XML 檔案時,系統會顯示新的視覺編輯器。由於您已參照 FragmentContainerViewnavGraph 屬性中的 nav_graph,如要新增目的地,請按一下畫面左上方的新增按鈕,然後為每個片段建立目的地 (一個用於 fragment_letter_list,另一個用於 fragment_word_list)。

dc2b53782de5e143.gif

新增完成後,這些片段應該就會顯示在畫面中央的導覽圖上。您也可以使用左側顯示的元件樹狀結構來選取特定目的地。

建立導覽動作

如要建立目的地 letterListFragmentwordListFragment 之間的導覽動作,請將滑鼠游標懸停在 letterListFragment 目的地上方,然後從右側顯示的圓圈拖曳至 wordListFragment 目的地。

980cb34d800c7155.gif

現在您應該會看到已建立的箭頭,用來表示兩個目的地之間的動作。點選箭頭後,屬性窗格就會顯示名為 action_letterListFragment_to_wordListFragment 的動作,您可以在程式碼中參照該動作。

指定 WordListFragment 的引數

使用意圖在活動之間導覽時,您指定了「extra」(額外項目),使得所選字母可傳遞到 wordListFragment。導覽也支援在目的地之間傳遞參數,並以型別安全的方式完成這項操作。

選取 wordListFragment 目的地,然後在屬性窗格的「Arguments」下方,按一下加號按鈕建立新的引數。

引數應名為 letter,型別應為 String。您先前新增的 Safe Args 外掛程式就能派上用場。將這個引數指定為字串,可確保在程式碼中執行導覽動作時,String 會如預期運作。

f1541e01d3462f3e.png

設定起始目的地

雖然您的 NavGraph 得知所有必要的目的地時,但是 FragmentContainerView 會如何得知應優先顯示哪個片段?在 NavGraph 中,您必須將字母清單設定為起始目的地。

如要設定起始目的地,請選取 letterListFragment,然後按一下「Assign start destination」按鈕。

3fdb226894152fb0.png

  1. 這就是您目前需要在 NavGraph 編輯器執行的所有操作。此時,您就可以繼續並建立專案。在 Android Studio 中,從選單列依序選取「Build」>「Rebuild Project」。系統會根據您的導覽圖產生一些程式碼,方便您使用剛建立的導覽動作。

執行導覽動作

開啟 LetterAdapter.kt 以執行導覽動作。只需要兩個步驟即可完成。

  1. 刪除按鈕 setOnClickListener() 的內容。您必須改為擷取剛建立的導覽動作。將以下內容新增至 setOnClickListener()
val action = LetterListFragmentDirections.actionLetterListFragmentToWordListFragment(letter = holder.button.text.toString())

系統可能無法辨識部分的類別和函式名稱,原因是在建構專案後已自動產生這些名稱。如此一來,您在第一個步驟新增的 Safe Args 外掛程式就能派上用場,而在 NavGraph 中建立的動作會轉為您可以使用的程式碼。然而,這些名稱應該要相當直覺易懂。LetterListFragmentDirections 可讓您參照以 letterListFragment 為起點的所有可能導覽路徑。

actionLetterListFragmentToWordListFragment() 函式

是前往 wordListFragment 的特定動作。

取得導覽動作的參照後,只要取得「NavController」(此物件可用來執行導覽動作) 的參照,並呼叫 navigate() 傳入該動作即可。

holder.view.findNavController().navigate(action)

設定 MainActivity

最後一段設定位於 MainActivity 中。僅需要少許變更 MainActivity 就可確保一切運作正常。

  1. 建立 navController 屬性。此標記為 onCreate,因為會在 lateinit 中進行設定。
private lateinit var navController: NavController
  1. 然後,在 onCreate() 中呼叫 setContentView() 後,取得對 nav_host_fragment 的參照 (這是 FragmentContainerView 的 ID),並指派給 navController 屬性。
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
  1. 然後,在 onCreate() 中呼叫 setupActionBarWithNavController(),並傳入 navController。這可確保畫面上會顯示動作列 (應用程式列) 按鈕,例如 LetterListFragment 中的選單選項。
setupActionBarWithNavController(navController)
  1. 最後,導入 onSupportNavigateUp()。除了在 XML 中將 defaultNavHost 設定為 true 之外,此方法也可用來處理向上按鈕。不過,您的活動必須提供實作。
override fun onSupportNavigateUp(): Boolean {
   return navController.navigateUp() || super.onSupportNavigateUp()
}

到目前為止,所有元件就緒可以進行導覽,這樣就能使用片段。不過,現在導覽功能是使用片段而非意圖執行,因此您在 WordListFragment 中使用的字母的意圖額外項目將不再有效。在下一個步驟中,您必須更新 WordListFragment 以取得 letter 引數。

9. 用 WordListFragment 取得引數

您先前曾在 WordListFragment 中參照 activity?.intent 以存取 letter 額外項目。這雖然有效,但不是最佳做法,因為片段可嵌入其他版面配置,而在大型應用程式中,系統較難假設片段屬於哪個活動。此外,使用 nav_graph 執行導覽且搭配使用安全引數時,並沒有任何意圖,因此嘗試存取意圖額外項目根本不可行。

幸好,存取安全引數非常簡單,也不必等到呼叫 onViewCreated()

  1. WordListFragment 中建立 letterId 屬性。您可以將此屬性標記為 lateinit,而不必將其設為可為空值。
private lateinit var letterId: String
  1. 接著覆寫 onCreate() (而不是 onCreateView()onViewCreated()!),並加入以下內容:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {
        letterId = it.getString(LETTER).toString()
    }
}

由於 arguments 可能是選用性質,因此請務必呼叫 let() 並傳入 lambda。這段程式碼會假設 arguments 不是空值,並傳入 it 參數的非空值引數。不過,如果 argumentsnull,則 lambda 將不會執行。

96a6a3253cea35b0.png

雖然 Android Studio 不是實際程式碼之部分,但還提供實用的提示,讓您瞭解 it 參數。

Bundle 到底是什麼?可視為用來在類別 (例如活動和片段) 之間傳遞資料的鍵/值組合。實際上,當您在這個應用程式的第一個版本中執行意圖時,呼叫 intent?.extras?.getString() 時已經使用套裝組合。使用片段時,從引數取得字串的方式完全相同。

  1. 最後,當您設定回收業者的轉接程式時,您可以存取 letterId。將 onViewCreated() 中的 activity?.intent?.extras?.getString(LETTER).toString() 替換成 letterId
recyclerView.adapter = WordAdapter(letterId, requireContext())

您成功了!請花點時間執行應用程式。現在,不需要任何意圖就可以在兩個畫面之間導覽,並且皆在單一活動中。

10. 更新片段標籤

您已成功將這兩個畫面轉換成使用片段。在進行變更之前,每個片段的應用程式列都會針對應用程式列中的每個活動提供描述性標題。然而,一旦轉換成使用片段,這個標題就不會出現在詳細資料活動中。

c385595994ba91b5.png

片段有一個名為 "label" 的屬性,您可以在其中設定父項活動要在應用程式列中使用的標題。

  1. strings.xml 中,在應用程式名稱之後,新增下列常數。
<string name="word_list_fragment_label">Words That Start With {letter}</string>
  1. 您可以在導覽圖中設定每個片段的標籤。返回 nav_graph.xml 中,在元件樹狀結構中選取 letterListFragment,然後前往屬性窗格將標籤設定為 app_name 字串:

a5ffe7a27aa03750.png

  1. 選取 wordListFragment,並將標籤設定為 word_list_fragment_label

29c206f03a97557b.png

恭喜您完成目前為止的工作!請再次執行應用程式,您應該會看到程式碼研究室一開始呈現的模樣,不過現在所有導覽都已透過單一活動代管,且每個畫面都設有獨立的片段。

11. 解決方案程式碼

本程式碼研究室的解決方案程式碼位於下方所示專案中。

  1. 前往專案指定的 GitHub 存放區頁面。
  2. 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」

1e4c0d2c081a8fd2.png

  1. 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。

1debcf330fd04c7b.png

  1. 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
  2. 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
  3. 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。

在 Android Studio 中開啟專案

  1. 啟動 Android Studio。
  2. 在「Welcome to Android Studio」視窗中,按一下「Open」

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。

8d1fda7396afe8e5.png

  1. 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
  2. 按兩下該專案資料夾。
  3. 等待 Android Studio 開啟專案。
  4. 按一下「Run」按鈕 8de56cba7583251f.png 即可建構並執行應用程式。請確認應用程式的建構符合預期。

12. 摘要

  • 片段是可嵌入活動中可重複使用的使用者介面。
  • 片段的生命週期與活動生命週期不同,而檢視畫面設定發生在 onViewCreated()中,而不是 onCreateView()
  • FragmentContainerView 可用來將片段嵌入至其他活動中,以及管理片段之間的導覽。

使用導覽元件

  • 設定 FragmentContainerViewnavGraph 屬性可讓您在活動內的不同片段之間進行導覽。
  • NavGraph 編輯器可讓您新增導覽動作,以及指定不同目的地的之間的引數。
  • 使用意圖進行導覽時,您必須傳入額外項目,導覽元件會使用 SafeArgs 自動為導覽動作產生類別和方法,以確保引數的類型安全性。

片段的用途

  • 使用導覽元件時,許多應用程式可在單一活動中管理整個版面配置,且所有導覽動作都在片段之間進行。
  • 片段可讓您使用常見的版面配置模式,例如平板電腦上的主控制項/詳細資料版面配置,或是相同活動中的多個分頁。

13. 瞭解詳情