QMUI實戰(二)—Activity 和 Fragment,咱們該選擇誰?

在一開始, 官方只提供了 Activity 來做爲 UI 界面的載體,所以咱們也別無選擇,只能用它。而在 Android 3.0 後,Fragment 也面世了,它一開始是用於適配平板的,以郵件列表與詳情的適配爲例,手機端夠小,所以開始展現列表,點擊進入詳情,而平板夠大,則能夠列表顯示在左側,詳情顯示在右側,點擊列表只是切換詳情。對於這種適配場景,列表頁和詳情頁必須在同一個 Activity 裏了,而這即是我所知道的 Fragment 誕生的場景了。可是隨着 Fragment 框架的不斷完善, 單 ActivityFragment 的架構也被提出了,甚至如今官方的 navigation 庫都是這種模式, 可見官方對 Fragment 是多麼的青睞了。android

固然,如今咱們寫 UI 也不僅是這兩種選擇了, Reative Native, Flutter,以及未來的 compose 爲咱們寫 UI 提供了不少不少的選擇,同時咱們學習的東西也愈來愈多了。今天咱們只討論一些 ActivityFragment 相關的話題,對於另外的幾個,我只是期待 compose 時代的早點到來。緩存

Activity

提到 Activity,可能最容易犯的一個錯誤就是忘記在 AndroidManifest 裏註冊了吧,直到運行出錯,纔想起要去註冊,而後再編譯,時間就是這樣沒了的。對於 Activity 的使用,咱們須要關注生命週期、啓動模式、setContentView 外,咱們還須要掌握的一個關鍵知識點就是 SaveState 了。bash

iOS 端、 Web 端都是不存在 SaveState 的,於是這幾乎是 Android 獨有的。由於 Android 最初的年代是比較看輕 View 的。 在 Android 眼裏, View 是廉價的,數據、狀態是寶貴的,View 是能夠隨時銷燬,而後根據狀態進行恢復的。例如在橫豎屏旋轉、Dark Mode 切換這些場景中,Android 就選擇銷燬掉當前 Activity,而後再從新建立,從新建立時會去 resource 裏讀當前配置下的資源,例如字體大小、顏色等等,這些資源都是能夠根據不一樣狀態在不一樣文件件裏配置成不一樣的值,於是咱們就不須要去寫各類判斷。可是問題來了,當 UI 顯示出來後,大多數狀況是有數據渲染或狀態變動的,那麼銷燬重建後,咱們的數據該如何恢復呢? 這就要靠 SaveState 機制了:在銷燬時,咱們把數據保存下來,而後在重建時,咱們再把數據恢復出來。架構

固然,你也能夠選擇不要讓系統重建你 Activity,而是本身處理配置的變動。作法就是在 AndroidManifest 文件裏爲 ActivityconfigChanges 屬性里加上你想要本身處理的配置。例如:框架

android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
複製代碼

具體的配置項能夠看官方文檔。 例如 orientation 即是橫豎屏旋轉, uiMode 能夠用於接收 Dark Mode 的切換。當這些配置好後。 咱們在 Activity 裏能夠經過重寫 onConfigurationChanged 來接受配置的變動。ide

