Android技術棧(一)從Activity遷移到Fragment

本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈

1.首先什麼是Fragment?

FragmentAndroid的視圖生命週期控制器(帶生命週期的自定義View),是Activity上的View層級中的一部分,通常能夠把它看作一個輕量級的Activity.與傳統的Activity相比,它只佔用更少的資源,而且提供更大的編碼靈活性、在超低版本上的兼容性等.java

使用Fragment,即便是在肥腸差勁的平臺(例如API 19如下連ART都沒有的的老系統)上也能獲得較好的運行效果,而且能將過渡動畫兼容到更低的版本(經過FragmentTransition指定)。android

早期的Fragment出現過不少問題,好比沒有onBackPressed(),沒有啓動模式,重複建立,辣雞的回退棧,迷之生命週期等等,致使不少開源做者本身獨立開發了用於Fragment管理的框架,其中比較出名的有YoKeyword大佬的Fragmentation.git

不過事物老是曲折發展的,通過Google多年的調教,如今的Fragment的功能已經很完善了,在不少場合,足以在不少場合替代Activity的存在,上面的一些問題也獲得了比較妥善的解決,若是看完這篇文章,相信你會找到答案。github

巨佬JakeWharton曾經建議:一個App只須要一個Activity.api

這說的就是單ActivityFragment模式.使用這種模式有許多好處:bash

  • 首先第一個好處就是流暢,要知道Activity屬於系統組件,受AMS管理而且自身是一個God Object(上帝對象,Activity的功能太過強大以致於耦合了View層和Model層),它的開銷是很大的,單Activity模式能夠爲咱們節省不少資源,還能夠避免資源不足時,被前臺Activity覆蓋的Activity被殺掉致使頁面數據丟失的狀況(由於只有一個Activity,除非JAVA堆內存到達系統要殺掉一個程序的臨界點,不然系統最不傾向於殺死前臺正在運行的Activity);
  • 其次就是能夠將業務邏輯拆分紅更小的模塊,並將其組合複用,這在這在大型軟件系統中尤其重要(新版知乎就使用了單ActivityFragment這種模式),由於咱們都知道Activity的是沒法在多個頁面中複用的,而此時Fragment就有了它的用武之地,它做爲輕量級的Activity,基本能夠代理Activity的工做,而且他是可複用
  • 再者,使用Fragment能夠爲程序帶來更大的靈活性,咱們都知道在Activity之間傳遞對象,對象須要序列化,這是由於Activity做爲系統組件,是受AMS管理的,而AMS屬於系統進程,不在當前程序運行的進程中,啓動Activity時須要暫時離開當前進程去到AMS的進程中,而AMS則會將你準備好的數據(也就是Intent之類的)用來啓動Activity,這也是FragmentActivity之間的區別之一,Activity屬於系統組件,能夠在別的進程運行(組件化/多進程方案),而Fragment只是框架提供給咱們的的一個組件,它必須依附於Activity生存,而且只能在當前進程使用,但這同時也意味這它能夠得到更大的靈活性,咱們能夠給Fragment傳遞對象而無需序列化,甚至能夠給Fragment傳遞View之類的對象,這都是Activity不容易作到的.

2.要使用Fragment你必須知道的一些事情

首先要提一點,若是你要學習Fragment那麼你至少得是掌握了Activity的,若是你還不瞭解Activity,筆者建議你先去看一些Activity相關的文章,再來進階Fragment.從下面的文章開始,默認讀者已經瞭解了Activity的生命週期等相關知識。微信

Fragment有兩種方式生成,一是硬編碼到xml文件中,二是在Java代碼中new,而後經過FragmentManager#beginTransaction開啓FragmentTransaction提交來添加Fragment(下文會介紹).兩種方式存在着必定區別.硬編碼到xmlFragment沒法被FragmentTransition#remove移除,與Activity同生共死,因此你要是這麼用了,就不用試了,移除不了的,可是在代碼中new出來的是能夠被移除的.app

