起初Jetpack Navigation把我逼瘋了,可是後來真香

作者:年小個大
鏈接:https://juejin.cn/post/6925331537399382030

1. Navigation到底該如何正確的使用

相信大家對 Navigation都有所耳聞,我不細說怎麼用了,官方的講解也很詳細。我是想說一下到底該如何更好的使用這個組件。

這個組件其實是需要配合官方的MVVM架構使用的,ViewModel+LiveData結合才能更好的展現出Navigation的優勢。

在官方的講解示例中沒有用到ViewModelLiveData,官方只是演示了Navigation怎麼用怎麼在頁面之間傳值,和這個組件的一些特性之類的。但真正用好還是要結合ViewModelLiveData

2. Navigation大家都以爲的缺陷

起初我用Navigation的時候,最頭疼的是當按下返回鍵回到上個頁面的時候整個頁面被重建了,這是開發中不想要的結果,很多時候大家都會去尋求一種方式:將官方的replace方式替換爲HideShow。起初也是想到這個方式,然後結合在網上得到的資料自己寫了一個方式FragmentNavigatorHideShow

3. 然而這不是缺陷

但是很快啊,我發現這個方式(HideShow)存在嚴重的邏輯問題。

這裏可以看到,有一些場景下,我們有某個頁面可以打開和自己相同的頁面,只不過是展示的數據不同而已。當我用hideshow的方式展示下個頁面的時候,會發現打開的還是上個頁面。當按下返回鍵之後,上個相同的頁面不見了,新打開的頁面和上個頁面盡然是同一個對象,這肯定不符合業務邏輯。於是我又開始研究起replace的方式,當然我在使用這個Navigation的時候就採用了MVVM + ViewModel+LiveData,這時候我想起ViewModel是不受Fragment重建影響的。於是我打印了一下在使用replace方式下頁面生命週期的變化。

HomeFragment進入MyFragment生命週期變化:

可以看到,在replace之後HomeFragment並沒有執行onDestory而是執行了onDestoryView這也使得頁面必須要重建。而onDestoryView不會導致 ViewModel的銷燬。也就是說 ViewModel還在,ViewModel中的LiveData所保存的數據也是存在的。當我按下返回鍵,重新回到HomeFragment頁面理所當然的執行了onViewCreated,此時代碼中頁面對ViewModel中的LiveData所觀察數據又重新進行了observe觀察,因爲LiveData之前保存過數據所以這段代碼也理所當然的被執行了。頁面上也重新填充了數據。

    override fun initLiveData() {
        viewModel.liveData.observe(this) {
            Log.d(TAG, "data change :  $it ")
            textView.text = it
        }
    }

這個時候,你會發現,頁面好像沒有重建一樣。我這才理解了谷歌的用意。它這步棋下的很巧啊。

也裏所當然的我拋棄了FragmentNavigatorHideShow,又擁抱回了谷歌爸爸。

說回上面那個問題,當一個頁面中可以打開自己的時候,在FragmentNavigator源碼中只要是導航到下一個目的地就會重新創建一個新的Fragment,上一個Fragment會被加入回退棧裏,所以纔可以在Fragment中打開一個新的自己,來展示不同的信息。而hideshow的方式會每次都去查找之前有沒有創建過這個頁面,如果有,就Show,如果沒有就創建。所以纔會導致自己打開自己,永遠都是同一個Fragment對象。

4. 那麼到底該如何正確使用

到底該如何正確使用Navigation,這也是我這段時間使用的一點點經驗。

Fragment中的所有動態數據都由ViewModel中的LiveData保存。我們只監聽LiveData的數據變化,這也符合MVVM 的架構麻,當然還有一個Model我沒說Repository,這個我就不解釋了。

Fragment之間傳遞的數據都交給Bundle頁面重建的時候這些數據也會被保存,再次走一遍從Bundle中取數據的過程是完全不會報錯的。所以頁面上的數據不會被丟失了,而像RecyclerView,ViewPager之類的控件它們也會保存自己之前的狀態,頁面重建後,RecyclerView,ViewPager會記錄自己滑動的位置的,這個不用擔心,還有一點就是有一些控件,比如CoordinatorLayout你可能需要給它和它的子View控件一個Id才能保存滑動狀態。

遵循這樣的一個規則之後呢,就可以忽略這個頁面重建的問題了。

5. Navigation的頁面轉場動畫的一些問題

用過Navigation的都知道,頁面轉場動畫要一個一個的添加,就像這樣:

<!--這是官方的Demo示例-->
<fragment
            android:id="@+id/title_screen"
            android:name="com.example.android.navigationsample.TitleScreen"
            android:label="fragment_title_screen"
            tools:layout="@layout/fragment_title_screen">
        <action
                android:id="@+id/action_title_screen_to_register"
                app:destination="@id/register"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"/>
        <action
                android:id="@+id/action_title_screen_to_leaderboard"
                app:destination="@id/leaderboard"
                app:popEnterAnim="@anim/slide_in_left"
                app:popExitAnim="@anim/slide_out_right"
                app:enterAnim="@anim/slide_in_right"
                app:exitAnim="@anim/slide_out_left"/>
    </fragment>
