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

_晨曦_發表於2019-03-20

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

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

1.首先什麼是Fragment?

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

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

早期的Fragment出現過很多問題,比如沒有onBackPressed(),沒有啟動模式,重複建立,辣雞的回退棧,迷之生命週期等等,導致很多開源作者自己獨立開發了用於Fragment管理的框架,其中比較出名的有YoKeyword大佬的Fragmentation.

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

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

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

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

  • 首先第一個好處就是流暢,要知道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出來的是可以被移除的.

直接硬編碼到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要在什麼位置顯示.

    //在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)時,才會執行,不過他會保證在下一幀渲染之前得到執行(通過Handler#createAsync機制),若要在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生命週期的流程圖.,最後......真香,因為這圖實在是太複雜了,真要畫它時間上有點過不去,所以我只好拿來主義.

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

Android技術棧(一)從Activity遷移到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的介紹差不多就到這了,我也是想到什麼就寫什麼,所以文章的結構可能會有些亂(逃......),以後如果還有其他知識點我會慢慢補充上來.

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

相關文章