咱們說回 SaveState。 當發生配置變動、或者由於內存等緣由被系統殺死的 Activity, 會調用 onSaveInstanceState 來保存 UI 狀態,默認狀況下會遍歷 View, 而後以 View 的 id 爲鍵來收集 View 的狀態。固然每一個 View 都是能夠重寫 onSaveInstanceState 來更改默認行爲, 例如 RecyclerView/ListView 就阻止了子 View 狀態的保存。那麼這裏就有一個問題了。 若是同一個 Activity 裏出現了兩個 id 相同的 View, 那會怎樣呢? 那樣就會發生狀態的覆蓋,狀態恢復也會混亂。不要覺得這種事情不會發生,我來列舉下有可能出現的場景,看看你有沒有遇到呢?函數

  1. 如今界面愈來愈複雜了,同一個 Activity 裏可能會使用 ViewPagerViewPager 的現象,而不少人起 id 也很隨意,不會業務化,都叫 viewPager,而後就會發現,我本來選中的是第二個 tab,怎麼屏幕一旋轉,就變成第三個了?這就是由於 ViewPager 會保存當前選中的是哪一個 tab,多個相同 id 的 ViewPager, 在狀態保存是後者覆蓋前者,恢復過來後永遠是最後一個 ViewPager 選中的狀態了。
  2. 有許多人切換 Fragment 時, 使用的是 add 而不是 replace。這樣就形成同一時刻可能有不少 Fragment 共存,每個 Fragment 在界面上的表現就是一個 View, 這樣無形增長了系統佈局、渲染的時間外,也是有可能狀態保存於恢復出錯的, 例如每一個 Fragment 都有一個 RecyclerView,而後它的 id 是 recyclerView, 這樣界面上就有不少個同 id 的 RecyclerView 了,而 RecyclerView 在狀態保存時會保存滾動位置的,那麼當屏幕旋轉後,你全部的 RecyclerView 狀態都恢復成了最後一個保存的 RecyclerView 的了,就會出現滾動到了開始或者某個奇奇怪怪的位置了。
  3. RecyclerView 沒成熟以前,咱們作橫向的翻頁滾動,基本都是用 ViewPager 作的(又是它),若是咱們 ViewPager 的每個 Pager 都採用相同的 xml, 那又可能出現這種問題了,假想一下,你每一個 Pager 都有一個進度條,結果屏幕一旋轉,進度全都亂套了。 那麼你們如今就知道緣由了。 RecyclerView 不會出現這種問題,由於它攔截了 SaveState 的派發,徹底由 Adapter 的數據決定 UI 的渲染。

在複雜的 UI 下,各類奇奇怪怪的狀態問題老是容易出現,但問題的本質都是相同的,所以熟悉 UI 的人,在遇到這樣的場景,會快速反應過來問題出如今哪裏了,進而進行修復。而若是可以在寫 UI 時就考慮到這種問題,起 id 名不隨意,那就更妙了。佈局

這其實也暴露了另一點,咱們在作自定義 View 時,是否有考慮這些問題,坦白點講, QMUI 在這方面作得就不夠好,咱們更多的強調自適應,而推薦用 configChange 來規避了 StateSave 的問題。性能

最後一點,若是咱們有大量的數據,這種 StateSave 的確定是不知足需求的,這個時候,Activity 提供了 onRetainNonConfigurationInstance 的方式來保存狀態無關的數據,若是你使用 ViewModel, 你就根本不須要關係這些了, 在 Activity 銷燬與重建後,使用的是同一個 ViewModel, 所以仍是早點用上 ViewModel 吧,它不止你表面上看到的那些東西。學習

Fragment

Fragment 雖然比 Activity 輕量,有不少特性,可是使用起來是比 Activity 複雜的, 相應的坑點也比 Activity 多。

第一點就是它的生命週期有兩個,一個是 Fragment 的生命週期, 另一個就是 Fragment 管理的 View 的生命週期。爲何會有兩個呢? 前面已經說過了,Android 是輕 UI 的,那麼從 FragmentA 切換到 FragmentB 時,就會去釋放掉 FragmentA 管理的 View,從 FragmentB 返回到 FragmentA 時,再從新建立 View。 因此 Fragment.onCreateView(三參數)Fragment.onDestoryView()會被屢次調用,這或許是不少人都會疑惑的點。

咱們在切換 Fragment 的時候就會發生 View 的銷燬,那麼一樣須要作 StateSave 了,所以,FragmentonSaveInstanceStateonViewStateRestored 基本上是與 View 的生命週期掛鉤的,而不只僅是屏幕旋轉、Dark Mode 切換時才調用了。固然,通常狀況下,在 Activity 銷燬與重建時,Fragment 也會銷燬與重建。但若是你調用了 Fragment.setRetainInstance(true) ,它就會走 ActivityonRetainNonConfigurationInstance,而不會銷燬重建了,可是 View 的生命週期依舊會走銷燬重建。

Fragment 存在 View 這個生命週期的另一點就是咱們不能像在 Activity裏同樣隨意操做 UI。 若是你在 onDestoryView 以後操做了 UI,那麼當 View 重建後,以前的操做就白費了。這個時候 ViewModelLiveData 就顯得特別重要了, 它可讓你在數據在正確的生命週期裏渲染到 UI 上。可是須要注意的是咱們對 LifeCircleOwner 的選擇, Fragment 是有兩個 LifeCircleOwner 的,若是與 UI 相關, 咱們應該選用 viewLifeCircleOwner, 而且應該在 onActivityCreated 裏調用 LiveDataobserve 方法。

