activity 点击后传递数据给fragment_Fragment 的过去、现在和将来
Fragment 是 Android 中歷史十分悠久的一個組件,它在 API 11 被加入,時至今日已成為 Android 開發中最常用的組件之一。Fragment 有了哪些新特性、修復了哪些問題,都是開發者們十分關心的話題。下面我們就來重新說一說 Fragment —— 不僅僅是說現在的 Fragment,還會回顧它的發展,并讓您一瞥它未來的樣子。
Fragment 的誕生與發展
不知道您是否還記得 "上古時期",在那些還沒有 Fragment 的日子,幾乎所有邏輯都被放在了 Activity 中,使得 Activity 臃腫而又混亂。此時,Fragment 作為一個微型 Activity 而誕生,邁出了縮減 Acitvity 之路上的一小步。
不過 "欲戴王冠,必承其重",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 正是由此而來。
Fragment 的現狀
我們的想法產生改變,還是 2011 年的事,到今天已經經歷了很長的時間。這期間我們花費了很多的精力去重新構思 Fragment 的定位。我們希望 Fragment 成為一個真正的核心組件,它應該擁有可預測的、合理的行為,不應該出現隨機錯誤,也不應該破壞現有的功能。
其實我們希望挑個時間發布 Fragment 的 2.0 版,它將只包含那些新的、好用的 API。但在時機成熟之前,我們會在現有的 Fragment 中逐步加入新的并棄用舊的 API,并為舊功能提供更好的替代方案。當沒人再使用已棄用的 API 時,遷移到 Fragment 2.0 就會變得很容易。接下來我就來講講,我們為此所做的一些工作。
FragmentScenario
首先我要說,合理的 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() }FragmentFactory
講到 Fragment 的重建,就聯想到 Fragment 的實例化。Fragment 已經有很多種實例化方式了,后來又有了 FragmentScenario。我們希望能統一這些方法,而解決方案便是 FragmentFactory,它讓我們可以注入 Fragment 的構造方法,也順帶解除了 Fragment 必須有一個無參構造方法的限制。
下面是一個簡單的 FragmentFactory,它只有一個方法 —— instantiate,您只需要在這個方法中傳入 Fragment 的類名,隨后 super.instantiate() 方法就會使用反射調用對應 Fragment 的無參構造方法。正如我們在《Android 依賴注入指南》這場演講中提到的,我們很樂意通過這種模式來減少使用者的重復工作。而如果您需要傳入參數,則可以將參數傳入 FragmentFactory 并通過構造方法注入將參數傳入 Fragment。
接下來,您需要將 FragmentManager 的 FragmentFactory 設置為您的 FragmentFactory 。這一步最好放在 super.onCreate() 之前,因為它是重新實例化 Fragment 的地方。
// 使用自定義 FragmentFactory 創建 FragmentScenario val scenario =launchFragmentInContainer<MyFragment>(factory = MockFactory())為了保證 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())FragmentContainerView
關于 API 的一致性,我們也嘗試解決了 Fragment 的另一個一致性問題。
我們發現在添加 Fragment 時,通過 <Fragment> 標簽添加與通過 FragmentTransaction 使用的是完全不同的兩套系統。為了提供行為一致的 API,我們創建了 FragmentContainerView,并把它作為 Fragment 專屬的容器。
FragmentContainerView 繼承于 FrameLayout,但它只允許填充 FragmentView。它同時也替代了 <Fragment> 標簽,只要在 class 屬性中傳入類名即可。由于 FragmentContainerView 內部使用的是 FragmentTransaction,所以無需擔心,稍后再替換這個 Fragment 時也不會出現問題。
<!-- 與在 onCreate 中調用 add() 方法效果相同 --> <androidx.fragment.app.FragmentContainerViewclass="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 間的層級關系處于正確的狀態,我們就可以看到切換動畫了。
OnBackPressedDispatcher
另一個長期困擾我們的問題,是在 Fragment 中處理系統回退事件。為了解決這個問題,我們加入了 onBackPressedDispatcher。我們沒有選擇在 Fragment 中添加這個 API,而是將其加入了 Activity 中。現在任何組件都可以通過依賴 Activity 來處理回退事件。
下面是一段使用 onBackPressedDispatcher 的示例代碼。您可以看到,首先 Fragment 從調用它的 Activity 中獲取 onBackPressedDispatcher 對象,然后通過 addCallBack() 方法創建了一個 OnBackPressCallback,由于 Fragment 是 LifecycleOwner,所以這里可以傳入 "this"。在此示例中,如果用戶觸發了回退操作,就會彈出一個確認窗口,而如果用戶隨后表示無論如何都想要退出的話,您可以先使回調失效,然后就可以執行默認的回退操作。
// 設置只讓當前展示的 Fragment 調用 onResume() 方法 class MyAdapter : FragmentPagerAdapter(BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)所以這里其實并沒有新的 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 的未來
前面講過的功能大多在 Fragment 1.1 中已經提供,與此同時,我們強烈建議使用 FragmentContainerView 容器來存儲動態添加的 Fragment,而不要使用 FrameLayout 或其他布局。
當然,未來我們還將對 Fragment 做出許許多多的改進,下面我就來介紹幾個我們當前正在進行的長期規劃。不過要注意的是,接下來部分內容目前還沒有正式推出,所以一些細節可能會有改變。
多重回退棧 (Multiple Back Stack)
首先要講的是多重回退棧 (Multiple Back Stack)。我們知道在 Android 中,總是會有一個 Activity 棧,而 Fragment 也實現了同樣的結構,用于保存回退棧信息。而我們想要實現的則是一種同時支持單一回退棧和多重回退棧的模型,好讓屏幕上不可見的 Fragment 也能保存自己的狀態,從而避免狀態的丟失。與此相關的使用場景,比較典型的就是底部導航一類的導航視圖。
下面是一個我們的實例應用。我們想要做的事情就是讓應用中每個底部標簽頁都擁有自己的棧,這樣它們就能保存各自的狀態。而當您在這些標簽頁間切換時,我們也將幫您處理好從一個棧到另一個棧時狀態的保存和恢復。
Fragment 間的通訊問題
我們想要解決的另一個問題與返回結果有關。
一直以來,諸如如何在 Fragment 間通訊,或者說如何在 Android 的各種組件間通訊的這類問題都深深困擾著我們。想要在 Fragment 間通訊,方法有很多,它們有好有壞。而這正體現出 Fragment 在這方面的 API 設計不佳。我們可以設計一些用于 Fragment 間通訊的 API,并且讓它們在基于 Fragment 間互相持有依賴的前提下工作。但是這樣的話,當前的 Fragment 將無法感知其它 Fragment 的生命周期。如果通訊的 Fragment 處在不活躍的生命周期中,那么通訊也將失敗。
還有一個選項,是使用類似 onActivityResult 的 API。但我們所考慮的,不只是在 Fragment 之間通訊,而是希望能設計出一套公用的 API。它應當同時兼容 Activity、Fragment 等可能的導航組件,這樣就算不知道對方的類型,也能建立通訊。
簡化 Fragment 的生命周期
最后要說的問題,是 Fragment 的生命周期。當前 Fragment 的生命周期十分復雜,它包含了兩套不同的生命周期。Fragment 自己的生命周期從它被添加到 FragmentManager 的時候開始,一直持續到它被 FragmentManager 移除并銷毀為止;而 Fragment 所包含的視圖,則有一個完全分離的生命周期。當您的 Fragment 進入回退棧時,視圖將會被銷毀。但 Fragment 則會繼續存活。
于是我們產生了一個大膽的想法: 將兩者合二為一會怎么樣?在 Fragment 視圖銷毀時便銷毀 Fragment,想要重建視圖時就直接重建 Fragment,這樣的話將大大減少 Fragment 的復雜度。而諸如 FragmentFactory 和狀態保存一類,以往在 onConfigrationChange、 進程的死亡和恢復時使用的方法,在這種情況下將會成為默認選項。
當然,這個改動將會是十分的巨大。我們目前處理的方式,是將它作為一個可選 API 加入到了 FragmentActivity 中。使用了這個新的 API,就可以開啟生命周期簡化過的新世界。
總結
我們前面講了 Fragment 一些歷史問題的由來,以及我們剛剛為它加入的一些特性,包括:
- FragmentScenario,Fragment 的測試框架
- FragmentFactory,統一的 Fragment 實例化組件
- FragmentContainerView,Fragment 專屬視圖容器
- OnBackPressedDispatcher,幫助您在 Fragment 或其他組件中處理返回按鈕事件
最后還介紹了幾個我們仍在開發中的功能:
- 多重回退棧
- 使 Fragment 以及其他導航組件間可以優雅的通訊
- 簡化 Fragment 的生命周期
希望這些內容可以幫助您更好地使用和理解 Fragment。
總結
以上是生活随笔為你收集整理的activity 点击后传递数据给fragment_Fragment 的过去、现在和将来的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《创造吧我们的星球》植物种植嫁接攻略-植
- 下一篇: midjourney怎么进不去? Mid