複製代碼

每一個標籤都要寫一遍一樣的代碼,讓我很頭疼。於是我還是想到了,重寫FragmentNavigator將所有的增加一個判斷如果標籤中沒有設置專場動畫,那麼我就給這個Fragment添加上專場動畫。

        //我一開始設想的載源碼位置處添加的動畫操作
        int enterAnim = navOptions != null ? navOptions.getEnterAnim() : 動畫id;
        int exitAnim = navOptions != null ? navOptions.getExitAnim() : 動畫id;
        int popEnterAnim = navOptions != null ? navOptions.getPopEnterAnim() : 動畫id;
        int popExitAnim = navOptions != null ? navOptions.getPopExitAnim() : 動畫id;
        if (enterAnim != -1 || exitAnim != -1 || popEnterAnim != -1 || popExitAnim != -1) {
            enterAnim = enterAnim != -1 ? enterAnim : 0;
            exitAnim = exitAnim != -1 ? exitAnim : 0;
            popEnterAnim = popEnterAnim != -1 ? popEnterAnim : 0;
            popExitAnim = popExitAnim != -1 ? popExitAnim : 0;
            ft.setCustomAnimations(enterAnim, exitAnim, popEnterAnim, popExitAnim);
        }
複製代碼

然而我太天真了,我們想到的,谷歌爸爸都考慮過了。因爲如果像我一樣天真的加上這樣的判斷之後,你會發現,第一個默認Fragment也擁有了動畫屬性。而且做隱式鏈接跳轉的時候,這個動畫會非常影響觀感。所以第一個默認Fragment不能有轉場動畫。當然後來我想到了判斷返回棧是否存在爲空,通過這個判斷是否是第一個頁面。但是我都能想到谷歌爸爸肯定也想到了。他們不這麼做肯定是有原因的。於是我放棄了,老老實實的挨個複製粘貼,

6. Replace在重建Fragment的時候,過度動畫卡頓

在使用 Navigation的時候,按下返回鍵回到上個頁面,頁面重建,這個時候會發現過度動畫會有那麼幾百毫秒卡那麼一下,一個轉場動畫也就400毫秒左右,卡那麼一下效果是非常明顯的。這也歸功於Fragment重建的原因了,頁面展示的數據量巨大的時候,重建時的繪製工作量也是相當的大,所以肯定會卡那麼一下下啦。

那麼延遲初始化數據能解決嗎?

當然能,感覺多次一舉,延遲初始化數據頁面,數據會突然的從無到有,雖然不影響轉場動畫了,但是界面會閃一下,用戶也會感到莫名其妙:這啥情況?頁面怎麼閃一下?

後來我仔細的觀察發現,Activity進行跳轉的時候會有那麼100毫秒的延遲,Fragment進行切換的時候是沒有延遲的,cua的一下,很快啊。

後來我發現了一個方法:

    override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
        return super.onCreateAnimation(transit, enter, nextAnim)
    }
複製代碼

