我為何要封裝DialogFragment?

牛犇發表於2016-03-02

最近在重構專案程式碼,專案中建立對話方塊用的是Dialog,AlertDialog。但是官方推出了DialogFragment來代替Dialog。那我就去認真的瞭解下DialogFragment。

DialogFragment

DialogFragment是在Android3.0的時候被引入的,從其名字可以很直觀的看出它是一種基於Fragment的Dialog,可以用來建立對話方塊,它是用來替代Dialog的。一個新事物的出現是為了解決舊事物存在的問題,那不建議使用的Dialog存在什麼問題呢?下面簡單的說下。

Dialog存在問題:

  • 在手機配置發生變化後(比如:旋屏後),變化之前顯示的Dialog,變化之後不會顯示,更別提Dialog狀態的恢復了。
  • 管理自定義的Dialog和系統原生的Dialog麻煩

DialogFragment怎麼解決Dialog存在的問題:

  • DialogFragment說到底還是一個Fragment,因此它繼承了Fragment的所有特性。同理FragmentManager會管理DialogFragment。在手機配置發生變化的時候,FragmentManager可以負責現場的恢復工作。呼叫DialogFragment的setArguments(bundle)方法進行資料的設定,可以保證DialogFragment的資料也能恢復。
  • DialogFragment裡的onCreateView和onCreateDIalog 2個方法,onCreateView可以用來建立自定義Dialog,onCreateDIalog 可以用Dialog來建立系統原生Dialog。可以在一個類中管理2種不同的dialog。

用DialogFragment替代Dialog

既然DialogFragment有這些好處,那我就毅然決然的對專案中的Dialog用DialogFragment來進行替代。
重構的思路是這樣的:

  • 首先先建立一個ConfirmDialogFragment類(該類是用來建立確認對話方塊的),ConfirmDialogFragment類繼承了DialogFragment。
  • 其次在建立一個ProgressDialogFragment類(該類是用來建立進度對話方塊),同時它也繼承了DialogFragment。
  • 其他型別的Dialog就不舉例了。
  • 最後在BaseActivity(專案中所有Activity的基類)新增顯示Dialog的方法,供BaseActivity的子類、Fragment、還有非Activity和非Fragment的類來呼叫。

我們先看下關鍵程式碼片段:程式碼地址
ConfirmDialogFragment中的程式碼片段:程式碼地址

ConfirmDialogFragment很關鍵的一點,ConfirmDialogFragment中的mListener屬性的值是通過

方式獲取的。

BaseActivity中程式碼片段:程式碼地址

那我們就重構BaseActivity的子類顯示Dialog的程式碼:
我拿MainActivity來舉例子:
MainActivity的關鍵重構程式碼:

現在MainActivity裡的程式碼執行起來完全沒問題,因為MainActivity裡面包含了3個Fragment,每個Fragment裡面都有顯示ConfirmDialog和ProgressDialog的程式碼,所以開始重構這3個Fragment:
重構思路:

  • 每個Fragment裡都可以獲取到相對應的Activity的例項,只要獲取到例項就可以呼叫顯示對話方塊的方法來顯示對話方塊了。
  • 對話方塊中的事件怎麼傳遞給Fragment問題?Activity可以獲取到Fragment的例項,對話方塊可以把事件傳遞給Activity,因此Activity順理成章的可以把事件傳遞給對應的Fragment。
  • 一個Activity有多個Fragment呼叫顯示對話方塊的方法,在Activity的實現了對話方塊介面的方法裡怎樣區分不同的Fragment呼叫者?可以在BaseActivity顯示對話方塊的方法里加個id引數,用id來區分不同的Fragment呼叫者。
    那就上關鍵程式碼片段:
    MainActivity中的AFragment程式碼片段: 

同理MainActivity的BFragment,CFragment的重構與AFragment類似。
MainActivity的關鍵程式碼片段:

產生的問題

看了MainActivity的onClick方法裡面程式碼我都對自己無語了,onClick方法裡面充斥著各種的if else 語句,並且當前的MainActivity裡,若再有別的顯示ConfirmDialog的呼叫者,onClick方法裡少不了要增加對應的else if語句。MainActivity只是專案中所有Activity的一個縮影。其他的Activity也會遇到同樣的問題(這不是我意淫的,提早預估到問題,提早入手進行解決總是好的)

我們拿MainActivity來代表所有的Activity總結下使用DialogFragment建立Dialog產生的問題:

  • MainActivity裡的onClick方法維護、擴充套件性不好,充斥著各種if else if語句,可讀性也不好。
  • MainActivity裡的onClick方法除了把Dialog的事件轉發給相對應的呼叫者之外,沒有多任何其他操作,所以是多餘的
  • 顯示Dialog的方法不靈活

存在這些問題嚴重影響了我後面的重構工作,於是乎我就去國內國外網站上搜尋對應的解決方法,但是也沒有找到好的方法,最後我就想辦法自己解決上面的問題,這也是我為何要封裝DialogFragment的緣由

封裝DialogFragment,讓DialogFragment使用非常簡單、靈活

