經過前幾篇跟資料處理相關的介紹,接下來幾天就要進入介面相關的 library,如何實作出設計好的畫面,以及要如何接上處理好的資料,以下如有解釋不清或是描述錯誤的地方還請大家多多指教:
隨著不同尺寸螢幕的出現,進而推出的介面結構,Fragment 可重複使用並具有自己的生命週期,但不能獨立存在,必須依附在 Activity 或是另一個 Fragment 之下,因此 Fragment 很適合來定義或是管理畫面,根據不同尺寸的螢幕並搭配不同元件顯示對應的 style。
除了基本型的 Fragment,library 還提供了另外兩種情境可使用的 Fragment:
DialogFragment
以呈現 Dialog 的形式呈現你的 FragmentPreferenceFragmentCompat
搭配 preference 呈現資料,適合來製作設定頁面而在 Activity 的 XML 中使用 FragmentContainerView 來當 fragment 的容器:
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.ExampleFragment" />
Fragment 跟 Activity 一樣擁有自己的生命週期,在每個頁面生成移除或是消失在螢幕所見範圍都有各自的狀態,透過各自的 LifecycleOwner 來管理以下狀態:
那我們可以透過 Fragment 類別提供的 callbacks 在特定的階段處理一些事,像是我們想在 Fragment 被銷毀的同時釋放掉一些 listner,或是在頁面生成的時候觸發 LiveData 的監聽等等的,下面的圖將上面提到的狀態分別對應 callback 執行的時機點。
至於 Fragment 和 Fragment 之間的狀態管理會交由 FragmentManager 來處理,以下針對狀態管理做一些重點整理:
setMaxLifecycle()
設定 Fragment 的最高生命週期setUserVisibleHint()
setUserVisibleHint()
主要是在 ViewPager 來監聽可看見的子層 Fragment,主要的問題點在於不可見的子層 Fragment 在可見的子層 Fragment 執行 setUserVisibleHint()
時,生命週期會執行到 onResume,大家看到問題了嗎?在不可見的情況下執行了 onResume,當你在 onResume 時有設定執行某個方法時,卻在這個時候執行了,這種情境下可能還會需要再做額外的判斷來撇除在不可見的情況下不要執行,至於 ViewPager2 已經有做相關的處理了setMaxLifecycle()
可設置 Fragment 當前最大的狀態,因此可以透過一些參數來設置在不可見的情況下生命週期限制為 STARTED 的狀態,可見時才會到 RESUMED<FragmentContainerView>
來取代 <fragment>
,主要是因為這個 tag 不再受限於 FragmentManager 的狀態onAttach()
, 並從FragmentManager 移除之後執行 onDetach()
onSaveInstanceState()
bundle 也不會是空值的狀態喔!我們透過 FragmentManager 來對 Fragment 進行 add, remove, replace 和 back stack 的操作,而平常使用 Navigation library 來管理頁面轉換的人,如果點進 library 的 source code 應該也可以隱約看出底層有透過 FragmentManager 執行某些行為,以下我們就來了解他是怎麼運作的。
常見的幾個 UI 呈現方式,包括有 menu 的抽屜展開形式或是有 tab 的頁面,這些設計我們很常在網購的頁面上看到對吧!上面第一段也有提到說 Fragment 必須依附在 Activity 下,那這樣的結構之下我們要怎麼取得頁面各自的 Manager 呢?我們先來看看頁面的結構分層:
當介面比較複雜的時候會有這種多層 Fragment 的結構出現,Activity 可透過 getSupportFragmentManager()
來取得管理底下 Fragment 的 manager,而最外層的 Fragment 則可以透過 getChildFragmentManager()
取得管理子 Fragment 的 manager,如果子層的不管是 Child Fragment 還是 Host Fragment 想取得父層的 manager,可透過 getParentFragmentManager()
來存取。
這一小節只會提到如何執行頁面的轉換,如果對更細節的處理有興趣可以閱讀一下官方提供的文件;頁面轉換我們可以透過 add() 或是 replace() ,以簡單的例子會是這樣:
supportFragmentManager.commit {
replace<YourFragment>(R.id.fragment_container)
setReorderingAllowed(true)
addToBackStack("name") // name 可以是 null
}
// 如果寫成 function 可以由外部帶入想替換的 fragment
// 也可以加入一些換頁的動畫
fun setFragment(fragment: Fragment) {
supportFragmentManager.commit {
setCustomAnimations(R.anim.fragment_slide_left_enter,
R.anim.fragment_slide_left_exit,
R.anim.fragment_slide_right_enter,
R.anim.fragment_slide_right_exit)
replace(R.id.yourContainer, fragment)
setReorderingAllowed(true)
addToBackStack(null)
}
}
addToBackStack
要不要將 Fragment 加入 back stack 取決於你頁面的行爲有沒有要讓他可以退回上一個 Fragment 的狀態,生命週期狀態也會有所不一樣,當你移除了一個 fragment:
addToBackStack
→ STOPPED
addToBackStack
→ DESTROYED
一個可退回去另一個則不行
在開始實作之前我們要先設定環境,加入 AndroidX Fragment library 有兩個步驟:在 settings.gradle 中加入 Google Maven repository。
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
...
}
}
在 build.gradle 加入 dependencies:
dependencies {
def fragment_version = "1.5.0"
// Java language implementation
implementation "androidx.fragment:fragment:$fragment_version"
// Kotlin
implementation "androidx.fragment:fragment-ktx:$fragment_version"
}
接下來根據設計好的介面定義我的 Activity 和 Fragment,在前言與準備中提到想做的幾個功能:
依照預想的介面和功能我將頁面分成
在建立好的 activity_main.xml 放置 Container:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/myNavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>