Dcard Android FragmentResult 架構介紹 (2) — 使用 Kotlin Class Delegation 自動產生 FragmentTag 並註冊 Callback

黃德銘
Dcard Tech Blog
Published in
7 min readJul 13, 2023

上一篇介紹了我們如何使用 Kotlin Property Delegation 減少許多重複的 code 並讓取得 callback 簡化許多,最後也提出 3 個可以優化的方向,這次就要介紹我們如何利用 Kotlin Class Delegation 來達成這個目標。

首先先來介紹一下 Kotlin Class Delegation,在一個物件職責變複雜或是邏輯可以複用時我們會用許多方式來重構它,常見的做法就是口頭上的拆出去,那麼就是開一個新的 Class 將特定邏輯包裝起來,也就是委託模式,那麼就用 Java 舉一個例子

這個 Developer 有許多不同領域的工作,未來這個 Developer 工作內容變得更加複雜時工作領域就會變得更不明確,所以做一個小重構

重構完後可以看到 Developer 的工作內容依領域劃分了一下,接著過了幾年公司規模變大之後想找專職人員,就會變成這樣

那麼說了這麼多 Java 的寫法,如果上面的實作結果改用 Kotlin 撰寫會是怎麼樣呢?

對,就這樣而已,如果沒有要 override 預設實作內容的話就是這麼簡單

介紹完以後回到想要解決的問題,在上一篇文章有提到有 3 個優化方向

  • 複雜的頁面會實作許多 Interaction
  • 通用的 DialogFragment 的 Interaction 會衝突
  • 自定義 Fragment Tag 麻煩且有隱憂

這些問題是我們 Android 專案中若有似無的問題,平常其實習慣複製貼上了所以這好像也不是什麼問題,但有一天我忽然倦怠複製貼上所以開始思考改善方法,於是訂下改善目標

  • 讓 Activity/Fragment 不用實作 Interaction,而是委託別的物件實作
  • 被委託的物件開啟 DialogFragment,並實作對應的 Interaction
  • Fragment Tag 自動產生且不衝突

在思考許多方式以及散步幾次後,腦中忽然閃過了 Kotlin Class Delegation 這個選項,評估後也覺得這是對現有的 codebase 衝擊最小的一項,當 Activity/Fragment 加上了 XXX by XXX() 後就可以使用,這功能就像 Plugin 一樣隨加即用且對於 Activity/Fragment 本身沒什麼負擔,就決定往這方向設計,那就先定義 interface 有哪些功能

其中 findRegisterInteraction 是給上篇文章的 getInteractionFromParent() 使用的,那麼來進行實作

這邊有 2 個重點

使用 Class name 及流水號來產生 Fragment tag

由於要讓 Fragment tag 為絕對獨一無二,那就使用實作 FragmentResult 的 class name,在實際的應用上 Fragment 只會取 parentFragmentActivity 來使用,而且以階層概念來說當有 parentFragment 時就不會用 Activity,流水號則用有原子性的 AtomicInteger 來確保不會重複

限制註冊的時機

在使用流水號產生 Fragment tag 時就需要限制註冊時機,不限制的話會怎麼樣讓我們看一下:

  1. override onStart 註冊
  2. 使用者只要按 home 鍵
  3. 回到 App
  4. 重複 n 次動作 2 與 3

這時候 Fragment 的 tag 可能是 FragmentResult_MainActivity_10,當畫面旋轉一下後走了重建流程,此時已開啟的 Fragment 會被自動產生,而且新的 Activity 的流水號只有 1,所以透過 findRegisterInteraction(FragmentResult_MainActivity_10) 會找不到任何 interaction,那麼呈現在畫面上的 DialogFragment 則會無法正確操作。

知道原因後就整理一下 Activity 和 Fragment 可註冊的時機

  • Activity:initial, onCreate()
  • Fragment:initial, onCreate()

這時候眼睛亮的人就會發現上面可註冊的時機 Activity 和 Fragment 一樣,但 code check 裡的 lifecycle state 不一樣,為什麼 Activity 是 Lifecycle.State.STARTED 而 Fragment 是 Lifecycle.State.CREATED?我是不是寫錯了?

這時候只好看個 source code 了,目前撰寫文章時使用的版本如下

  • androidx.activity:activity-ktx:1.7.2
  • androidx.fragment:fragment-ktx:1.5.7

由於我們是使用 lifecycle.currentState 所以就找一下 state 是什麼時候被更新,那麼用關鍵字 handleLifecycleEvent(Lifecycle.Event.ON_START) 來找

FragmentActivity

如果我們要在 onStart 中註冊就會 override onStart(),但 override 時必定會 call super.onStart(),所以在執行自己的 onStart() 時會先執行上述程式將 lifecycle.currentState 更新為 Lifecycle.State.STARTED

Fragment

從這邊可以看到 lifecycle.currentState 更新時是在 onStart() 以後,所以我們在 onStart() 註冊時的狀態是前一個狀態 Lifecycle.State.CREATED

接下來就來繼續實作 registerDialogFragment()

內容其實很簡單,就是註冊後拿到一個 DialogFragmentLauncher,其實作如下

DialogFragmentLauncher 持有 Fragment tag 並讓它可以顯示 DialogFragment,也加上一個 extension function 提升使用的便利程度

那麼就來改寫原本的 MainActivitygetInteractionFromParent()

getInteractionFromParent() 多加了一段 findRegisterInteraction() 從新加的 FragmentResult 取得 interaction,如果取不到再從舊的方式取得。而 MainActivity 變得更簡潔,想看一個 DialogFragment 會做什麼事就直接去看啟動它的 launcher 就好,未來如果要移除掉 DialogFragment 就只需要移除跟 launcher 相關的 code 而不用到處找來找去,整體來說變得更清晰。

還有一個 registerFragment() 沒在文章中介紹實作,但其實跟 registerDialogFragment() 大同小異,有興趣的人可以去看 Demo Repo 就好,這篇主要介紹我設計的 FragmentResult 架構的思路,從看到什麼問題、訂了改善目標以及最後的成果,而中間因為 Lifecycle 狀態的問題而去看 Android source code(撰寫文章時又看了一次 😱),希望這段過程能激發大家一些靈感,也許自己專案上的某些問題可以因此被解決或優化。

參考資料:

--

--

黃德銘
Dcard Tech Blog

愛東玩西玩的 Android 工程師,玩過 Ktor, Jetpack Compose for Desktop 和 iOS