我們仔細的分析下上文的問題的主要原因是顯示Dialog的方法沒有把Dialog裡面的開放的介面作為引數導致的,假如能像下面的使用方式:

上文中所有的問題都可以解決。

為什麼不按下面的做法做

做法1:那我們直接把ConfirmDialogListener 的例項賦值給ConfirmDialogFragment 的例項的mListener屬性,以下為程式碼:

那我就詳細的解釋下為什麼不這樣做的具體原因:

  • 在建立Fragment的時候,最好是把傳遞給Fragment的資料存放在Bundle中,然後在呼叫fragment的setArguments(bundle)方法進行資料的設定,這種做法好處是:系統會儲存Fragment的資料,在手機配置發生變化後(比如旋屏),系統會把儲存的Fragment資料進行恢復。

以上做法,ConfirmDialogFragment 中mListener屬性系統沒有為之儲存,所以手機配置發生變化後,ConfirmDialogFragment 中的mListener 是null。

做法2:那我們是否可以把ConfirmDialogListener例項(ConfirmDialogListener是ConfirmDialogFragment 對外開放的介面)存放在Bundle中?
答案是不可以,首先 Bundle對存放的資料是有限制的,把ConfirmDialogListener的例項存入Bundle中是比較複雜的操作。其次即使通過艱辛萬苦把ConfirmDialogListener例項存入了Bundle中,儲存ConfirmDialogListener例項是毫無意義的,只有儲存資料對於Fragment來說才有意義,儲存行為對Fragment是無意義的。

有思路

我們在回顧下ConfirmDialogFragment中onAttach的方法的關鍵程式碼:程式碼地址

以上程式碼的關鍵之處在於mListener= (ConfirmDialogListener)getActivity()。同時痛點也在此處,這是一種類似於硬編碼的方式,硬編碼的一個不好的地方就是沒有擴充套件性。解決思路:

  • 那我們就想辦法讓此處變的有彈性。我們可以把BaseActivity想象為一個ConfirmDialogListener的存取工具,呼叫者可以把自己實現的ConfirmDialogListener存入BaseActivity中, ConfirmDialogFragment可以從BaseActivity中取出ConfirmDialogListener例項,那我的問題就迎刃而解了。
  • 既然可以獲取到BaseActivity的例項,那也可以獲取到BaseFragment的例項(getParentFragment()可以獲取到)。既然BaseFragment例項可以獲取到,那解決ConfirmDialogFragment同時服務於BaseActivity和BaseFragment就不是問題了

同時我還想解決在任何的類中(不管Fragment、Activity、或其他類中)顯示Dialog不需要依賴BaseActivity。而是有一個類(假如叫DialogFactory)定義顯示各種Dialog的方法。像下面一樣:

那我就說下思路:

  • 新建DialogFactory類,該類封裝了顯示各種Dialog的方法
  • 新建BaseDialogFragment類,該類是各種型別Dialog的基類,裡面封裝了一些公用的方法
  • 修改BaseActivity和BaseFragment類,在各自的類中分別定義DialogFactory屬性mDialogFactory,這樣顯示Dialog的任務就交給了mDialogFactory

DialogFactory 程式碼:程式碼地址

DialogFactory關鍵程式碼介紹:

  • DialogFactory可以供任何的類來使用
  • mFragmentManager屬性在現實Dialog時起作用,若呼叫者(顯示Dialog)是Activity,則傳遞getFragmentManager();若呼叫者是Fragment,則傳遞getChildFragmentManager()。不過不需要擔心這些,BaseActivity和BaseFragment都已經封裝了這些引數
  • DialogFactory把呼叫者傳遞過來的BaseDialogListener傳遞給Activity或Fragment

BaseDialogFragment程式碼:程式碼地址

BaseDialogFragment關鍵程式碼介紹:

  • BaseDialogListener定義一個空方法介面,新增的Dialog(若該Dialog包含對外介面),則新增的Dialog提供的對外介面必須繼承BaseDialogListener
  • onReceiveDialogListener方法是提供給子類來實現,讓子類來接收呼叫者傳遞進來的BaseDialogListener例項
  • onActivityCreated方法很重要,該方法是使BaseDialogFragment可以相容Activity和Fragment的關鍵程式碼

    上面程式碼的作用是假如當前呼叫者(顯示Dialog)是一個Fragment,則會把Fragment中持有的BaseDialogListener賦給對應的Dialog,若當前呼叫者是一個Activity,則會做同樣的事情

ConfirmDialogFragment 關鍵程式碼片段:程式碼地址

ProgressDialogFragment基本沒發生多大改變,我們就不貼具體程式碼了。

修改BaseActivity和BaseFragment類關鍵程式碼:

介紹下BaseActivity修改的程式碼:

  • mDialogFactory 是供Activity來顯示各種Dialog的
  • mListener是Activity持有呼叫者傳遞的BaseDialogListener的例項,BaseDialogFragment會從getDialogListener()方法獲取該例項

BaseFragment的修改和BaseActivity一樣,就不介紹了。

