Dcard Android FragmentResult 架構介紹 (1) — 使用 Kotlin Property Delegation 讓 DialogFragment 取得 Callback

黃德銘
Dcard Tech Blog
Published in
7 min readMay 30, 2023

Configuration change 對於 Android Developer 來說一直是個重要的議題,手機畫面旋轉、後來推出的 Dark Mode 以及在 Android 13 推出個別應用程式語言偏好,這些變動都增加 configuration change 觸發的可能性,當 configuration change 觸發時,我們會需要將重要資訊儲存並在重建後還原。Activity 的還原難度可能算還好,在 Fragment 推出後越來越多東西會做成 Fragment 來使用,例如後來推出了 DialogFragment 取代早期使用的 Dialog,早期的 Dialog 在畫面旋轉時要想辦法保存當前狀態並在重建後開啟,而且還要手動關閉 Dialog 避免一些 leak 的情況,換成 DialogFragment 就輕鬆多了,只要照著 Fragment 的標準流程來處理就沒什麼大問題,但有個要解決的問題是我們使用 DialogFragment 時通常是想讓使用者做些操作,並把操作結果傳回給使用它的人,那今天就想跟大家聊聊我們是怎麼取得 callback。

假如我們現在有個需求是要做出下面這個 DialogFragment

目前也做了一個 CustomDialogFragment

不知道大家會怎麼設定確定取消的 click callback?

可能會有人這樣實作

透過設定 Positive 和 Negative 的點擊事件,希望使用者點擊後可以讓開啟 DialogFragment 的地方獲得操作結果,這樣做理論上是沒什麼問題,但有個致命的缺點,使用者一旦旋轉畫面以後,開啟它的地方就會收不到 DialogFragment 的點擊結果,為什麼會這樣呢?

因為 configuration change 會重新產生 Activity 和 Fragment 的實體

若是我們沒有把 callback 資訊保存起來,在 configuration change 後就會遺失,那麼就來思考這個問題,我們能把 callback 保存起來嗎?

我認為是沒辦法,因為有以下 2 個問題

保存資料必須存進 Bundle 中

一般來說我們要將資料保存起來會透過 Fragment 的 onSaveInstanceState(outState: Bundle) 這個 function,但 Bundle 並不是什麼東西都能夠存,物件只要沒有實作 SerializableParcelable 的話那就無解。

就算能存,也沒辦法指到正確的實體

在我們這個例子中 callback 實體是 Activity 的內部類別,在畫面旋轉後會建立新的 Activity,所以就算能夠存到 Bundle 中,callback 也沒辦法正確的指到新的 Activity

基於以上的 2 點,我們的 callback 沒辦法從外面設定,那麼我們要怎麼處理呢?

Dcard 以前是這樣解決

我們會在 DialogFragment 中建立專屬的 interface Interaction,並且讓開啟它的地方實作,再利用 Fragment 的生命週期取得最新實體,這樣一來不管是畫面旋轉幾次,只要 Fragment 有被重建,都可以指到正確的實體。

那麼這樣做就有個缺點,我必須在每個 DialogFragment 都 override onAttach,這樣一來每開一個新的 DialogFragment 都要從其他檔案複製 onAttach 出來(你應該不會想要每次都手寫這段 code…),當然是可以用繼承加泛型的方式來做,但每多一次繼承就會在未來有變動時多一個不確定因素,所以我的腦子就動到了 Kotlin 的 Property Delegation,那麼來介紹一下這個是什麼東西。

Kotlin Property Delegation

Kotlin Property Delegation 是 Kotlin 提供可以將 property 的邏輯委託出去的功能,除了常見的 notNull, vetoableobservable 之外還可以實作自己的自定義委託邏輯,那麼來想像一下實作完成的使用方式

自定義的委託依照變數定義的 valvar 會使用對應的 ReadOnlyPropertyReadWriteProperty,在這邊因為使用 val 所以就使用ReadOnlyProperty,實作結果如下

ReadOnlyProperty 後面有 2 個泛型

  • 第一個是在哪個類別底下能使用它
  • 第二個是回傳型別是什麼

因為 DialogFragment 為 Fragment 的子類別,而且有些 Fragment 為工具型,也是會有 callback,所以就定在 Fragment 底下,這樣子 Delegation 就寫完了,之後就可以應用在各個 DialogFragment 上,而且使用上也滿簡單的。

這篇文章介紹了我們如何兼顧 configuration change 後還能正常取得 callback 的方式,我們利用 Kotlin Property Delegation 來包裝原本需要 override onAttach 的作法,讓使用上更加方便,但我覺得目前還有幾個優化方向

複雜的頁面會實作許多 Interaction

有些複雜的頁面會需要開啟較多的 DialogFragment,所以會需要實作較多的 Interaction,該 Activity/Fragment 就會多了很多 override function,這樣在看 code 時就會更加干擾,例如沒辦法第一眼看出某個 override function 是屬於哪個 Interaction 的

通用的 DialogFragment 的 Interaction 會衝突

比如說我們會需要跳個 alert 所以實作一個 AlertDialogFragment,一個頁面可能會跳多種 alert 但 Interaction 卻只有一個,所以就需要花更多工去分辨觸發 Interaction 的是哪個情境,也許可以利用 Fragment 的 Tag,但這樣又要手動去定義 Fragment Tag,就會變得不易使用

自定義 Fragment Tag 麻煩且有隱憂

由於 Fragment Tag 是字串,取名太複雜會需要花時間思考取名,太簡單又有衝突的可能性,而且在一些重複的功能可能會抽出一個 Util 或 Helper,如果 Fragment Tag 定義在 Util 中,那就有可能跟其他頁面發生有衝突

基於以上 3 個理由讓我在某天開始思考有沒有辦法做出一個符合以下條件的架構

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

所以下一篇會介紹這篇文章標題所提到的 FragmentResult 架構,並且調整今天介紹的 getInteractionFromParent() 來串接新架構。

參考資料:

--

--

黃德銘
Dcard Tech Blog

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