1. 事前準備
在「活動與意圖」程式碼研究室中,您在 Words 應用程式新增了在兩項活動之間導覽的意圖。雖然這是實用的導覽模式,但僅是為應用程式撰寫動態使用者介面的一部分。許多 Android 應用程式不需個別為每個畫面設定活動。事實上,許多常見的使用者介面模式 (如分頁標籤) 均存在單一活動內,並使用名為「片段」的組成部分。
片段是可重複使用的使用者介面,並可嵌入一或多個活動中。在上方的螢幕截圖中,輕觸分頁標籤並不會觸發顯示下一個畫面的意圖。而是,切換分頁標籤僅僅是在先前片段與原先片段之間調換。這些事項都不需要啟動其他活動。
您甚至可以在單一畫面上一次顯示多個片段,例如平板電腦裝置的主控制項詳細資料版面配置。在以下範例中,左側的導覽 UI 和右側的內容都可以包含在不同的片段中。兩個片段在同一個活動中並存。
如您所見,片段是建構高品質應用程式的關鍵要素。在本程式碼研究室中,您將瞭解片段的基本概念,並轉換 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 中開啟專案時,請選取這個資料夾。
- 前往專案指定的 GitHub 存放區頁面。
- 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。
- 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open」。
注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。
- 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
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
狀態。
下圖概述各種片段生命週期,以及各狀態之間的轉換。
生命週期狀態和回呼方法非常類似於活動中使用的方法。不過,請記住與 onCreate()
方法的差異。配合活動,使用此方法加載版面配置並繫結檢視。不過,在片段生命週期內,系統會在建立檢視畫面之前呼叫 onCreate()
,因此您無法在這裡加載版面配置。請改為在 onCreateView()
中執行這項操作。建立檢視畫面之後,系統會呼叫 onViewCreated()
方法,然後將屬性繫結至特定檢視畫面。
儘管似乎包含很多理論,但您現在已瞭解片段的基本運作方式,以及片段與活動的異同之處。在本程式碼研究室的其餘部分,您將充分學以致用。首先,您必須遷移先前使用 Words 應用程式至使用片段為基礎的版面配置。接下來,導入在單一活動中不同片段之間的導覽功能。
4. 建立片段和版面配置檔案
如同活動,您新增的每個片段都包含兩個檔案:一個檔案是版面配置的 XML 檔案,另一個檔案則是顯示資料和處理使用者互動的 Kotlin 類別。您必須新增字母清單和字詞清單的片段。
- 在「Project Navigator」(專案導覽器) 中選取「app」(應用程式),並加入下列片段 (「File」(檔案) >「New」(新增) >「Fragment」(片段) >「Fragment (Blank)」(片段 (空白))),應該會產生各片段的類別和版面配置檔案。
- 將第一個片段的「Fragment Name」(片段名稱) 設定為
LetterListFragment
。「Fragment Layout Name」(片段版面配置名稱) 應填入fragment_letter_list
。
- 將第二個片段的「Fragment Name」(片段名稱) 設定為
WordListFragment
。「Fragment Layout Name」(片段版面配置名稱) 應填入fragment_word_list.xml
。
- 為這兩個片段產生的 Kotlin 類別都包含許多樣板程式碼,通常用於實作片段。不過,由於您是第一次使用片段,請刪除這兩個檔案中的所有內容,只保留
LetterListFragment
和WordListFragment
的類別宣告。我們會引導您您從頭開始逐步實作片段,使您瞭解所有程式碼的運作方式。刪除樣板程式碼後,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() {
}
- 將
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()
) 中的值。
- 在
LetterListFragment.kt
中,會先取得FragmentLetterListBinding
的參照,並將參照命名為_binding
。
private var _binding: FragmentLetterListBinding? = null
這是可為空值,因此每次存取 _binding
的屬性 (例如 _binding?.someView
) 時,您都必須納入 ?
來提供空值安全。然而,這並不意謂您會因為一個空值,而捨棄有問號的程式碼。如果您確定某值在存取時不會為空值,則可以在類型名稱中附加 !!
。於是您就不需使用 ?
運算子,也能和任何其他屬性一樣進行存取。
- 建立名為 binding 的新屬性 (不含底線),並將其設為等於
_binding!!
。
private val binding get() = _binding!!
此處,get()
意指屬性是「get-only」。這意指您可以「get」(取得) 這個值,但一旦指派 (如此處),就無法指派後給其他。
- 如要顯示選項選單,請覆寫
onCreate()
。在onCreate()
內呼叫setHasOptionsMenu()
並傳入true
。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
- 請記住,使用片段時,系統會在
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
}
- 在
binding
屬性下方,建立回收器檢視畫面的屬性。
private lateinit var recyclerView: RecyclerView
- 然後在
onViewCreated()
中設定recyclerView
屬性的值,並呼叫chooseLayout()
,就像您在MainActivity
中的做法一樣。您很快就會將chooseLayout()
方法移到LetterListFragment
,所以不需擔心有錯誤。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
recyclerView = binding.recyclerView
chooseLayout()
}
請注意,繫結類別已經建立 recyclerView
的屬性,因此您不需要針對每個檢視畫面呼叫 findViewById()
。
- 最後在
onDestroyView()
中,將_binding
屬性重設為null
,因為檢視畫面已不存在。
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- 另外要注意的唯一事項是,使用片段時,
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)
}
- 從
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)
}
}
- 最後,複製
MainActivity
的isLinearLayoutManager
屬性。將此屬性放在recyclerView
屬性的宣告正下方。
private var isLinearLayoutManager = true
- 現在所有功能都已經移至
LetterListFragment
,MainActivity
類別所需要做的只是加載版面配置,使得片段顯示在檢視畫面中。繼續從MainActivity
.刪除所有內容,惟onCreate()
除外。變更後,MainActivity
只能包含下列項目。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
換您囉
就是將 MainActivity
遷移至 LettersListFragment
。DetailActivity
的遷移作業幾乎相同。請執行下列步驟,將程式碼遷移至 WordListFragment
。
- 將夥伴模式物件從
DetailActivity
複製到WordListFragment
。確保WordAdapter
中的SEARCH_PREFIX
參照已更新為參照WordListFragment
。 - 新增
_binding
變數。變數應可為空值,並將null
作為初始值。 - 新增名為 binding 的 get-only 變數 (等於
_binding
變數)。 - 在
onCreateView()
中加載版面配置、設定_binding
的值並傳回根層級檢視畫面。 - 完成
onViewCreated()
中的任何其餘設定:取得回收業者檢視畫面的參照、設定版面配置管理員和轉接程式,以及新增其項目裝飾。您必須從意圖中取得字母。由於片段沒有intent
屬性,因此通常不應存取父項活動的意圖。目前您參照activity.intent
(而非DetailActivity
中的intent
),以獲得額外項目。 - 在
onDestroyView
中將_binding
重設為空值。 - 刪除
DetailActivity
中的其餘程式碼,只保留onCreate()
方法。
在繼續之前,請嘗試自己完成這些步驟。下一個步驟將列出詳細的逐步操作說明。
6. 將 DetailActivity 轉換為 WordListFragment
希望您很喜歡有機會 DetailActivity
遷移至 WordListFragment
。這幾乎與將 MainActivity
遷移至 LetterListFragment
的方法相同。如果您在任何時候遇到困難,這些步驟總結如下。
- 首先,請將夥伴模式物件複製到
WordListFragment
。
companion object {
val LETTER = "letter"
val SEARCH_PREFIX = "https://www.google.com/search?q="
}
- 然後在
LetterAdapter
中,在執行意圖的onClickListener()
中,您必須將呼叫更新為putExtra()
,將DetailActivity.LETTER
替換為WordListFragment.LETTER
。
intent.putExtra(WordListFragment.LETTER, holder.button.text.toString())
- 同樣地,在
WordAdapter
中,在您前往字詞的搜尋結果時必須更新出現onClickListener()
,並將DetailActivity.SEARCH_PREFIX
替換為WordListFragment.SEARCH_PREFIX
。
val queryUrl: Uri = Uri.parse("${WordListFragment.SEARCH_PREFIX}${item}")
- 返回
WordListFragment
,新增FragmentWordListBinding?
型別的繫結變數。
private var _binding: FragmentWordListBinding? = null
- 然後建立 get-only 變數,這樣無需使用
?
就能參照檢視畫面。
private val binding get() = _binding!!
- 接著,請調整版面配置,指派
_binding
變數並傳回根層級檢視畫面。請記得,對於片段,您在onCreateView()
中執行此作業,而不是onCreate()
。
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentWordListBinding.inflate(inflater, container, false)
return binding.root
}
- 接下來,您要導入
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)
)
}
- 最後,您可以在
onDestroyView()
中重設_binding
變數。
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- 這些功能全都移至 WordListFragment 後,您就可以從 DetailActivity 刪除程式碼。您只須保留 onCreate() 方法。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
}
移除 DetailActivity
現在,您已順利將 DetailActivity
的功能遷移至 WordListFragment
,所以不再需要 DetailActivity
。您可以繼續刪除 DetailActivity.kt
和 activity_detail.xml
,也可以對資訊清單進行小幅變更。
- 首先,請刪除
DetailActivity.kt
- 確認已取消勾選「Safe Delete」,然後按一下「OK」。
- 接著刪除
activity_detail.xml
。再次確認已取消勾選「Safe Delete」(安全刪除)。
- 最後,由於
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 提供視覺編輯器,可用於在導覽圖中加入目的地和動作。
NavHost
:NavHost
是用來顯示在活動內來自導覽圖的目的地。當您在片段之間導覽時,NavHost
中顯示的目的地也會隨之更新。您需要在MainActivity
中使用內建的實作,名為NavHostFragment
。NavController
:NavController
物件可讓您控制NavHost
中顯示的目的地之間的導覽動作。使用意圖時,您必須呼叫 startActivity 才能前往新的畫面。您可以使用 Navigation 元件呼叫NavController
的navigate()
方法,調換所顯示的片段。NavController
也有助於您處理一般工作,例如回應系統「向上」按鈕,即可返回先前顯示的片段。
導覽依附元件
- 在專案層級的
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"
}
...
}
- 在應用程式層級的
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 整合至您的專案。
- 在頂層
build.gradle
檔案中,於 buildscript > dependencies 新增下列類別路徑。
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
- 在應用程式層級的
build.gradle
檔案中,在plugins
內的頂端新增androidx.navigation.safeargs.kotlin
。
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'androidx.navigation.safeargs.kotlin'
}
- 編輯 Gradle 檔案後,頁面頂端會顯示黃色橫幅,要求您同步處理專案。按一下「Sync Now」,等待一兩分鐘讓 Gradle 更新專案的依附元件,反映所做變更。
同步完成後,您就可以進行下一步來新增導覽圖。
8. 使用導覽圖
現在您對片段和生命週期都有基本瞭解,接著就要開始更有趣的事情。下一步是納入導覽元件。導覽元件只是一系列導入尤其在片段之間導覽的工具集合。您將使用新的視覺編輯器協助導入片段之間的導覽;導覽圖 (簡稱 NavGraph)。
什麼是導覽圖?
導覽圖 (簡稱 NavGraph) 是應用程式導覽的虛擬對應 在這種情況下,每個螢幕或片段都變成一個可以前往的可能「目的地」。NavGraph
能以 XML 檔案表示,表明每個目的地彼此之間有何關聯。
這項功能會在幕後建立 NavGraph
類別的新執行個體。不過,FragmentContainerView
會向使用者顯示導覽圖中的目的地。您只需建立 XML 檔案並定義可能的目的地即可。然後使用產生的程式碼以在片段之間導覽。
在 MainActivity 中使用 FragmentContainerView
由於版面配置已包含在 fragment_letter_list.xml
和 fragment_word_list.xml
中,因此 activity_main.xml
檔案不再需要包含應用程式中第一個畫面的版面配置。而是重複利用 MainActivity
以包含 FragmentContainerView
來作為片段的 NavHost。從現在開始,應用程式中的所有導覽動作都會發生在 FragmentContainerView
中。
- 將 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" />
- 在 ID 屬性下方新增
name
屬性,並設定為androidx.navigation.fragment.NavHostFragment
。雖然您可以針對此屬性指定特定片段,但設定為NavHostFragment
即可讓FragmentContainerView
在片段之間導覽。
android:name="androidx.navigation.fragment.NavHostFragment"
- 在 layout_height 和 layout_width 屬性下方新增
app:defaultNavHost
屬性,並設定為等於"true"
。如此一來,片段容器就可以與導覽階層互動。舉例來說,按下系統返回按鈕後,容器就會回到先前顯示的片段,就像顯示新活動時的情況一樣。
app:defaultNavHost="true"
- 新增名為
app:navGraph
的屬性,並將該屬性設定為等於"@navigation/nav_graph"
。這會指向一個 XML 檔案,用於定義應用程式片段之間的導覽方式。Android Studio 暫時會顯示尚未解決的符號錯誤。您會在接下來的工作中解決這個問題。
app:navGraph="@navigation/nav_graph"
- 最後,由於您使用應用程式命名空間新增了兩項屬性,因此務必在
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」的新資源資料夾。
建立 XML 檔案時,系統會顯示新的視覺編輯器。由於您已參照 FragmentContainerView
的 navGraph
屬性中的 nav_graph
,如要新增目的地,請按一下畫面左上方的新增按鈕,然後為每個片段建立目的地 (一個用於 fragment_letter_list
,另一個用於 fragment_word_list
)。
新增完成後,這些片段應該就會顯示在畫面中央的導覽圖上。您也可以使用左側顯示的元件樹狀結構來選取特定目的地。
建立導覽動作
如要建立目的地 letterListFragment
到 wordListFragment
之間的導覽動作,請將滑鼠游標懸停在 letterListFragment
目的地上方,然後從右側顯示的圓圈拖曳至 wordListFragment
目的地。
現在您應該會看到已建立的箭頭,用來表示兩個目的地之間的動作。點選箭頭後,屬性窗格就會顯示名為 action_letterListFragment_to_wordListFragment
的動作,您可以在程式碼中參照該動作。
指定 WordListFragment 的引數
使用意圖在活動之間導覽時,您指定了「extra」(額外項目),使得所選字母可傳遞到 wordListFragment
。導覽也支援在目的地之間傳遞參數,並以型別安全的方式完成這項操作。
選取 wordListFragment
目的地,然後在屬性窗格的「Arguments」下方,按一下加號按鈕建立新的引數。
引數應名為 letter
,型別應為 String
。您先前新增的 Safe Args 外掛程式就能派上用場。將這個引數指定為字串,可確保在程式碼中執行導覽動作時,String
會如預期運作。
設定起始目的地
雖然您的 NavGraph 得知所有必要的目的地時,但是 FragmentContainerView
會如何得知應優先顯示哪個片段?在 NavGraph 中,您必須將字母清單設定為起始目的地。
如要設定起始目的地,請選取 letterListFragment
,然後按一下「Assign start destination」按鈕。
- 這就是您目前需要在 NavGraph 編輯器執行的所有操作。此時,您就可以繼續並建立專案。在 Android Studio 中,從選單列依序選取「Build」>「Rebuild Project」。系統會根據您的導覽圖產生一些程式碼,方便您使用剛建立的導覽動作。
執行導覽動作
開啟 LetterAdapter.kt
以執行導覽動作。只需要兩個步驟即可完成。
- 刪除按鈕
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
就可確保一切運作正常。
- 建立
navController
屬性。此標記為onCreate
,因為會在lateinit
中進行設定。
private lateinit var navController: NavController
- 然後,在
onCreate()
中呼叫setContentView()
後,取得對nav_host_fragment
的參照 (這是FragmentContainerView
的 ID),並指派給navController
屬性。
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
- 然後,在
onCreate()
中呼叫setupActionBarWithNavController()
,並傳入navController
。這可確保畫面上會顯示動作列 (應用程式列) 按鈕,例如LetterListFragment
中的選單選項。
setupActionBarWithNavController(navController)
- 最後,導入
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()
。
- 在
WordListFragment
中建立letterId
屬性。您可以將此屬性標記為 lateinit,而不必將其設為可為空值。
private lateinit var letterId: String
- 接著覆寫
onCreate()
(而不是onCreateView()
或onViewCreated()
!),並加入以下內容:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
letterId = it.getString(LETTER).toString()
}
}
由於 arguments
可能是選用性質,因此請務必呼叫 let()
並傳入 lambda。這段程式碼會假設 arguments
不是空值,並傳入 it
參數的非空值引數。不過,如果 arguments
是 null
,則 lambda 將不會執行。
雖然 Android Studio 不是實際程式碼之部分,但還提供實用的提示,讓您瞭解 it
參數。
Bundle
到底是什麼?可視為用來在類別 (例如活動和片段) 之間傳遞資料的鍵/值組合。實際上,當您在這個應用程式的第一個版本中執行意圖時,呼叫 intent?.extras?.getString()
時已經使用套裝組合。使用片段時,從引數取得字串的方式完全相同。
- 最後,當您設定回收業者的轉接程式時,您可以存取
letterId
。將onViewCreated()
中的activity?.intent?.extras?.getString(LETTER).toString()
替換成letterId
。
recyclerView.adapter = WordAdapter(letterId, requireContext())
您成功了!請花點時間執行應用程式。現在,不需要任何意圖就可以在兩個畫面之間導覽,並且皆在單一活動中。
10. 更新片段標籤
您已成功將這兩個畫面轉換成使用片段。在進行變更之前,每個片段的應用程式列都會針對應用程式列中的每個活動提供描述性標題。然而,一旦轉換成使用片段,這個標題就不會出現在詳細資料活動中。
片段有一個名為 "label"
的屬性,您可以在其中設定父項活動要在應用程式列中使用的標題。
- 在
strings.xml
中,在應用程式名稱之後,新增下列常數。
<string name="word_list_fragment_label">Words That Start With {letter}</string>
- 您可以在導覽圖中設定每個片段的標籤。返回
nav_graph.xml
中,在元件樹狀結構中選取letterListFragment
,然後前往屬性窗格將標籤設定為app_name
字串:
- 選取
wordListFragment
,並將標籤設定為word_list_fragment_label
:
恭喜您完成目前為止的工作!請再次執行應用程式,您應該會看到程式碼研究室一開始呈現的模樣,不過現在所有導覽都已透過單一活動代管,且每個畫面都設有獨立的片段。
11. 解決方案程式碼
本程式碼研究室的解決方案程式碼位於下方所示專案中。
- 前往專案指定的 GitHub 存放區頁面。
- 驗證分支版本名稱與程式碼研究室中指定的分支版本名稱相符。例如,在下列螢幕截圖中,分支版本名稱為「main」。
- 在專案的 GitHub 頁面中,按一下「Code」按鈕,畫面上會出現彈出式視窗。
- 在彈出式視窗中,按一下「Download ZIP」按鈕,將專案儲存至電腦。等待下載作業完成。
- 在電腦中找到該檔案 (可能位於「下載」資料夾中)。
- 按兩下解壓縮 ZIP 檔案。這項操作會建立含有專案檔案的新資料夾。
在 Android Studio 中開啟專案
- 啟動 Android Studio。
- 在「Welcome to Android Studio」視窗中,按一下「Open」。
注意:如果 Android Studio 已開啟,請改為依序選取「File」>「Open」選單選項。
- 在檔案瀏覽器中,前往已解壓縮的專案資料夾所在的位置 (可能位於「Downloads」資料夾中)。
- 按兩下該專案資料夾。
- 等待 Android Studio 開啟專案。
- 按一下「Run」按鈕 即可建構並執行應用程式。請確認應用程式的建構符合預期。
12. 摘要
- 片段是可嵌入活動中可重複使用的使用者介面。
- 片段的生命週期與活動生命週期不同,而檢視畫面設定發生在
onViewCreated()
中,而不是onCreateView()
。 FragmentContainerView
可用來將片段嵌入至其他活動中,以及管理片段之間的導覽。
使用導覽元件
- 設定
FragmentContainerView
的navGraph
屬性可讓您在活動內的不同片段之間進行導覽。 NavGraph
編輯器可讓您新增導覽動作,以及指定不同目的地的之間的引數。- 使用意圖進行導覽時,您必須傳入額外項目,導覽元件會使用 SafeArgs 自動為導覽動作產生類別和方法,以確保引數的類型安全性。
片段的用途
- 使用導覽元件時,許多應用程式可在單一活動中管理整個版面配置,且所有導覽動作都在片段之間進行。
- 片段可讓您使用常見的版面配置模式,例如平板電腦上的主控制項/詳細資料版面配置,或是相同活動中的多個分頁。