Fragment 存在另一個問題就是轉場動畫的問題了。若是在轉場動畫過程當中發生了數據渲染,那麼就會產生動畫卡頓現象,由於數據渲染形成了 UI 從新 layout,而 Activity 是 window 動畫,不受這個影響。對比起來,可能 Activity 的動畫更加流暢了。 而咱們也沒辦法解決 Fragment 的這個問題,只能避免在動畫過程當中進行數據渲染了,所以 QMUI 提供了 runAfterAnimation 方法。

Fragment 是用 FragmentManager 來管理的,而後經過 FragmentTransaction 來實現 Fragment 的添加、刪除與切換等。 固然 Fragment 提供了 childFragmentManager,這使得咱們能夠一層一層的添加 Child Fragment。 而當 Activity 切換到不一樣生命週期狀態時,會沿着 FragmentManager 派發給全部的 Fragment。 這是頗有用的,例如在作 ViewPagerPager 時,咱們能夠採用自定義 View, 也能夠採用 Fragment。可是當 Pager 須要在 onResumeonPause 等時機作一些事情時,若是用自定義 View 時,那就須要咱們寫一堆的代碼來控制這一切,而採用 Fragment 時,則能夠方便的運用這個特性來實現。所以官方也提供了 FragmentPagerAdapter。 若是咱們有一些公用的業務 UI 大組件, 不妨也經過它來封裝,方便生命週期的管理。

QMUI 爲 Activity 和 Fragment 添加的功能

QMUI 的 arch 庫,爲 ActivityFragment 添加了一些新的功能。

首先就是手勢返回,這個在 iOS 上是自帶的,而 Android 卻須要開發者本身去實現。一個主要的緣由就是 Android 是主張 View 能夠隨時建立與銷燬。 以 Fragment 爲例,當咱們從 FragmentA 切換到 FragmentB 後, FragmentA 的 View 已經銷燬了,這個時候手勢返回時確定沒法獲取到以前那個 View 的,所以默認是沒法實現相似 iOS 那種手勢返回的。但產品們就是但願看到這種手勢返回效果, 因此咱們也必須作,對於 Fragment 而言, QMUI 就是在 QMUI 層用成員變量保存了 Fragment 建立的 View, 而後在下次建立的時候直接傳入緩存的 View, 這直接打破了本來的官方的邏輯,也就會引入新的翔, 好比咱們可能會在 onActivityCreated 爲 TopBar 添加子 View,但由於 View 緩存後,就會出現重複添加的問題,所以 QMUI 也提供了 onViewCreated(一參數) 規避這個問題。Fragment 的手勢返回大量的運用了發射來更改 FragmentManager 裏記錄的數據信息,這是基於閱讀 FragmentManager 源碼後找到的插入點,通常而言,這種實現是有版本兼容問題的:若是官方更新了實現,那個人反射也就會失敗,但目前 Fragment 框架比較穩定了,也不會輕易去改核心功能的,因此影響其實還好。

相比於 Fragment 的手勢返回。 Activity 的手勢返回實現會簡單不少,可是會存在一些沒法解決的問題。目前主流的有三種實現方式:

  1. 手勢返回將 Activity 透明掉, 這樣就能夠看到背後的 Activity 了。可是調用透明 Activity 的方法是反射,並且比較耗時,而且背後的 Activity 沒辦法移動, 作不了視差。
  2. 手勢返回時將前一個 Activity 的 View 取下來,而後添加到當前 Activity 的 View 的下層。 手勢返回後再將 View 放回去。這個方案存在 ViewWindow 的添加與移除,並且沒法處理背後的 Activity有顯示 Dialog 的場景。
  3. 手勢返回時將前一個 Activity 的 View 以及 Dialog 的 View 繪製到當前 Activity 的背後,這個是 QMUI 提供的方案,性能多是最優的, 由於只是多了繪製,但問題是若是前一個 ActivityonPause 以後更改了 View 的顯示,那就可能在手勢返回時有錯誤的表現(例如 FaceBook 提供的 DraweeView),另外它對 SurfaceView 等支持也不太好,對於這種場景,直接禁用掉手勢返回比較好,其它的手勢返回其實也不能很好的支持 SurfaceView 這些。

當 Android 10 出來後,它提供了新的 Navigation Gesture,不少國產機已經開始這麼作了,這這種場景下,系統的手勢返回會優先於咱們的手勢返回,這個時候這套手勢返回就有點雞肋了(儘管它的交互必系統的更優雅,更貼合 iOS)。

