Fragment 是 Android 中歷史十分悠久的一個組件,它在 API 11 被加入,時至今日已成爲 Android 開發中最經常使用的組件之一。Fragment 有了哪些新特性、修復了哪些問題,都是開發者們十分關心的話題。下面咱們就來從新說一說 Fragment —— 不只僅是說如今的 Fragment,還會回顧它的發展,並讓您一瞥它將來的樣子。java
不知道您是否還記得 "上古時期",在那些尚未 Fragment 的日子,幾乎全部邏輯都被放在了 Activity 中,使得 Activity 臃腫而又混亂。此時,Fragment 做爲一個微型 Activity 而誕生,邁出了縮減 Acitvity 之路上的一小步。android
不過 "欲戴王冠,必承其重",Fragment 由此繼承了諸多原本是爲 Activity 設計的 API 和組件。其中有些組件,其實應該被設計爲獨立的 View,好比當年的 Action Bar,這個組件如今已經被 Toolbar 代替了;又好比現今已經基本沒人使用的 Context Menus。API 這部分就更復雜一些,全部之前要發送到 Activity 的信息,如今也要發送到 Fragment,咱們處理權限時很經常使用的 onActivityResult 就是這種狀況下的產物;當 Android 加入運行時權限時,Fragment 理所固然的也要支持,由於 Activity 已經支持了。相似的 API 還有 onMultiWindowModeChanged 以及 onPictureInPictureModeChanged。架構
遵循着成爲一個微型 Acitivity 的設計初衷,Fragment 天然而然的就獲得了這些功能。可是回過頭來看,這些功能其實並非專門爲 Fragment 設計的 —— 隨便一個什麼東西,有了這些回調,彷佛都能勝任 Fragment 的功能。這種情況使得咱們開始轉變思路,並嘗試拋棄讓 Fragment 去作微型 Activity 的想法,現在的 Fragment 正是由此而來。app
咱們的想法產生改變,仍是 2011 年的事,到今天已經經歷了很長的時間。這期間咱們花費了不少的精力去從新構思 Fragment 的定位。咱們但願 Fragment 成爲一個真正的核心組件,它應該擁有可預測的、合理的行爲,不該該出現隨機錯誤,也不該該破壞現有的功能。框架
其實咱們但願挑個時間發佈 Fragment 的 2.0 版,它將只包含那些新的、好用的 API。但在時機成熟以前,咱們會在現有的 Fragment 中逐步加入新的並棄用舊的 API,併爲舊功能提供更好的替代方案。當沒人再使用已棄用的 API 時,遷移到 Fragment 2.0 就會變得很容易。接下來我就來說講,咱們爲此所作的一些工做。ide
首先我要說,合理的 API 應當是可測試的。不能被測試的代碼不是好代碼,如今已經 2020 年了,咱們也但願 Fragment 能在這方面作得更好。因而,經過與 AndroidX 團隊緊密合做,咱們開發出了測試工具 FragmentScenario,它能夠用來單獨對 Fragment 進行測試。FragmentScenario 基於 ActivityScenario 實現,這也意味着它一樣適用於 Instrumentation 和 Robolectric 測試。同時它的 API 十分簡潔,它最主要的方法就是 onFragment,這個方法接收一個 Lambda 表達式,而 Lambda 表達式則在其中返回已存在的 Fragment 實例。同時 FragmentScenario 也提供了方便測試生命週期和重建 Fragment 的 Hook 方法。工具
以下示例代碼來講,首先使用 launchFragmentInContainer<MyFragment>() 建立 FragmentScenario 對象,這一步操做將會幫您完成建立 Fragment 的整個流程。接下來就能夠進行測試了,您能夠看到,使用 onView 測試 click() 方法時,Fragment 的層級結構已經被加載完成。最後只要在 onFragment 中檢查 Fragment 的狀態,就能夠確認 Fragment 是否有正確處理點擊事件。佈局
@Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() onView(withId(R.id.refresh)).perform(click()) scenario.onFragment { fragment -> // 檢查 Fragment 有沒有正確處理點擊事件 } }
若是須要測試一些更加複雜的狀況,好比 Fragment 的生命週期切換,您能夠調用 Scenario 的 moveToState() 方法,來讓 Fragment 觸發各類生命週期。測試 Fragment 的重建也是相似操做,假如您想要測試是否正確存儲和恢復了 Fragment 的狀態信息,只須要調用 recreate() 方法,就能夠檢查 Fragment 重建先後狀態信息的保存狀況,就是這麼簡單。測試
@Test fun testEventMoveToCreatedFragment() { val scenario = launchFragmentInContainer<MyFragment>() scenario.moveToState(State.CREATED) } @Test fun testEventFragment() { val scenario = launchFragmentInContainer<MyFragment>() scenario.recreate() }
講到 Fragment 的重建,就聯想到 Fragment 的實例化。Fragment 已經有不少種實例化方式了,後來又有了 FragmentScenario。咱們但願能統一這些方法,而解決方案即是 FragmentFactory,它讓咱們能夠注入 Fragment 的構造方法,也順帶解除了 Fragment 必須有一個無參構造方法的限制。動畫
下面是一個簡單的 FragmentFactory,它只有一個方法 —— instantiate,您只須要在這個方法中傳入 Fragment 的類名,隨後 super.instantiate() 方法就會使用反射調用對應 Fragment 的無參構造方法。正如咱們在 《Android 依賴注入指南》 這場演講中提到的,咱們很樂意經過這種模式來減小使用者的重複工做。而若是您須要傳入參數,則能夠將參數傳入 FragmentFactory 並經過構造方法注入將參數傳入 Fragment。
接下來,您須要將 FragmentManager 的 FragmentFactory 設置爲您的 FragmentFactory 。這一步最好放在 super.onCreate() 以前,由於它是從新實例化 Fragment 的地方。
private class MyFactory() : FragmentFactory() { override fun instantiate( classLoader: ClassLoader, className: String ) = when (className) { MyFragment::class.java.name -> MyFragment() else -> super.instantiate(classLoader, className) } } override fun onCreate(savedInstanceState: Bundle?) { supportFragmentManager.fragmentFactory = MyFactory() super.onCreate(savedInstanceState) }
爲了保證 API 的一致性,咱們還準備經過下面的方式統一其餘地方建立 Fragment 的方式。好比 Commit 操做,咱們代理了您的 FragmentFactory,如今您只須要使用 Fragment 的類名,經過一行簡單的代碼,便能完成 Fragment 的建立、添加和初始化。
// 經過類名來添加 Fragment supportFragmentManager.commit { add<MyFragment>(R.id.container) }
相似的,使用 FragmentScenario 時,只須要傳入您的 FragmentFactory 便可。這個 FragmentFactory 既能夠是隻用來模擬依賴的虛擬 Factory,也能夠是用於更多測試的真實 FragmentFactory。
// 使用自定義 FragmentFactory 建立 FragmentScenario val scenario = launchFragmentInContainer<MyFragment>(factory = MockFactory())
關於 API 的一致性,咱們也嘗試解決了 Fragment 的另外一個一致性問題。
咱們發如今添加 Fragment 時,經過 <Fragment> 標籤添加與經過 FragmentTransaction 使用的是徹底不一樣的兩套系統。爲了提供行爲一致的 API,咱們建立了 FragmentContainerView,並把它做爲 Fragment 專屬的容器。
FragmentContainerView 繼承於 FrameLayout,但它只容許填充 FragmentView。它同時也替代了 <Fragment> 標籤,只要在 class 屬性中傳入類名便可。因爲 FragmentContainerView 內部使用的是 FragmentTransaction,因此無需擔憂,稍後在替換這個 Fragment 時也不會出現問題。
<!-- 與在 onCreate 中調用 add() 方法效果相同 --> <androidx.fragment.app.FragmentContainerView class="com.example.MyFragment" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" />
FragmentContainerView 也讓咱們有機會解決一些動畫問題。例如 Fragment 在 Z 軸的層級問題。以下圖所示,咱們能夠看到在 FrameLayout 中,Fragment 切換時沒有顯示動畫,而是整個跳出到了屏幕上。這種問題是因爲切入的 Fragment 和它的動畫位於以前的 Fragment 的層級之下致使的。而 FragmentContainerView 會確保 Fragment 間的層級關係處於正確的狀態,咱們就能夠看到切換動畫了。
另外一個長期困擾咱們的問題,是在 Fragment 中處理系統回退事件。爲了解決這個問題,咱們加入了 onBackPressedDispatcher。咱們沒有選擇在 Fragment 中添加這個 API,而是將其加入了 Activity 中。如今任何組件均可以經過依賴 Activity 來處理回退事件。
下面是一段使用 onBackPressedDispatcher 的示例代碼。您能夠看到,首先 Fragment 從調用它的 Activity 中獲取 onBackPressedDispatcher 對象,而後經過 addCallBack() 方法建立了一個 OnBackPressCallback,因爲 Fragment 是 LifecycleOwner,因此這裏能夠傳入 "this"。在此示例中,若是用戶觸發了回退操做,就會彈出一個確認窗口,而若是用戶隨後表示不管如何都想要退出的話,您能夠先使回調失效,而後就能夠執行默認的回退操做。
val dispatcher by lazy { requireActivity().onBackPressedDispatcher } lateinit var callback: OnBackPressedCallback override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) callback = dispatcher.addCallback(this) { showConfirmDialog() } } private fun onConfirm() { callback.enabled = false dispatcher.onBackPressed() }
因此這裏其實並無新的 API,只是整合了 Fragment 和架構組件現有功能。而咱們接下來也打算進一步加深與架構組件的整合。舉個例子,在 Fragment 中理應能夠方便地得到 ViewModel 實例,但現實的情況卻稍微有些麻煩。爲了解決這個問題,咱們建立了一些 Kotlin 屬性代理。以下面的代碼所示,利用這些屬性代理,您能夠輕鬆得到不一樣做用域的 ViewModel。
// 讓獲取 ViewModel 實例變得簡單 val viewModel : MyViewModel by viewModels() val navGraphViewModel: MyViewModel by navGraphViewModels(R.id.main) val activityViewModel: MyViewModel by activityViewModels()
咱們也從 Lifecycle 組件中受益良多。好比,咱們再也不使用自定義的生命週期方法 setUserVisibleHint,取而代之的是在添加 Fragment 到 ViewPager 或 Adapter 時調用統一的生命週期。這即是 ViewPager2 目前的工做機制,只有當前頁面的 Fragment 會調用 onResume 方法。
// 設置只讓當前展現的 Fragment 調用 onResume() 方法 class MyAdapter : FragmentPagerAdapter(BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
前面講過的功能大多在 Fragment 1.1 中已經提供,與此同時,咱們強烈建議使用 FragmentContainerView 容器來存儲動態添加的 Fragment,而不要使用 FrameLayout 或其餘佈局。
固然,將來咱們還將對 Fragment 作出許許多多的改進,下面我就來介紹幾個咱們當前正在進行的長期規劃。不過要注意的是,接下來部份內容目前尚未正式推出,因此一些細節可能會有改變。
首先要講的是多重回退棧 (Multiple Back Stack)。咱們知道在 Android 中,老是會有一個 Activity 棧,而 Fragment 也實現了一樣的結構,用於保存回退棧信息。而咱們想要實現的則是一種同時支持單一回退棧和多重回退棧的模型,好讓屏幕上不可見的 Fragment 也能保存本身的狀態,從而避免狀態的丟失。與此相關的使用場景,比較典型的就是底部導航一類的導航視圖。
下面是一個咱們的示例應用。咱們想要作的事情就是讓應用中每一個底部標籤頁都擁有本身的棧,這樣它們就能保存各自的狀態。而當您在這些標籤頁間切換時,咱們也將幫您處理好從一個棧到另外一個棧時狀態的保存和恢復。
咱們想要解決的另外一個問題與返回結果有關。
一直以來,諸如如何在 Fragment 間通信,或者說如何在 Android 的各類組件間通信的這類問題都深深困擾着咱們。想要在 Fragment 間通信,方法有不少,它們有好有壞。而這正體現出 Fragment 在這方面的 API 設計不佳。咱們能夠設計一些用於 Fragment 間通信的 API,而且讓它們在基於 Fragment 間互相持有依賴的前提下工做。可是這樣的話,當前的 Fragment 將沒法感知其它 Fragment 的生命週期。若是通信的 Fragment 處在不活躍的生命週期中,那麼通信也將失敗。
還有一個選項,是使用相似 onActivityResult 的 API。但咱們所考慮的,不僅是在 Fragment 之間通信,而是但願能設計出一套公用的 API。它應當同時兼容 Activity、Fragment 等可能的導航組件,這樣就算不知道對方的類型,也能創建通信。
最後要說的問題,是 Fragment 的生命週期。當前 Fragment 的生命週期十分複雜,它包含了兩套不一樣的生命週期。Fragment 本身的生命週期從它被添加到 FragmentManager 的時候開始,一直持續到它被 FragmentManager 移除並銷燬爲止;而 Fragment 所包含的視圖,則有一個徹底分離的生命週期。當您的 Fragment 進入回退棧時,視圖將會被銷燬。但 Fragment 則會繼續存活。
因而咱們產生了一個大膽的想法: 將二者合二爲一會怎麼樣?在 Fragment 視圖銷燬時便銷燬 Fragment,想要重建視圖時就直接重建 Fragment,這樣的話將大大減小 Fragment 的複雜度。而諸如 FragmentFactory 和狀態保存一類,以往在 onConfigrationChange、 進程的死亡和恢復時使用的方法,在這種狀況下將會成爲默認選項。
固然,這個改動將會是十分的巨大。咱們目前處理的方式,是將它做爲一個可選 API 加入到了 FragmentActivity 中。使用了這個新的 API,就能夠開啓生命週期簡化過的新世界。
咱們前面講了 Fragment 一些歷史問題的由來,以及咱們剛剛爲它加入的一些特性,包括:
最後還介紹了幾個咱們仍在開發中的功能:
但願這些內容能夠幫助您更好地使用和理解 Fragment。
咱們正努力將文中提到的新特性帶給各位開發者,而在此以前,若是您在使用 Fragment 時有任何問題和疑惑,可使用 issuetracker.google.com 向咱們提交反饋或功能請求,謝謝!
您也能夠經過視頻回顧 2019 Android 開發者峯會演講 —— Fragment 的過去、如今和未來。