直接硬編碼到xml中:框架

<fragment
            android:id="@+id/map_view"
            android:name="org.kexie.android.dng.navi.widget.AMapCompatFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
複製代碼

添加Fragment的第二種方式就是使用FragmentManager#beginTransaction(代碼以下)動態添加,你須要先new一個Fragment,而後經過下面Fragment#requireFragmentManager獲取FragmentManager來使用beginTransaction添加Fragment,注意add方法的第一個參數,你須要給它指定一個id,也就是Fragment容器的id,一般容器是一個沒有子ViewFrameLayout,它決定了這個Fragment要在什麼位置顯示.ide

//在xml中編寫放置Fragment位置的容器
        <FrameLayout
            android:id="@+id/fragment_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
            
    //在java代碼中動態添加Fragment
        requireFragmentManager()
                .beginTransaction()
                .add(R.id.fragment_container, fragment)
                .runOnCommit(()->{/*TODO*/})
                .addToBackStack(null)
                .commit();
複製代碼

Fragment中,咱們可使用getId()能夠返回自身的id,一般用這個方法返回它所在的容器的id,供其餘Fragment添加進也添加到當前容器時使用(例如使用Fragment返回棧的場景)。

/**
     * Return the identifier this fragment is known by.  This is either
     * the android:id value supplied in a layout or the container view ID
     * supplied when adding the fragment.
     */
    final public int getId() {
        return mFragmentId;
    }
複製代碼

須要注意的是FragmentTransaction並非當即執行的,而是在當前代碼執行完畢後,回到事件循環(也就是大家知道的Looper)時,纔會執行,不過他會保證在下一幀渲染以前獲得執行,若要在FragmentTransaction執行時搞事情,你須要使用runOnCommit,在上面的代碼中我使用了Java8lambda表達式簡寫了Runnable.

若是你還想使用Fragment回退棧記得調用addToBackStack,最後別忘了commit,這樣纔會生效,此時commit函數返回的是BackStackEntryid

固然FragmentTransaction不止能夠執行add操做,一樣也能夠執行remove,show,hide等操做.

這裏插入一個簡短的題外話做爲上面知識的補充。如何在Android Studio中啓用Java8?在你模塊的build.gradle

android{

    //省略.....
    
    //加上下面的腳本代碼,而後sync你的項目
    
        compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
複製代碼

onBackPressed在哪?我知道第一次使用Fragment的人確定都超想問這個問題.衆所周知Fragment自己是沒有onBackPressed的.不是Google不設計,而是真的無法管理啊!!!,若是一個界面上有三四個地方都有Fragment存在,一按回退鍵,誰知道要交給哪一個Fragment處理呢?因此Fragment自己是沒有onBackPressed的.可是,實際上給Fragment添加相似onBackPressed的功能的辦法是存在的,只是Google把它設計成交給開發者自行管理了.

這個功能是徹底基於Googleappcompat包實現的,可是如果咱們想要使用這個功能,可能須要較高版本的appcompat包,或者你把項目遷移到AndroidX(遷移方式下面會介紹).

咱們可使用FragmentActivity(AppCompatActivity繼承了FragmentActivity)的addOnBackPressedCallback方法爲你的Fragment提供攔截OnBackPressed的功能了.(非AndroidX的其餘版本可能也有實現了這個功能)

public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
            @NonNull OnBackPressedCallback onBackPressedCallback)
複製代碼

OnBackPressedCallback#handleOnBackPressed須要返回一個boolean值。若是你在這個回調裏攔截了onBackPressed應該返回true,說明你本身已經處理了本次返回鍵按下的操做,這樣你的Fragment就不會被彈出返回棧了。

值得注意的是,這個函數的第一個參數,一個LifecycleOwner,ActivityFragment都是LifecycleOwner,用於提供組件的生命週期,這個參數能夠幫咱們自動管理OnBackPressedCallback回調,你無需手動將他從Activity中移除,在LifecycleOwnerON_DESTROY事件來到的時候,他會被自動移除列表,你無需擔憂內存泄漏,框架會幫你完成這些事情。