那我們在理一下上面程式碼的思路:

  • DialogFactory封裝了顯示各種Dialog的方法,使用者使用它來顯示Dialog。它會把使用者傳遞的BaseDialogListener傳遞給BaseActivity或BaseFragment
  • BaseActivity和BaseFragment在傳遞BaseDialogListener起了一個橋樑的作用。當Dialog即將被顯示時,BaseDialogFragment會從BaseActivity或BaseFragment獲取BaseDialogListener

解決最棘手的問題

當我還沉浸在happy中時,突然一個問題出現了,旋屏後重新彈出的ConfirmDialog的點選事件卻沒傳遞給呼叫者,我就細細的想原來是旋屏後BaseActivity或BaseFragment裡的mListener為空了。那我就繼續解決這棘手問題,為什麼棘手呢?因為我們一直都是在圍繞著怎樣解決旋屏後Dialog中的mListener屬性的值(BaseDialogListener的例項)怎麼重新獲取的問題,但是經過一番努力還是沒成功,不行我還得繼續想辦法。

上文中也提到過對於Fragment存放行為是毫無意義的,那我們就換個角度考慮問題,我們先用BaseFragment來舉例子(BaseActivity類似):

  • 在BaseFragment用一個屬性mDialogListenerKey去存mListener(型別是BaseDialogListener)屬性的類名,當手機配置發生變化的時候在BaseFragment的onSaveInstance(bundle)方法中把mDialogListenerKey存入Bundle中(前提條件Dialog沒消失)
  • 當BaseFragment重新被建立的時候,在onCreate(savedInstanceState)方法中從Bundle中把mDialogListenerKey值讀出來
  • 根據mDialogListenerKey去找到對應的BaseDialogListener子類的例項
  • 把上步中找到的例項通過呼叫BaseFragment的setDialogListener()方法進行設定

那我們就開始寫程式碼:
新建DialogListenerHolder,該類用來持有呼叫者傳遞的BaseDialogListener例項,即把原來BaseActivity或BaseFragment裡的mListener屬性放到該類中
DialogListenerHolder修改的程式碼片段:程式碼地址

程式碼有點複雜我先簡單介紹下:
DialogListenerHolder中的mDialogListenerKey是存BaseDialogListener的子類的類名。

DialogListenerHolder中的saveDialogListenerKey(Bundle outState)方法是把mDialogListenerKey存到Bundle中,這樣系統就可以儲存下該值。供BaseActivity或BaseFragment的onSaveInstanceState(Bundle outState)方法呼叫。

DialogListenerHolder中的getDialogListenerKey(Bundle savedInstanceState)方法是從Bundle中取出mDialogListenerKey,供BaseActivity或BaseFragment的onCreate(Bundle savedInstanceState)呼叫。

DialogListenerHolder中的restoreDialogListener(Object o)方法很重要,作用是從引數o中去查詢mDialogListenerKey對應的BaseDialogListener(查詢範圍是引數o和o中的屬性),若找到並呼叫setDialogListener()方法。
所以這裡對於呼叫者(調起Dialog)傳遞的BaseDialogListener有個要求:呼叫者實現了BaseDialogListener的子類或者呼叫者包含BaseDialogListener的子類的一個public屬性

DialogFactory修改程式碼片段:程式碼地址

BaseActivity修改程式碼片段:程式碼地址

BaseFragment修改程式碼片段:程式碼地址

到此為止我們的封裝就一切ok了,高興下。

總結

經過一步步艱辛的路程,封裝DialogFragment的工作終於結束了,封裝好的Dialog架構可以給您帶來以下好處:

  • 可以讓DialogFragment的使用像Dialog一樣的簡單、靈活,同時也保持了DialogFragment的優點,可以在任何的類中使用。就像下面程式碼:
  • 很簡單的新增新型別的Dialog

同時在使用的時候需要注意以下幾點:

1 . 在既不是Activity也不是Fragment的類(下面我們簡稱該類)中調起Dialog要求:

  • 該類擁有DialogFactory 屬性(DialogFactory 的值是從繼承了BaseActivity的Activity或繼承了BaseFragment的Fragment傳遞進來的)
  • 在給DialogFactory 屬性賦值後,緊接著需要呼叫DialogFactory 的restoreDialogListener(Object)方法
  • 該類實現了XXDialogListener或者該類包含XXDialogListener這樣的一個屬性(該屬性許可權必須是public)

2 .在繼承了BaseActivity的Activity(簡稱activity)中或者繼承了BaseFragment的Fragment(簡稱fragment)中調起Dialog的要求:

  • activity或fragment實現了XXDialogListener或者是activity或fragment包含XXDialogListener這樣的一個public型別的屬性。

3 .若需要建立新的型別的Dialog,需要注意的是:

  • 繼承BaseDialogFragment
  • 若該Dialog對外提供介面(介面需要繼承BaseDialogListener,需要實現onReceiveDialogListener()方法)

以上是我個人的總結,希望對給Android學習者提供幫助。程式碼地址

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

我為何要封裝DialogFragment? 我為何要封裝DialogFragment?

相關文章