Android技術棧(一)從Activity遷移到Fragment
Fragment
是 Android
的檢視生命週期控制器,可以把它看做一個輕量級的 Activity
,與傳統的 Activity
相比,它只佔用更少的資源,並且提供更大的編碼 靈活性
、在超低版本上的 相容性
等.
使用 Fragment
,即使是在肥腸差勁的平臺(例如API 19以下連 ART
都沒有的的老系統)上也能得到較好的執行效果,並且能將過渡動畫相容到更低的版本(通過 FragmentTransition
指定)。
早期的 Fragment
出現過很多問題,比如沒有 onBackPressed()
,沒有啟動模式,重複建立,辣雞的回退棧,迷之生命週期等等,導致很多開源作者自己獨立開發了用於 Fragment
管理的框架,其中比較出名的有 YoKeyword大佬的Fragmentation .
不過事物總是曲折發展的,經過 Google
多年的調教,現在的 Fragment
的功能已經很完善了,在很多場合,足以在很多場合替代 Activity
的存在,上面的一些問題也得到了比較妥善的解決,如果看完這篇文章,相信你會找到答案。

巨佬 JakeWharton
曾經建議:一個 App
只需要一個 Activity
.
這說的就是單 Activity
多 Fragment
模式.使用這種模式有許多好處:
- 首先第一個好處就是流暢,要知道
Activity
屬於系統元件,受AMS
管理並且自身是一個God Object
,它的開銷是很大的,單Activity
模式可以為我們節省很多資源,還可以避免資源不足時,被前臺Activity
覆蓋的Activity
被殺掉導致頁面資料丟失的情況(因為只有一個Activity
,除非JAVA
堆記憶體到達系統要殺掉一個程式的臨界點,否則系統最不傾向於殺死前臺正在執行的Activity
); - 其次就是可以將業務邏輯拆分成更小的模組,並將其組合複用,這在這在大型軟體系統中尤為重要(新版
知乎
就使用了單Activity
多Fragment
這種模式),因為我們都知道Activity
的是無法在多個頁面中複用的,而此時Fragment
就有了它的勇武之地,它作為輕量級的Activity
,基本可以代理Activity
的工作,並且他是可複用 - 再者,使用
Fragment
可以為程式帶來更大的靈活性,我們都知道在Activity
之間傳遞物件,物件需要序列化,這是因為Activity
作為系統元件,是受AMS
管理的,而AMS
屬於系統程序,不在當前程式執行的程序中,啟動Activity
時需要暫時離開當前程序去到AMS
的程序中,而AMS
則會將你準備好的資料(也就是Intent
之類的)用來啟動Activity
,這也是Fragment
和Activity
之間的區別之一,Activity
屬於系統元件,可以在別的程序執行(元件化/多程序方案),而Fragment
只是框架提供給我們的的一個元件,它必須依附於Activity
生存,並且只能在當前程序使用,但這同時也意味這它可以獲得更大的靈活性,我們可以給Fragment
傳遞物件而無需序列化,甚至可以給Fragment
傳遞View
之類的物件,這都是Activity
不容易做到的.
2.要使用Fragment你必須知道的一些事情
首先要提一點,如果你要學習 Fragment
那麼你至少得是掌握了 Activity
的,如果你還不瞭解 Activity
,筆者建議你先去看一些 Activity
相關的文章,再來進階 Fragment
.從下面的文章開始,預設讀者已經瞭解了 Activity
的生命週期等相關知識。
Fragment
擁有 Activity
所有的生命週期回撥函式並且由於自身特點還擴充套件了一些回撥函式,但是這些與 Activity
相關的回撥函式幾乎只與 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
改變時( hide
和 show
),就會回撥這個函式,引數 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) { } 複製程式碼
Fragment
有兩種方式生成,一是硬編碼到 xml
檔案中,二是在 Java
程式碼中 new
,然後通過 FragmentManager#beginTransaction
開啟 FragmentTransaction
提交來新增 Fragment
(下文會介紹).兩種方式存在著一定區別.硬編碼到 xml
的 Fragment
無法被 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
.
需要注意的是 FragmentTransaction
並不是立即執行的,而是在當前程式碼執行完畢後,回到事件迴圈(也就是你們知道的 Looper
)時,才會執行,不過他會保證在下一幀渲染之前得到執行(通過 Handler#createAsync
機制),若要在 FragmentTransaction
搞事情,你需要使用 runOnCommit
,下面的程式碼中我使用了 Java8
的 lambda
表示式簡寫了 Runnable
.
若要使用 Fragment
回退棧記得 addToBackStack
,最後別忘了 commit
,這樣才會生效,此時 commit
函式返回的是 BackStackEntry
的 id
requireFragmentManager() .beginTransaction() .add(getId(), fragment) .runOnCommit(()->{/*TODO*/}) .addToBackStack(null) .commit(); 複製程式碼
當然 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
把它設計成交給開發者自行管理了.
要想使用 Fragment
的 onBackPressed
,你可能需要先升級到 AndroidX
.
這裡可能有人會問 AndroidX
是什麼?
簡單來講 AndroidX
就是一個與平臺解綁的 appcompat
(低版本相容高版本功能)庫,也就是說在 build.gradle
中不需要再與 compileSdkVersion
寫成一樣,例如之前這樣的寫法:
compile 'com.android.support:appcompat-v7:24.+' 複製程式碼
(注:使用24.+則表明使用 24. 開頭的版本的最新版本,若直接使用+號則表明直接使用該庫的最新版本。
現在可以寫成:
implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' 複製程式碼
(注:新的依賴方式 implementation
與 compile
功能相同,但是 implementation
無法在該模組內引用依賴的依賴,但 compile
可以,這麼做的好處是可以加快編譯速度。新的依賴方式 api
與 compile
完全相同,只是換了名字而已)
在 Android Studo
中的 Refactor->Migrate to AndroidX
的選點選之後即可將專案遷移到 AndroidX
,在確認的時會提示你將專案備份以免遷移失敗時丟失原有專案,通常情況下不會遷移失敗,只是遷移的過程會花費很多的時間,如果專案很大,遷移時間會很長,這時即使 Android Studio
的 CPU
利用率為 0
也不要關閉, 但是如果發生遷移失敗,這時候就需要手動遷移了。
一些使用 gradle
依賴的一些第三方庫中的某些類可能繼承了 android.support.v4
包下的 Fragment
,但遷移到 AndroidX
後 appcompat
的 Fragment
變成了 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
)
差點扯遠了,搞定 AndroidX
後,我們就可以使用 FragmentActivity
的 addOnBackPressedCallback
方法為你的 Fragment
提供攔截 OnBackPressed
的功能了.
public void addOnBackPressedCallback(@NonNull LifecycleOwner owner, @NonNull OnBackPressedCallback onBackPressedCallback) 複製程式碼
OnBackPressedCallback#handleOnBackPressed
需要返回一個 boolean
值。如果你在這個回撥裡攔截了 onBackPressed
應該返回 true
,說明你自己已經處理了本次返回鍵按下的操作,這樣你的 Fragment
就不會被彈出返回棧了。
值得注意的是,這個函式的第一個引數,一個 LifecycleOwner
, Activity
和 Fragment
都是 LifecycleOwner
,用於提供元件的生命週期,這個引數可以幫我們自動管理 OnBackPressedCallback
回撥,你無需手動將他從 Activity
中移除,在 LifecycleOwner
的 ON_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
的最前面的.
Fragment
是必須要有 id
的,使用 getId
可以返回自身的 id
,通常用這個方法返回它所在的容器的 id
,供其他 Fragment
新增進 FragmentManager
時使用。(比如說你使用了一個 FrameLayout
作為 Fragment
的容器,那麼它就會返回那個 FrameLayout
的 id
)
/** * 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; } 複製程式碼
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
的管理下,否則就會報錯
最後,當我們在使用 Fragment#getActivity()
時返回的是一個可空值,如果沒有判空檢查在 Android Studio
中將會出現一個噁心的黃色警告,你可以使用 requireActivity()
來代替它,同樣的方法還有 requireFragmentManager()
等.
3.Fragment生命週期
這可能是最讓人懊惱的部分之一了。下面是絕對的高能區,因為接下來的那一張圖,彰顯了 Fragment
中最讓人恐懼的一部分,它的生命週期.
本來筆者想要用ProcessOn,自己畫一張 Fragment
生命週期的流程圖.怎麼說我都是 軟體工程
專業的啊,最後......真香,因為這圖實在是太複雜了,真要畫它時間上有點過不去,所以我只好拿來主義.
下圖展示了各回調發生的時間順序:

捋一下,常用的回撥有這些,覺得上面有圖有點煩的話的話那就看下面總結的文字吧:
-
onInflate(Context,AttributeSet,Bundle)
只有硬編碼在xml
中的Fragment
(即使用fragment
標籤)才會呼叫該方法,與自定義View
十分類似,在例項化xml
佈局時該方法會被呼叫 -
onAttach(Context)
執行該方法時,Fragment
與Activity
已經完成繫結,該方法傳入一個Context
物件,實際上就是該Fragment
依附的Activity
,此時呼叫getActivity
將不會返回null
,但是Activity#onCreate
可能還有沒有執行。 -
onCreate(Bundle)
用來初始化Fragment
。可通過引數savedInstanceState
獲取之前儲存的值。 -
onCreateView(LayoutInflater,ViewGroup,Bundle)
需要返回一個View
用來初始化Fragment
的佈局。預設返回null
,值得注意的是,若返回null
Fragment#onViewCreated
將不會執行。使用ViewPager
+Fragment
時此方法可能會被多次呼叫。 -
onActivityCreated(Bundle)
執行該方法時,與Fragment
繫結的Activity
的onCreate
方法已經執行完成並返回,若在此之前與Activity
互動,若引用了未初始化的資源會應發空指標異常。 -
onStart()
執行該方法時,Fragment
所在的Activity
由不可見變為可見狀態 -
onResume()
執行該方法時,Fragment
所在的Activity
處於活動狀態,使用者可與之互動 -
onPause()
執行該方法時,Fragment
所在的Activity
處於暫停狀態,但依然可見,使用者不能與之互動 -
onStop()
執行該方法時,Fragment
所在的Activity
完全不可見 -
onSaveInstanceState(Bundle)
儲存當前Fragment
的狀態。該方法會自動儲存Fragment
的狀態,比如EditText
鍵入的文字,即使Fragment
被回收又重新建立,一樣能恢復EditText
之前鍵入的文字。 -
onDestroyView()
銷燬與Fragment
有關的檢視,但未與Activity
解除繫結,一般在這個回撥裡解除Fragment
對檢視的引用。通常在ViewPager
+Fragment
的方式下會使用並重寫此方法,並且與Fragment#onCreateView
一樣可能是多次的。 -
onDestroy()
銷燬Fragment
。通常按Back
鍵退出或者Fragment
被移除FragmentManager
時呼叫此方法,此時應該清理Fragment
中所管理的所有資料。 -
onDetach()
解除與Activity
的繫結。在onDestroy
方法之後呼叫。若在此時getActivity()
,你將會得到一個null
。
4.Fragment的替代方案
看了那麼多有關 Fragment
的介紹,如果你還對 Fragment
嗤之以鼻,又想減小業務的邏輯的粒度,那麼我只能給你 Fragment
的替代方案了。
一位 square
公司(對就是那個誕生了 Retrofit
和 okhttp
的公司)的工程師開發的 Fragment
替代方案 《View框架flow》 ,以及相關博文,國內有優秀的簡書作者翻譯了這篇文章 《(譯)我為什麼不主張使用Fragment》 ,原作者在這篇文章中痛斥了 Fragment
的各種缺點,我想你可能會喜歡這個.
5.結語
好了關於從 Activity
遷移到 Fragment
的介紹差不多就到這了,我也是想到什麼就寫什麼,所以文章的結構可能會有些亂(逃......),以後如果還有其他知識點我會慢慢補充上來.
如果你喜歡我的文章記得給我點個贊,這是對我最大的鼓勵.