/**
 * Interface for handling {@link ComponentActivity#onBackPressed()} callbacks without
 * strongly coupling that implementation to a subclass of {@link ComponentActivity}.
 *
 * @see ComponentActivity#addOnBackPressedCallback(LifecycleOwner, OnBackPressedCallback)
 * @see ComponentActivity#removeOnBackPressedCallback(OnBackPressedCallback)
 */
public interface OnBackPressedCallback {
    /**
     * Callback for handling the {@link ComponentActivity#onBackPressed()} event.
     *
     * @return True if you handled the {@link ComponentActivity#onBackPressed()} event. No
     * further {@link OnBackPressedCallback} instances will be called if you return true.
     */
    boolean handleOnBackPressed();
}
複製代碼

咱們能夠看到Activity內管理的OnBackPressedCallback的執行循序與添加時間有關.最後被添加進去的能最早獲得執行.

public void addOnBackPressedCallback(@NonNull LifecycleOwner owner,
        @NonNull OnBackPressedCallback onBackPressedCallback) {
    Lifecycle lifecycle = owner.getLifecycle();
    if (lifecycle.getCurrentState() == Lifecycle.State.DESTROYED) {
        // Already destroyed, nothing to do
        return;
    }
    // Add new callbacks to the front of the list so that
    // the most recently added callbacks get priority
    mOnBackPressedCallbacks.add(0, new LifecycleAwareOnBackPressedCallback(
            lifecycle, onBackPressedCallback));
}
複製代碼

能夠看到它是添加到mOnBackPressedCallbacks這個List的最前面的.

startFragmentForResult方法在哪?對不起和OnBackPressed同樣,Google沒有直接爲咱們實現這個方法,但這並不表明Fragment沒有這個功能,你固然能夠直接用定義getter的方式來獲取Fragment上內容,但這並非最佳實踐,爲了規範編碼咱們最好仍是使用公共的API

Fragment#setTargetFragment能夠給當前Fragment設置一個目標Fragment和一個請求碼

public void setTargetFragment(@Nullable Fragment fragment, int requestCode)
複製代碼

噹噹前Fragment完成相應的任務後,咱們能夠這樣將返回值送回給咱們的目標Fragment經過Intent

getTargetFragment().onActivityResult(getTargetRequestCode(),
        Activity.RESULT_OK,new Intent());
複製代碼

不過要注意,目標Fragment和被請求的Fragment必須在同一個FragmentManager的管理下,不然就會報錯

好了若是你如今使用的appcompat包沒有上面的騷操做.那麼下面我將帶你遷移到AndroidX.

這裏可能有人會問AndroidX是什麼?

簡單來說AndroidX就是一個與平臺解綁的appcompat(低版本兼容高版本功能)庫,也就是說在build.gradle中不須要再與compileSdkVersion寫成同樣,例如以前這樣的寫法:

compile 'com.android.support:appcompat-v7:24.+'
複製代碼