QMUI 提供的另一些功能就只是一些 util 性質的函數了。例如 startFragmentstartFragmentAndDestroyCurrent等,後一個是一個特殊化的實現,對於官方設計而言,他們以爲應該遵循用戶行爲,我從 A 點擊進入 B,而後進入 C, 那麼我返回是就應該從 C 返回 B,再返回到 A。但產品需求可能不是這樣,例如我把某篇文章分享給某個用戶,那麼我要先打開用戶列表,選擇某個用戶,再進入用戶的聊天界面。當我從聊天界面返回時,應該是返回的文章界面,而不是中間的用戶列表界面。而咱們的 Fragment 並無 Activityfinish 方法來結束本身,這就是 startFragmentAndDestroyCurrent 存在的價值了。

另外須要提一下的是 FragmentManager.popBackStack() 這個方法,它的意思是將當前 Fragment 從堆棧中移除,咱們的返回就是經過它作的,可是它卻不是任什麼時候候都能調用的,通常在用戶主動的點擊行爲下調用是沒有問題的,但若是你想在 Fragment.onCreate 裏調用,是不行的,即便是在 onResume 裏也是不行的,對於上面那個分享的需求場景,若是你想以返回到用戶列表時馬上執行 popBackStack 來取代 startFragmentAndDestroyCurrent,那是會失敗的,若是有興趣,你能夠本身試試,官方的 Fragment 會直接 crash 掉,而 QMUIFragment 則會忽略此次執行。其緣由是由於 FragmentManager 裏的狀態機要在 onResume 以後才賦值當前狀態。 這是一個很常見的重入報錯的問題,我簡單寫個相似的例子,看完後你估計就能猜到是怎麼回事了:

interface Observer{
    fun doSomething()
}

class Observable{
    private val observers = arrayListOf<Observer>()
    
    fun addObserver(observer: Observer){
        observers.add(observer)
    }
    
    fun removeObserver(observer: Observer){
        observers.remove(observer)
    }
    
    fun dispatch(){
        val count = observers.size
        for(i in 0 until count){
            observers[i].doSomething()
        }
    }
}
複製代碼

上面這個代碼很簡單,就是觀察者模式的運用,你們可否看出其中錯誤呢?

假設個人調用是這樣:

val observable = Observable()
val secondObserver = object : Observer {
    override fun doSomething() {
        //....
    }
}
val firstObserver = object : Observer {
    override fun doSomething() {
        observable.removeObserver(secondObserver)
    }
}
observable.addObserver(firstObserver)
observable.addObserver(secondObserver)
observable.dispatch()
複製代碼

若是運行這段代碼,你會發現報 IndexOutOfBoundsException,這是由於咱們在第一個 Observer 將第二個 Observer 移除了,從而形成 Observable.dispatch 裏的 for 循環的 count 錯誤。 咱們的 Fragment.onResume 也處於這種階段,若是在這裏面去移除 Fragment,可能致使 FragmentManager 裏的某些變量值出錯。固然, FragmentManager 裏是有保護的,是不容許你的某些操做的,你操做了就拋出錯誤,終止整個執行。

QMUI arch 框架目前也在嘗試引入一些註解來簡化代碼,例如 FirstFragment 註解,例如 LatestVisitRecord 註解。在以後的實戰過程當中,我會詳細介紹這些註解使用的場景。

#總結 今天主要介紹了一些 ActivityFragment 的知識。知識點是比較零散的,須要咱們在使用過程當中不斷地學習與掌握。並且咱們不是作選擇題,而是二者都要會,在適合的場景引入 Fragment,能夠極大的減小工做量。例如在插件化場景中,咱們就不須要解決 Activity 的註冊問題了。 而 SaveStateViewModel、手勢返回等都埋藏了不少細節的知識點,咱們要想熟練的駕馭它,就須要熟悉它的各類坑點以及坑點產生的緣由。新手每每寫點 UI 就去看看效果,這是對各個控件極其不熟悉的緣由。若是能作到寫半天的代碼,一次編譯,結果效果全是本身想要的,那編寫 UI 的能力就是真正的上了一個臺階了。

GankWithQmui 會採用單 Activity 多 Fragment 的架構,這是我比較喜歡的架構。下一次咱們就開始構建咱們的第一個 Fragment 了。

下期博文:QMUI實戰(三)——你是如何啓動你的第一個 Fragment 的?

相關文章
相關標籤/搜索