iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0
Mobile Development

【Kotlin Notes And JetPack】Build an App系列 第 13

Day 13.【UI】Fragment 的介紹與應用

  • 分享至 

  • xImage
  •  

經過前幾篇跟資料處理相關的介紹,接下來幾天就要進入介面相關的 library,如何實作出設計好的畫面,以及要如何接上處理好的資料,以下如有解釋不清或是描述錯誤的地方還請大家多多指教:

什麼?

隨著不同尺寸螢幕的出現,進而推出的介面結構,Fragment 可重複使用並具有自己的生命週期,但不能獨立存在,必須依附在 Activity 或是另一個 Fragment 之下,因此 Fragment 很適合來定義或是管理畫面,根據不同尺寸的螢幕並搭配不同元件顯示對應的 style。

| Fragment

除了基本型的 Fragment,library 還提供了另外兩種情境可使用的 Fragment:

  • DialogFragment 以呈現 Dialog 的形式呈現你的 Fragment
  • PreferenceFragmentCompat 搭配 preference 呈現資料,適合來製作設定頁面
    其中幾個頁面會使用 BottomSheetDialogFragment 來製作,他的底層就是繼承 DialogFragment 喔 !

而在 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 來處理,以下針對狀態管理做一些重點整理:

  1. FragmentManager 可以使用 setMaxLifecycle() 設定 Fragment 的最高生命週期
    -> 這個功能是官方用來取代 setUserVisibleHint()
    -> setUserVisibleHint() 主要是在 ViewPager 來監聽可看見的子層 Fragment,主要的問題點在於不可見的子層 Fragment 在可見的子層 Fragment 執行 setUserVisibleHint() 時,生命週期會執行到 onResume,大家看到問題了嗎?在不可見的情況下執行了 onResume,當你在 onResume 時有設定執行某個方法時,卻在這個時候執行了,這種情境下可能還會需要再做額外的判斷來撇除在不可見的情況下不要執行,至於 ViewPager2 已經有做相關的處理了
    -> setMaxLifecycle() 可設置 Fragment 當前最大的狀態,因此可以透過一些參數來設置在不可見的情況下生命週期限制為 STARTED 的狀態,可見時才會到 RESUMED
  2. 子類的生命週期都不可早於父類的狀態,同樣的子類的活動必須在父類結束前停止
  3. 在 XML 中請改用 <FragmentContainerView> 來取代 <fragment> ,主要是因為這個 tag 不再受限於 FragmentManager 的狀態
  4. Fragment 會在 CREATED 之後新增至 FragmentManager 並執行 onAttach(), 並從FragmentManager 移除之後執行 onDetach()
  5. savedInstanceState 只有在第一次建立時 bundle 會是 null,就算未覆寫onSaveInstanceState() bundle 也不會是空值的狀態喔!
  6. onStop 觸發後的流程在 API 28 前後的版本有差異,如以下的圖示:
    https://ithelp.ithome.com.tw/upload/images/20220926/20151145jUo0uqEHQq.png

| FragmentManager

我們透過 FragmentManager 來對 Fragment 進行 add, remove, replace 和 back stack 的操作,而平常使用 Navigation library 來管理頁面轉換的人,如果點進 library 的 source code 應該也可以隱約看出底層有透過 FragmentManager 執行某些行為,以下我們就來了解他是怎麼運作的。

1. Activity and Fragment

常見的幾個 UI 呈現方式,包括有 menu 的抽屜展開形式或是有 tab 的頁面,這些設計我們很常在網購的頁面上看到對吧!上面第一段也有提到說 Fragment 必須依附在 Activity 下,那這樣的結構之下我們要怎麼取得頁面各自的 Manager 呢?我們先來看看頁面的結構分層:
https://ithelp.ithome.com.tw/upload/images/20220926/20151145JwN6nOXi8O.png

當介面比較複雜的時候會有這種多層 Fragment 的結構出現,Activity 可透過 getSupportFragmentManager() 來取得管理底下 Fragment 的 manager,而最外層的 Fragment 則可以透過 getChildFragmentManager() 取得管理子 Fragment 的 manager,如果子層的不管是 Child Fragment 還是 Host Fragment 想取得父層的 manager,可透過 getParentFragmentManager() 來存取。

2. Transaction

這一小節只會提到如何執行頁面的轉換,如果對更細節的處理有興趣可以閱讀一下官方提供的文件;頁面轉換我們可以透過 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:

  • do call addToBackStackSTOPPED
  • don't call addToBackStackDESTROYED

一個可退回去另一個則不行

如何?

| Set up

1. settings.gradle

在開始實作之前我們要先設定環境,加入 AndroidX Fragment library 有兩個步驟:在 settings.gradle 中加入 Google Maven repository。

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        ...
    }
}

2. build.gradle

在 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,在前言與準備中提到想做的幾個功能:

  • 顯示的主頁
  • 新增頁面
  • 刪除
  • 內頁
  • 小工具
  • 紫外線過高提醒功能(額外)

依照預想的介面和功能我將頁面分成
https://ithelp.ithome.com.tw/upload/images/20220926/20151145eVnSVrn3iA.png

| Start up

在建立好的 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>

Reference

Android Developer
Fragment 懶加載的新實現


上一篇
Day 12. Android Jetpack 是什麼 ?
下一篇
Day 14.【UI】ConstrainLayout 的介紹與應用
系列文
【Kotlin Notes And JetPack】Build an App30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言