(注:使用24.+則代表使用 24. 開頭的版本的最新版本,若直接使用+號則代表直接使用該庫的最新版本。

如今能夠寫成:

implementation 'androidx.appcompat:appcompat:1.1.0-alpha02'
複製代碼

(注:新的依賴方式implementationcompile功能相同,可是implementation沒法在該模塊內引用依賴的依賴,但compile能夠,這麼作的好處是能夠加快編譯速度。新的依賴方式apicompile徹底相同,只是換了名字而已)

Android Studo3.0以上中的Refactor->Migrate to AndroidX的選點擊以後便可將項目遷移到AndroidX,在確認的時會提示你將項目備份以避免遷移失敗時丟失原有項目,一般狀況下不會遷移失敗,只是遷移的過程會花費不少的時間,若是項目很大,遷移時間會很長,這時即便Android StudioCPU利用率爲0也不要關閉, 可是若是發生遷移失敗,這時候就須要手動遷移了。

一些使用gradle依賴的一些第三方庫中的某些類可能繼承了android.support.v4包下的Fragment,但遷移到AndroidXappcompatFragment變成了androidx.fragment.app包下,原有的代碼下會畫紅線,Android Studio也會警告你出現錯誤,可是不用擔憂,依然能夠正常編譯,Android Studio在編譯的時候會自動完成基類的替換,但前提是你要確保你項目裏的gradle.properties進行了以下設置。

android.useAndroidX=true

android.enableJetifier=true
複製代碼

爲了消除這些難看的紅線,你能夠直接將新的Fragment使用這種方式強制轉換成原有的Fragment

TextureSupportMapFragment mapFragment = TextureSupportMapFragment
.class.cast(getChildFragmentManager()
.findFragmentById(R.id.map_view));
複製代碼

同理,也能夠將舊的Fragment強制類型轉換成新的Fragment.

Fragment f = Fragment.class.cast(mapFragment);
複製代碼

(注:上面的TextureSupportMapFragment是一個典型案例,他是高德地圖SDK中的Fragment,它自己已經繼承了v4包下的Fragment,能夠用過上面的轉換來使他兼容AndroidX

最後補充一個小Tips:當咱們在使用Fragment#getActivity()時返回的是一個可空值,若是沒有判空檢查在Android Studio中將會出現一個噁心的黃色警告,你可使用requireActivity()來代替它,一樣的方法還有requireFragmentManager()等.

3.Fragment生命週期

這多是最讓人懊惱的部分之一了。它彰顯了Fragment中最讓人恐懼的一部分,它的生命週期.

Fragment擁有Activity全部的生命週期回調函數而且因爲自身特色還擴展了一些回調函數,若是不熟悉Fragment,很容易憑直覺形成誤會.例如,一個Fragment並不會由於在Fragment回退棧上有其餘Fragment把它蓋住,又或者是你使用FragmentTransition將它hide而致使他onPause,onPause只跟此Fragment依附的Activity有關,這在Fragment的源碼中寫得清清楚楚.

/**
     * Called when the Fragment is no longer resumed.  This is generally
     * tied to {@link Activity#onPause() Activity.onPause} of the containing
     * Activity's lifecycle. */ @CallSuper public void onPause() { mCalled = true; } 複製代碼

那當咱們想在Fragment不顯示時作一些事情要怎麼辦呢?咱們有onHiddenChanged回調,當Fragment的顯示狀態經過FragmentTransition改變時(hideshow),就會回調這個函數,參數hidden將告訴你這個Fragment如今是被隱藏仍是顯示着.

/**
     * Called when the hidden state (as returned by {@link #isHidden()} of
     * the fragment has changed.  Fragments start out not hidden; this will
     * be called whenever the fragment changes state from that.
     * @param hidden True if the fragment is now hidden, false otherwise.
     */
    public void onHiddenChanged(boolean hidden) {
    }
複製代碼

原本筆者想要用ProcessOn,本身畫一張Fragment生命週期的流程圖.,最後......真香,由於這圖實在是太複雜了,真要畫它時間上有點過不去,因此我只好拿來主義.

下圖展現了各回調發生的時間順序(出處在這):

捋一下,以爲上面有圖有點煩的話的話那就看下面總結的文字吧,經常使用的回調有這些:

  • onInflate(Context,AttributeSet,Bundle)只有硬編碼在xml中的Fragment(即便用fragment標籤)纔會回調此方法,這與自定義View十分相似,在實例化xml佈局時該方法會被調用,先於onAttach.

  • onAttach(Context)執行該方法時,FragmentActivity已經完成綁定,當一個Fragment被添加到FragmentManager時,若是不是在xml中直接定義fragment標籤,那麼該方法老是最早被回調.該方法傳入一個Context對象,實際上就是該Fragment依附的Activity.重寫該方法時記得要調用父類的super.onAttach,父類的onAttach調用返回後,此時調用getActivity將不會返回null,可是Activity#onCreate可能還有沒有執行完畢(若是是在xml中定義,這種狀況就會發生,由於此時這個回調的這個發生的時間也就是你在Activity#onCreatesetContentView的時間,直到Fragment#onViewCreated返回以後,Activity#onCreate纔會繼續執行)。

  • onCreate(Bundle)用來初始化Fragment。它老是在onAttach執行完畢後回調,可經過參數savedInstanceState獲取以前保存的值,記得必定要調用父類的super.onCreate

  • onCreateView(LayoutInflater,ViewGroup,Bundle)須要返回一個View用來初始化Fragment的佈局,它老是在onCreate執行完畢後回調。默認返回null,值得注意的是,若返回null Fragment#onViewCreated將會被跳過,且若是是在xml中定義fragment標籤並用name指定某個Fragment,則這個方法不容許返回null,不然就會報錯。當使用ViewPager+Fragment時此方法可能會被屢次調用(與Fragment#onDestroyView成對調用)。

  • onActivityCreated(Bundle)執行該方法時,與Fragment綁定的ActivityonCreate方法已經執行完成並返回,若在此方法以前與Activity交互交互沒有任何保證,引用了未初始化的資源就會應發空指針異常。

  • onStart()執行該方法時,Fragment所在的Activity由不可見變爲可見狀態

  • onResume()執行該方法時,Fragment所在的Activity處於活動狀態,用戶可與之交互.

  • onPause()執行該方法時,Fragment所在的Activity處於暫停狀態,但依然可見,用戶不能與之交互,好比Dialog蓋住了Activity

  • onStop()執行該方法時,Fragment所在的Activity徹底不可見

  • onSaveInstanceState(Bundle)保存當前Fragment的狀態。該方法會自動保存Fragment的狀態,好比EditText鍵入的文本,即便Fragment被回收又從新建立,同樣能恢復EditText以前鍵入的文本,說實話我不太喜歡這個方法,保存到Bundle裏的設計實在是太蠢了,不過好在如今已經有了代替它的方案,GoogleAndroid Jetpack MVVM框架,以後我也會專門出一篇文章來介紹。

  • onDestroyView()銷燬與Fragment有關的視圖,但未與Activity解除綁定,通常在這個回調裏解除Fragment對視圖的引用。一般在ViewPager+Fragment的方式下會使用並重寫此方法,而且與Fragment#onCreateView同樣多是屢次的。

  • onDestroy()銷燬Fragment。一般按Back鍵退出或者Fragment被移除FragmentManager時調用此方法,此時應該清理Fragment中所管理的全部數據,它會在onDetach以前回調。

  • onDetach()解除與Activity的綁定。在onDestroy方法以後調用。Fragment生命週期的最末期,若在super.onDetach返回後getActivity(),你將會獲得一個null

4.Fragment的替代方案

看了那麼多有關Fragment的介紹,若是你還對Fragment嗤之以鼻,又想減少業務的邏輯的粒度,那麼我只能給你Fragment的替代方案了。

一位square公司(對就是那個誕生了Retrofitokhttp的公司)的工程師開發的Fragment替代方案《View框架flow》,以及相關博文,國內有優秀的簡書做者翻譯了這篇文章《(譯)我爲何不主張使用Fragment》,原做者在這篇文章中痛斥了Fragment的各類缺點,我想你可能會喜歡這個.

5.結語

好了關於從Activity遷移到Fragment的介紹差很少就到這了,我也是想到什麼就寫什麼,因此文章的結構可能會有些亂(逃......),之後若是還有其餘知識點我會慢慢補充上來.

若是你喜歡個人文章記得給我點個贊,拜託了,這對我真的很重要.

相關文章
相關標籤/搜索