本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
1.首先什麼是Fragment?
Fragment
是Android
的檢視生命週期控制器(帶生命週期的自定義View
),是Activity
上的View
層級中的一部分,一般可以把它看做一個輕量級的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
的功能太過強大以至於耦合了View
層和Model
層),它的開銷是很大的,單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
有兩種方式生成,一是硬編碼到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
,注意add
方法的第一個引數,你需要給它指定一個id
,也就是Fragment
容器的id
,通常容器是一個沒有子View
的FrameLayout
,它決定了這個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
,在上面的程式碼中我使用了Java8
的lambda
表示式簡寫了Runnable
.
如果你還想使用Fragment
回退棧記得呼叫addToBackStack
,最後別忘了commit
,這樣才會生效,此時commit
函式返回的是BackStackEntry
的id
當然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
把它設計成交給開發者自行管理了.
這個功能是完全基於Google
的appcompat
包實現的,但是若是我們想要使用這個功能,可能需要較高版本的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
,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
的最前面的.
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'
複製程式碼
(注:新的依賴方式implementation
與compile
功能相同,但是implementation
無法在該模組內引用依賴的依賴,但compile
可以,這麼做的好處是可以加快編譯速度。新的依賴方式api
與compile
完全相同,只是換了名字而已)
在Android Studo3.0
以上中的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
)
最後補充一個小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
改變時(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) {
}
複製程式碼
本來筆者想要用ProcessOn,自己畫一張Fragment
生命週期的流程圖.,最後......真香,因為這圖實在是太複雜了,真要畫它時間上有點過不去,所以我只好拿來主義.
下圖展示了各回撥發生的時間順序(出處在這):
捋一下,覺得上面有圖有點煩的話的話那就看下面總結的文字吧,常用的回撥有這些:
-
onInflate(Context,AttributeSet,Bundle)
只有硬編碼在xml
中的Fragment
(即使用fragment
標籤)才會回撥此方法,這與自定義View
十分類似,在例項化xml
佈局時該方法會被呼叫,先於onAttach
. -
onAttach(Context)
執行該方法時,Fragment
與Activity
已經完成繫結,當一個Fragment
被新增到FragmentManager
時,如果不是在xml
中直接定義fragment
標籤,那麼該方法總是最先被回撥.該方法傳入一個Context
物件,實際上就是該Fragment
依附的Activity
.重寫該方法時記得要呼叫父類的super.onAttach
,父類的onAttach
呼叫返回後,此時呼叫getActivity
將不會返回null
,但是Activity#onCreate
可能還有沒有執行完畢(如果是在xml
中定義,這種情況就會發生,因為此時這個回撥的這個發生的時間也就是你在Activity#onCreate
裡setContentView
的時間,直到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
繫結的Activity
的onCreate
方法已經執行完成並返回,若在此方法之前與Activity
互動互動沒有任何保證,引用了未初始化的資源就會應發空指標異常。 -
onStart()
執行該方法時,Fragment
所在的Activity
由不可見變為可見狀態 -
onResume()
執行該方法時,Fragment
所在的Activity
處於活動狀態,使用者可與之互動. -
onPause()
執行該方法時,Fragment
所在的Activity
處於暫停狀態,但依然可見,使用者不能與之互動,比如Dialog
蓋住了Activity
-
onStop()
執行該方法時,Fragment
所在的Activity
完全不可見 -
onSaveInstanceState(Bundle)
儲存當前Fragment
的狀態。該方法會自動儲存Fragment
的狀態,比如EditText
鍵入的文字,即使Fragment
被回收又重新建立,一樣能恢復EditText
之前鍵入的文字,說實話我不太喜歡這個方法,儲存到Bundle
裡的設計實在是太蠢了,不過好在現在已經有了代替它的方案,Google
的Android 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
公司(對就是那個誕生了Retrofit
和okhttp
的公司)的工程師開發的Fragment
替代方案《View框架flow》,以及相關博文,國內有優秀的簡書作者翻譯了這篇文章《(譯)我為什麼不主張使用Fragment》,原作者在這篇文章中痛斥了Fragment
的各種缺點,我想你可能會喜歡這個.
5.結語
好了關於從Activity
遷移到Fragment
的介紹差不多就到這了,我也是想到什麼就寫什麼,所以文章的結構可能會有些亂(逃......),以後如果還有其他知識點我會慢慢補充上來.
如果你喜歡我的文章記得給我點個贊,拜託了,這對我真的很重要.