吼~~ 我好像發現了什麼ψ(`∇´)ψ。

我讓它轉場動畫延遲幾百毫秒執行不就行了,給Fragment重建填充數據時預留個時間,等它差不多重建ok了,再執行動畫不就不卡了。

於是我再BaseFragment中重寫了這個方法,並給動畫100毫秒的延遲時間。

 override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? {
        if (enter) {
            //如何nextAnim是-1或0那麼就是沒有設置轉場動畫,直接走super就行了
            if (nextAnim > 0) {
                val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
                //延遲100毫秒執行讓View有一個初始化的時間,防止初始化時刷新頁面與動畫刷新衝突造成卡頓
                animation.startOffset = 100
                animation.setAnimationListener(object : Animation.AnimationListener{
                    override fun onAnimationStart(animation: Animation?) {
                    }

                    override fun onAnimationEnd(animation: Animation?) {
                        onEnterAnimEnd()
                    }

                    override fun onAnimationRepeat(animation: Animation?) {
                    }

                })
                return animation
            } else {
                onEnterAnimEnd()
            }
        } else {
            if (nextAnim > 0) {
                val animation = AnimationUtils.loadAnimation(requireActivity(), nextAnim)
                //延遲100毫秒執行讓View有一個初始化的時間,防止初始化時刷新頁面與動畫刷新衝突造成卡頓
                animation.startOffset = 100
                return animation
            }
        }
        return super.onCreateAnimation(transit, enter, nextAnim)
    }

    /**
     * 如果真的要延遲初始化,那麼重寫這個方法,等動畫結束了再初始化
     */
    fun onEnterAnimEnd(){

    }

只需要監聽進入動畫就可以了,退出動畫只給它延遲但不給任何的事件監聽。

這樣搞完之後,頁面轉場動畫果然不卡了,延遲100毫秒也完全沒有影響使用體驗。效果很接近Activity轉場。

我可真是個機靈鬼。

我分享的內容,和我是如何使用的就到這裏,我不確定我的方式是不是完全的正確,但目前來看,項目跑的也是挺ok的。沒出什麼問題。

如果我有什麼說的不正確的地方,大家也可以多多指點,交流一下經驗。

尾聲

Jetpack架構組件 並不是一項複雜的技術,很多開發者都可以快速上手。但也正是簡單易懂,開發者卻很容易忽視註解背後的底層技術。在面試和實際架構的過程中,對技術理解膚淺、缺少細節成爲無數開發者的致命傷。最近整理收集了Jetpack架構組件 基礎到實戰底層學習手冊,對於上面這些實戰問題講解很透徹,今天分享給大家。

點擊我,即可獲得!

Jetpack架構組件從入門到精通學習手冊入門篇

這幾個模塊是 Jetpack架構組件 入門篇, 主要介紹 Jetpack架構組件 特性,分類、應用架構 、實戰本節內容主要如下:

Jetpack架構組件實戰到原理手冊—Data Binding篇

Google在2018年推出 Android Jetpack ,本人最近在學習 Android Jetpack ,如果你有研究過 Android Jetpack ,你會發現Livedata,ViewModel和Livecycles等一系列 Android Jetpack 組件非常適用於實現MVVM,因此,在進行 Android Jetpack 的下一步研究之前, 我們有必要學習一下MVVM設計模式以及Android中實現MVVM的 Data Binding 組件。

Jetpack架構組件實戰到原理手冊— ViewModel & LiveData篇

由於 ViewModel 和 LiveData 關聯性比較強且使用簡單(其實 LiveData 可以和很多組件一起使用), 故打算一次性介紹這兩個Android Jetpack 組件。

Jetpack架構組件實戰到原理手冊— Room 篇

我們在日常的工作中,免不了和數據打交道,因此,存儲數據便是一項很重要的工作,在此之前,我使用過GreenDao、DBFlow等優秀的ORM數據庫框架,但是,這些框架都不是谷歌官方的,現在,我們有了谷歌官方的Room 數據庫框架,看看它能夠給我們帶來什麼?


Jetpack架構組件實戰到原理手冊— Paging 篇

我相信幾乎所有的Android開發者都會遇到在 RecyclerView 加載大量數據的情況,如果是在數據庫請求,需要消耗數據庫資源並且需要花費較多的時間,同樣的,如果是發送網絡請求,則需要消耗帶寬和更多的時間,無論處於哪一種情形,對於用戶的體驗都是糟糕的。在這兩種情形中,如果採用分段加載則縮短了時間,給用戶帶來了良好的體驗。


Jetpack架構組件實戰到原理手冊— WorkManger 篇

Android中處理後臺任務的選擇挺多的,比如 Service 、 DownloadManager 、 AlarmManager 、 JobScheduler等,那麼選擇 WorkManager 的理由是什麼呢?


Jetpack架構組件實戰到原理手冊— Lifecycle篇

一直以來,解藕都是軟件開發永恆的話題。在Android開發中,解藕很大程度上表現爲系統組件的生命週期與普通組件之間的解藕,因爲普通組件在使用過程中需要依賴系統組件的的生命週期。舉個例子,我們經常需要在頁面的onCreate()方法中對組件進行初始化,然後在onStop()中停止組件,或者在onDestory()方法中對進行進行銷燬。事實上,這樣的工作非常繁瑣,會讓頁面和頁面耦合度變高,但又不得不做,因爲如果不即時的釋放資源,有可能會導致內存泄露。


Jetpack架構組件實戰到原理手冊— Compose 最全上手指南

Jetpack Compose 是一個用於構建原生Android UI 的現代化工具包,它基於聲明式的編程模型,因此你可以簡單地描述UI的外觀,而Compose則負責其餘的工作-當狀態發生改變時,你的UI將自動更新。由於Compose基於Kotlin構建,因此可以與Java編程語言完全互操作,並且可以直接訪問所有Android和Jetpack API。它與現有的UI工具包也是完全兼容的,因此你可以混合原來的View和現在新的View,並且從一開始就使用Material和動畫進行設計。

Jetpack架構組件實戰到原理手冊— App Startup 篇

Android Jetpack最新更新組件介紹

Hilt 是一個幫助你簡化 依賴注入 操作的 Android 類庫,它讓你可以專注於定義和注入的重要部分, 而無需擔心管理所有的 DI 設置。基於 Dagger 之上,Hilt 繼承了它的編譯期正確性,也提升了運行時性能和可擴展性。Hilt 增加了對 Jetpack 類庫和Android Framework 類的集成。例如,要注入 ViewModel 的參數的話,你可以在 ViewModel 的構造函數上添加@ViewModelInject 註解,並在 Fragment 上添加@AndroidEntryPoint 註解。


Android Jetpack項目實戰(從0搭建Jetpack版的WanAndroid客戶端)

在接觸Android Jetpack組件時, 就深深被其巧妙的設計和強大的功能所吸引,暗自告訴自己一定要學會這些組件,而網上並不能找到系統的學習資料,於是利用每天的時間訪問Google Develper網站,把Jetpac的每個組件從使用到源碼進行了系統的學習和總結。


總結

這份學習筆記手冊主要分爲如下:

學習資料可以免費分享給大家,需要完整版的朋友,【點這裏可以看到全部內容】。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章