上週在《學不動也要學!深刻了解ViewPager2》一篇文章的留言中,你們廣泛對於Fragment懶加載的問題比較關心。其實,對於Fragment懶加載問題的處理由來已久,網上不乏相關的優秀文章。可是,因爲Fragment生命週期的緣由使得懶加載問題的處理並非那麼的優雅。顯然,Google也意識到了問題所在。所以,在Androidx的庫中對於Fragment的生命週期狀態的控制進行了深度優化,使得咱們更容易的去管控Fragment的生命週期,也使得咱們更容易的去處理懶加載問題。可是,前提條件是咱們要了解Google對於Fragment作了哪些優化。那麼就讓咱們藉此機會一塊兒來探究一下吧!( 懶加載稱做延遲加載我以爲更貼切一些,因此下文就統稱爲延遲加載了。 )java
雖然本篇文章是對於Fragment新特性的探究,可是我以爲寫文章總要有個因果。也爲了照顧一下還不太瞭解什麼是延遲加載的同窗。咱們仍是先來了解一下延遲加載,順便回顧一下Fragment延遲加載的舊方案。android
首先,咱們要搞清楚一個問題。「Fragment延遲加載「中的「延遲」並不指的是延遲加載Fragment,而是延遲加載Fragment中的數據。對於Fragment的使用一般咱們會結合ViewPager。在《學不動也要學!深刻了解ViewPager2》一文中咱們也提到ViewPager的預加載問題。ViewPager會默認在當前頁面的左右兩邊至少預加載一個頁面以保證ViewPager的流暢性。咱們假設在ViewPager的全部Fragment中都存在網絡請求。當咱們打開這個頁面的時候因爲ViewPager的預加載緣由,即便在其它Fragment不可見的狀況下也會去進行網絡請求加載數據。而若是此時用戶根本就沒有去滑動ViewPager就退出了應用或者切換到了其餘頁面。那麼對於這個不可見的Fragment中的網絡請求豈不是既浪費了流量也浪費了手機和服務器的性能?git
那麼此時有的同窗就有問題了。你就不能在Fragment顯示的時候去加載數據嗎?問的好!在解答以前咱們先來看下Fragment的生命週期 github
經過上一小節的分析咱們知道想要在Fragment的生命週期中處理延遲加載的問題顯然是走不通的。因此想要處理Fragment的延遲加載就須要另想它法了。還好,在Fragment中爲咱們提供了一個setUserVisibleHint(isVisibleToUser: Boolean)的方法,這個方法中有一個isVisibleToUser的boolean類型的參數,其意義表示當前的Fragment是否對用戶可見。所以,對於Fragment的延遲加載咱們即可以利用這個方法中展開。 既然要使用setUserVisibleHint(isVisibleToUser: Boolean)那麼就應該知道這個方法的調用時機。咱們寫一個ViewPager嵌套Fragment的例子來打印下日誌:緩存
可見該方法是在Fragment的onAttach以前就已經被調用了。所以,對於延遲加載咱們能夠在setUserVisibleHint(isVisibleToUser: Boolean)方法及onViewCreated(view: View, savedInstanceState: Bundle?)添加標誌位來控制是否加載數據。咱們來看下代碼:服務器
abstract class BaseLazyFragment : Fragment() {
/** * 當前Fragment狀態是否可見 */
private var isVisibleToUser: Boolean = false
/** * 是否已建立View */
private var isViewCreated: Boolean = false
/** * 是否第一次加載數據 */
private var isFirstLoad = true
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
this.isVisibleToUser = isVisibleToUser
onLazyLoad()
}
private fun onLazyLoad() {
if (isVisibleToUser && isViewCreated && isFirstLoad) {
isFirstLoad = false
lazyLoad()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isViewCreated = true
onLazyLoad()
}
protected abstract fun lazyLoad()
}
複製代碼
咱們經過在Fragment中添加了三個標誌位實現了延遲加載的功能。咱們到TestFragment嘗試一下:網絡
class TestFragment : BaseLazyFragment() {
private var position: Int = 0
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
val bundle = arguments
position = bundle!!.getInt(KEY_POSITION)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val cardView = CardView(inflater, container)
cardView.bind(Card.fromBundle(arguments!!),position)
return cardView.view
}
companion object {
private const val KEY_POSITION = "position"
fun getInstance(card: Card, position: Int): TestFragment {
val fragment = TestFragment()
val bundle = card.toBundle()
bundle.putInt(KEY_POSITION, position)
fragment.arguments = bundle
return fragment
}
}
override fun lazyLoad() {
showToast("Fragment$position is loading data")
}
private fun showToast(content: String) {
Toast.makeText(context, content, Toast.LENGTH_SHORT).show()
}
}
複製代碼
咱們來看下效果: app
上一節中咱們講到由於ViewPager的預加載機制以及Fragment的生命週期沒法得以控制,咱們不得不經過setUserVisibleHint(isVisibleToUser: Boolean)和onViewCreated(view: View, savedInstanceState: Bundle?)方法以及添加三個標誌位來處理延遲加載,顯然這樣的代碼並不優雅。ide
當咱們將Android項目遷移到Androidx並將androidx版本升級到1.1.0以後發現,咱們第一節中用到的setUserVisibleHint(isVisibleToUser: Boolean)方法已被標記爲廢棄了! post
/** * ... 省略其它註釋 * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} * instead. */
@Deprecated
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded() && mIsCreated) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
if (mSavedFragmentState != null) {
// Ensure that if the user visible hint is set before the Fragment has
// restored its state that we don't lose the new value
mSavedUserVisibleHint = isVisibleToUser;
}
}
複製代碼
而且從註釋中能夠看到使用 FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)方法來替換setUserVisibleHint方法。setMaxLifecycle實在Androidx 1.1.0中新增長的一個方法。setMaxLifecycle從名字上來看意思是設置一個最大的生命週期,由於這個方法是在FragmentTransaction中,所以咱們能夠知道應該是爲Fragment來設置一個最大的生命週期。咱們來看下setMaxLifecycle的源碼:
/** * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is * already above the received state, it will be forced down to the correct state. * * <p>The fragment provided must currently be added to the FragmentManager to have it's * Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise * an {@link IllegalArgumentException} will be thrown.</p> * * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance */
@NonNull
public FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) {
addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state));
return this;
}
複製代碼
這個方法接收一個Fragment參數和一個Lifecycle的狀態參數。Lifecycle是jetpack中很重要的一個庫,它具備對Activity和Fragment生命週期感知能力,相信不少同窗都應該對Lifecycle都略知一二。在Lifecycle的State中定義了五種生命週期狀態,以下:
public enum State {
/** * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch * any more events. For instance, for an {@link android.app.Activity}, this state is reached * <b>right before</b> Activity's {@link android.app.Activity#onDestroy() onDestroy} call. */
DESTROYED,
/** * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is * the state when it is constructed but has not received * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet. */
INITIALIZED,
/** * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached in two cases: * <ul> * <li>after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call; * <li><b>right before</b> {@link android.app.Activity#onStop() onStop} call. * </ul> */
CREATED,
/** * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached in two cases: * <ul> * <li>after {@link android.app.Activity#onStart() onStart} call; * <li><b>right before</b> {@link android.app.Activity#onPause() onPause} call. * </ul> */
STARTED,
/** * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state * is reached after {@link android.app.Activity#onResume() onResume} is called. */
RESUMED;
/** * Compares if this State is greater or equal to the given {@code state}. * * @param state State to compare with * @return true if this State is greater or equal to the given {@code state} */
public boolean isAtLeast(@NonNull State state) {
return compareTo(state) >= 0;
}
}
複製代碼
而在setMaxLifecycle中接收的生命週期狀態要求不能低於CREATED,不然會拋出一個IllegalArgumentException的異常。當傳入參數爲DESTROYED或者INITIALIZED時則會拋出以下圖的異常:
咱們先來看下在不設置setMaxLifecycle的時候添加一個Fragment的狀態,以便和後邊的狀況進行對比。首先咱們在Activity中添加一個Fragment,代碼以下:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.commit()
複製代碼
啓動Activity,咱們將該Fragment生命週期的日誌打印出來以下:
接下來,咱們將maxLifecycle設置爲CREATED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED)
fragmentTransaction.commit()
複製代碼
再來看日誌輸出:
那麼如今問題來了,假設Fragment已經執行到了onResume,此時再爲Fragment設置一個CREATED的最大生命週期會出現什麼樣的狀況呢?咱們經過日誌來驗證一下:
接下來,咱們將maxLifecycle設置爲STARTED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED)
fragmentTransaction.commit()
複製代碼
日誌輸出以下:
一樣,假設Fragment已經執行到了onResume方法再爲其設置最大生命週期爲STARTED會怎樣呢?來看日誌:
最後,咱們將maxLifecycle設置爲RESUMED:
fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.add(R.id.ll_fragment, fragment)
fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)
fragmentTransaction.commit()
複製代碼
而對於已經執行了onResume後的Fragment,再去設置最大生命週期爲RESUMED會怎麼樣呢?由於當前Fragment已是RESUMED狀態了,因此不會再去執行任何代碼。
到這裏咱們能夠得出一個結論:
經過setMaxLifecycle方法能夠精確控制Fragment生命週期的狀態,若是Fragment的生命週期狀態小於被設置的最大生命週期,則當前Fragment的生命週期會執行到被設置的最大生命週期,反之,若是Fragment的生命週期狀態大於被設置的最大生命週期,那麼則會回退到被設置的最大生命週期。
有了這一結論,在ViewPager中即可以對Fragment的生命週期進行控制,以此來更方便的實現延遲加載功能了。
經過上一小節的分析咱們知道了能夠經過setMaxLifecycle來設置Fragment的最大生命週期,從而能夠實現ViewPager中Fragment的延遲加載。固然,關於生命週期狀態處理的操做無需咱們本身實現,在Androidx 1.1.0版本中的FragmentStatePagerAdapter已經幫咱們實現了,只須要在使用時候傳進去相應的參數便可。
FragmentStatePagerAdapter的構造方法接收兩個參數,以下:
/** * Constructor for {@link FragmentStatePagerAdapter}. * * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. * * @param fm fragment manager that will interact with this adapter * @param behavior determines if only current fragments are in a resumed state */
public FragmentStatePagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior) {
mFragmentManager = fm;
mBehavior = behavior;
}
複製代碼
第一個FragmentManager 參數沒必要多說,第二個參數時一個枚舉類型的Behavior參數,其可選值以下:
@Retention(RetentionPolicy.SOURCE)
@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})
private @interface Behavior { }
複製代碼
當behavior爲BEHAVIOR_SET_USER_VISIBLE_HINT時,Fragment改變的時候,setUserVisibleHint方法會被調用,也就是這個參數實際上是爲了兼容之前的老代碼。而且BEHAVIOR_SET_USER_VISIBLE_HINT參數已經被置爲廢棄。因此咱們的可選參數只剩下了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。
當behavior爲BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時意味着只有當前顯示的Fragment會被執行到onResume,而其它Fragment的生命週期都只會執行到onStart.
這一功能時如何實現的呢?咱們追隨BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT的腳步找到了setPrimaryItem方法,這個方法的做用是設置ViewPager當前顯示的Item,其源碼以下:
public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
} else {
mCurrentPrimaryItem.setUserVisibleHint(false);
}
}
fragment.setMenuVisibility(true);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
} else {
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
複製代碼
這段代碼很是簡單易懂,mCurrentPrimaryItem是當前正在顯示的item,fragment是接下來要顯示的item。能夠看到當mBehavior 爲BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時,mCurrentPrimaryItem的最大生命週期被設置爲了STARTED,而fragment的最大生命週期則被設置爲了RESUMED。而當mBehavior爲BEHAVIOR_SET_USER_VISIBLE_HINT時仍然會調用setUserVisibleHint方法,這種狀況就再也不討論,由於BEHAVIOR_SET_USER_VISIBLE_HINT也已經被廢棄掉了。 那麼咱們着重來分析一下BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT時的狀況:
mCurrentPrimaryItem是當前顯示的Fragment,因此該Fragment必然已經執行到了onResume方法,而此時爲其設置了最大生命週期STARTED,那麼mCurrentPrimaryItem必然會執行onPause退回到STARTED狀態。 而fragment當前生命週期狀態爲onStart,當爲其設置了RESUME的最大生命週期狀態後,fragment必然會執行onResume方法進入RESUMED狀態。
知道了這一結論後,咱們再去進行懶加載的控制是否是就異常簡單了?此時咱們只須要一個flag去標誌是不是第一次加載數據就能夠了。所以,懶加載的實現能夠以下:
abstract class TestLifecycleFragment : Fragment() {
private var isFirstLoad = true
override fun onResume() {
super.onResume()
if (isFirstLoad) {
isFirstLoad = false
loadData()
}
}
abstract fun loadData()
}
複製代碼
上篇文章《學不動也要學!深刻了解ViewPager2》中咱們對ViewPager2有了比較深刻的瞭解,其中在講解ViewPager2的offScreenPageLimit時候得出過這樣一個結論:
ViewPager2的offScreenPageLimit默認值爲OFFSCREEN_PAGE_LIMIT_DEFAULT,當setOffscreenPageLimit爲OFFSCREEN_PAGE_LIMIT_DEFAULT時候會使用RecyclerView的緩存機制。默認只會加載當前顯示的Fragment,而不會像ViewPager同樣至少預加載一個item.當切換到下一個item的時候,當前Fragment會執行onPause方法,而下一個Fragment則會從onCreate一直執行到onResume。當再次滑動回第一個頁面的時候當前頁面一樣會執行onPuase,而第一個頁面會執行onResume。
也就是說在ViewPager2中,默認關閉了預加載機制。沒有了預加載機制再談延遲加載其實也沒有任何意義了。因此關於ViewPager2的延遲加載也就不用多說了吧。相信隨着ViewPager2的普及延遲加載的概念也會慢慢淡出開發者的視線。
本篇文章對於Fragment的延遲加載進行了深刻的探究,而且瞭解了在Androidx 1.1.0版本中對Fragment最大生命週期狀態的控制,從而探究出了Fragment延遲加載的新方案。對於ViewPager2,因其默認不會進行預加載所以也就意味着咱們無需處理ViewPager2的延遲加載問題。好了,這一篇花費了我兩個週末(實際上是上週末寫了一半偷了個懶)的文章到此就結束了,若是你從中學有所收穫那麼請你不要吝嗇留下你的贊。
給你們推薦一下BannerViewPager。這是一個基於ViewPager實現的具備強大功能的無限輪播庫。騰訊視頻、QQ音樂、酷狗音樂、支付寶、天貓、淘寶、優酷視頻、喜馬拉雅、網易雲音樂、嗶哩嗶哩等APP的Banner樣式以及指示器樣式均可以經過BannerViewPager實現 。歡迎你們到github關注BannerViewPager!