Fragment可見性及懶加載終極解決方案

Fragment 有很多種使用方法,官方并沒有提供一個統(tǒng)一的 api 來處理 Fragment 的可見性判斷和回調(diào)重慢,導(dǎo)致在不同的使用場景下需要使用不同的方法來判斷 Fragment 的可見性畸写。網(wǎng)上已經(jīng)有很多講 Fragment 可見性的文章藏雏,但是大部分文章覆蓋的使用場景不夠全面涯塔,有些文章的用法也過時了鹤啡,因此本人梳理了當(dāng)前 Fragment 的各種使用場景惯驼,提供了一個統(tǒng)一的 api 來處理 Fragment 的可見性。

一般使用場景

在Activity中直接使用

在 xml 文件中聲明 Fragment递瑰,或者在代碼中通過 FragmentTransaction 的 add 或 replace 動態(tài)載入 Fragment祟牲。這兩種情況下都只要監(jiān)聽 Fragment 的 onResume 和 onPause 方法就能判斷 Fragment 的可見性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

使用show和hide控制顯示和隱藏

Google 在 androidx.fragment 1.2.0 中新增了一個 FragmentContainerView抖部,用來替代 FlameLayout 做為 Fragment 的容器说贝,在下文中將使用 FragmentContainerView 作為 Fragment 的容器。

老的用法

通過 FragmentTransaction 的 add 將 Fragment 添加到 FragmentManager 后慎颗,F(xiàn)ragment 的生命周期會跟隨綁定的 Activity 或父 Fragment 走到 onResume乡恕,這個時候,只要所依附的 Activity 或父 Fragment 的生命周期不發(fā)生變化俯萎,通過 FragmentTransaction 的 show 和 hide 方法控制 Fragment 的顯示和隱藏并不會改變 Fragment 的生命周期几颜,這個時候需要監(jiān)聽 onHiddenChanged 判斷 Fragment 的可見性。

一般情況下讯屈,將 Fragment add 到 FragmentManager 的過程是在 Activity 中的 onCreate 回調(diào)中進行的蛋哭,第一次回調(diào) onHiddenChanged 是在 Fragment 回調(diào) onCreateView 之前。如果需要在 Fragment 第一次可見的時候進行 UI 操作涮母,就會出錯谆趾,為了避免出錯,需要結(jié)合 Fragment 的 onResume 和 onPause 判斷 Fragment 的可見性叛本。

override fun onHiddenChanged(hidden: Boolean) {
    super.onHiddenChanged(hidden)

    if (hidden) {
        determineFragmentInvisible()
    } else {
        determineFragmentVisible()
    }
}

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

AndroidX用法

調(diào)用了 hide 后沪蓬,接著調(diào)用 setMaxLifecycle(fragment, Lifecycle.State.STARTED),F(xiàn)ragment 生命周期會走到 onPause来候。調(diào)用 show 方法后跷叉,接著調(diào)用 setMaxLifecycle(fragment, Lifecycle.State.RESUMED),F(xiàn)ragment 生命周期會走到 onPause营搅。這樣只要監(jiān)聽 Fragment 的 onResume 和 onPause 方法就能判斷 Fragment 的可見性云挟。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

在ViewPager中使用

老的用法

在 support 和 androidx.fragment 1.0.0,通過監(jiān)聽 setUserVisibleHint 判斷Fragment 的可見性转质。如果將 Fragment add 到 FragmentManager 的過程是在 Activity 中的 onCreate 回調(diào)中進行的园欣,第一次回調(diào) setUserVisibleHint 也是在 Fragment 回調(diào) onCreateView 之前,也需要結(jié)合 Fragment 的 onResume 和 onPause 判斷 Fragment 的可見性休蟹。

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)
    
    if (isVisibleToUser) {
        determineFragmentVisible()
    } else {
        determineFragmentInvisible()
    }
}

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

AndroidX用法

谷歌從 androidx.fragment 1.1.0 中開始沸枯,對 FragmentPagerAdapter 和 FragmentStatePagerAdapter 進行了調(diào)整日矫,支持使用 setMaxLifecycle 控制 Fragment 的生命周期,只需要創(chuàng)建 Adpter 的時候绑榴, Behavior 選擇 BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT哪轿。

public static final int BEHAVIOR_SET_USER_VISIBLE_HINT = 0;
public static final int BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT = 1;

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
    Fragment fragment = (Fragment)object;
    if (fragment != mCurrentPrimaryItem) {
        if (mCurrentPrimaryItem != null) {
            ...
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                ...
                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);
            } else {
                mCurrentPrimaryItem.setUserVisibleHint(false);
            }
        }
        ...
        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            ...
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);
        } else {
            fragment.setUserVisibleHint(true);
        }
        ...
    }
}

這樣只要監(jiān)聽 Fragment 的 onResume 和 onPause 方法就能判斷 Fragment 的可見性。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

在ViewPager2中使用

在 ViewPager2 中使用 Fragment 時翔怎,使用的適配器是 FragmentStateAdapter缔逛,F(xiàn)ragmentStateAdapter 內(nèi)部使用 FragmentMaxLifecycleEnforcer ,F(xiàn)ragmentMaxLifecycleEnforcer 也是通過 setMaxLifecycle 控制 Fragment 的生命周期

class FragmentStateAdapter {

    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        ...
        mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
        ...
    }

    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        ...
        mFragmentMaxLifecycleEnforcer = null;
    }

    class FragmentMaxLifecycleEnforcer {

        void updateFragmentMaxLifecycle(boolean dataSetChanged) {
            ...
            for (int ix = 0; ix < mFragments.size(); ix++) {
                ...
                if (itemId != mPrimaryItemId) {
                    transaction.setMaxLifecycle(fragment, STARTED);
                } else {
                    toResume = fragment; // itemId map key, so only one can match the predicate
                }
                ...
            }
            if (toResume != null) { // in case the Fragment wasn't added yet
                transaction.setMaxLifecycle(toResume, RESUMED);
            }
            ...
        }
    }
}

這樣只要監(jiān)聽 Fragment 的 onResume 和 onPause 方法就能判斷 Fragment 的可見性姓惑。

override fun onResume() {
    super.onResume()
 
    determineFragmentVisible()
}

override fun onPause() {
    super.onPause()
    
    determineFragmentInvisible()
}

具體實現(xiàn)

IFragmentVisibility 中定義 Fragment 可見性相關(guān)方法:

interface IFragmentVisibility {

    /**
     * Fragment可見時調(diào)用。
     */
    fun onVisible() {}

    /**
     * Fragment不可見時調(diào)用按脚。
     */
    fun onInvisible() {}

    /**
     * Fragment第一次可見時調(diào)用于毙。
     */
    fun onVisibleFirst() {}

    /**
     * Fragment可見時(第一次除外)調(diào)用。
     */
    fun onVisibleExceptFirst() {}

    /**
     * Fragment當(dāng)前是否對用戶可見
     */
    fun isVisibleToUser(): Boolean
}

Fragment可見

Fragment 可見受到幾個因素影響:Fragment 是否處于 RESUMED 狀態(tài)辅搬、Fragment 是否顯示唯沮、Fragment Hint 是否對用戶可見,判斷Fragment可見性可能會被連續(xù)調(diào)用多次堪遂,如果當(dāng)前已經(jīng)對用戶可見介蛉,則不進行判斷可見性。

// Fragment當(dāng)前是否對用戶可見溶褪。
private var mIsFragmentVisible = false

// Fragment當(dāng)前是否是第一次對用戶可見币旧。
private var mIsFragmentVisibleFirst = true

private fun determineFragmentVisible() {
    if (isResumed && !isHidden && userVisibleHint && !mIsFragmentVisible) {
        mIsFragmentVisible = true
        onVisible()
        if (mIsFragmentVisibleFirst) {
            mIsFragmentVisibleFirst = false
            onVisibleFirst()
        } else {
            onVisibleExceptFirst()
        }
    }
}

Fragment不可見

當(dāng) Fragment 處于可見狀態(tài),調(diào)用一次 determineFragmentInvisible 方法猿妈,F(xiàn)ragment 就變成不可見了吹菱。

private fun determineFragmentInvisible() {
    if (mIsFragmentVisible) {
        mIsFragmentVisible = false
        onInvisible()
    }
}

Fragment嵌套

老的用法

從日志中可以看到,F(xiàn)ragment-1 和 Fragment-1-1 處于可見狀態(tài)彭则,但是奇怪的是 Fragment-2-1 也處于可見狀態(tài)鳍刷,這不符合邏輯,判斷可見性邏輯還有待優(yōu)化的地方俯抖。

分析日志可知输瓜,所有的 Fragment 生命周期都走到了onResume,但是 Fragment-2芬萍、Fragment-1-2尤揣、Fragment-2-2 因為 isHidden = true,判斷出是不可見狀態(tài)柬祠。Fragment-2-1 是 isHidden = false芹缔,但是 Fragment-2 是 isHidden = true,從邏輯上父 Fragment 不可見瓶盛,子 Fragment 也應(yīng)該不可見最欠。所以在判斷 Fragment 是否可見的時候示罗,還要考慮父 Fragment 是否可見(如果存在父 Fragment)。

當(dāng)從 Fragment-1 切換到 Fragment-2 后芝硬,可以看到蚜点,F(xiàn)ragment-1 不可見,F(xiàn)ragment-2 可見拌阴,但是本應(yīng)該不可見的 Fragment-1-1 還是可見绍绘,本應(yīng)該可見的 Fragment-2-1 還是不可見,說明判斷可見性邏輯還有待優(yōu)化的地方迟赃。

從 Fragment-1 切換到 Fragment-2陪拘,這兩者的 onHiddenChanged 被調(diào)用了,所以它們的可見性發(fā)生了變化纤壁。Fragment-1-1 和 Fragment-2-1 沒有任何操作左刽,但是它們的可見性也應(yīng)該隨著父Fragment 可見性發(fā)生變化而變化,所以應(yīng)該在父 Fragment 可見性變化的時候重新判斷一次子 Fragment 的可見性酌媒。

AndroidX用法

全部使用 setMaxLifecycle 控制 Fragment 生命周期欠痴,可以看到 Fragment 的可見性判斷是正確的。

從 Fragment-1 切換到 Fragment-2秒咨,可見性判斷還是正確的喇辽。

子 Fragment 的生命周期會根據(jù)所綁定的 Activity 或父 Fragment 的生命周期變化而變化,setMaxLifecycle 改變了父 Fragment 的生命周期雨席,子 Fragment 的生命周期自然就跟著變化了菩咨。所以,僅監(jiān)聽 Fragment 的 onResume 和 onPause 就能判斷 Fragment 的可見性陡厘,不需要調(diào)整判斷邏輯旦委。

具體實現(xiàn)

在 determineFragmentVisible 中增加判斷父 Fragment 是否可見的代碼:

private fun determineFragmentVisible() {
    val parent = parentFragment
    if (parent != null && parent is VisibilityFragment) {
        if (!parent.isVisibleToUser()) {
            // 父Fragment不可見,子Fragment也一定不可見
            return
        }
    }
    ...
}

在 determineFragmentVisible 和 determineFragmentInvisible 增加判斷子 Fragment 的可見性代碼:

private fun determineFragmentVisible() {
    ...
    if (isResumed && !isHidden && userVisibleHint && !mIsFragmentVisible) {
        ...
        determineChildFragmentVisible()
    }
}

private fun determineFragmentInvisible() {
    if (mIsFragmentVisible) {
        ...
        determineChildFragmentInvisible()
    }
}

private fun determineChildFragmentVisible() {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentVisible()
        }
    }
}

private fun determineChildFragmentInvisible() {
    childFragmentManager.fragments.forEach {
        if (it is VisibilityFragment) {
            it.determineFragmentInvisible()
        }
    }
}

懶加載

在實現(xiàn)了上述功能后雏亚,對于需要懶加載功能的 Fragment缨硝,只需要重寫 onVisibleFirst,在里面加載數(shù)據(jù)就可以了罢低。

總結(jié)

對于全部使用 setMaxLifecycle 控制 Fragment 生命周期的代碼查辩,F(xiàn)ragment 的可見性判斷相對比較簡單,只要監(jiān)聽 Fragment 的 onResume 和 onPause 方法就能判斷 Fragment 的可見性网持。

對于老的用法或者老的用法和 setMaxLifecycle 混用的代碼宜岛,F(xiàn)ragment 可見性判斷不僅要考慮使用方式,也要考慮父 Fragment 的可見性功舀,同時自身可見性改變的時候萍倡,也要主動調(diào)用子 Fragment 判斷可見性的代碼。

項目地址

fragment-visibility辟汰,覺得用起來很爽的列敲,請不要吝嗇你的 Star 阱佛!

參考

如何判斷Fragment是否對用戶可見

Fragment新功能,setMaxLifecycle了解一下

Androidx 下 Fragment 懶加載的新實現(xiàn)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末戴而,一起剝皮案震驚了整個濱河市凑术,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌所意,老刑警劉巖淮逊,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扶踊,居然都是意外死亡泄鹏,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門秧耗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來备籽,“玉大人,你說我怎么就攤上這事绣版。” “怎么了歼疮?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵杂抽,是天一觀的道長。 經(jīng)常有香客問我韩脏,道長缩麸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任赡矢,我火速辦了婚禮杭朱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吹散。我一直安慰自己弧械,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布空民。 她就那樣靜靜地躺著刃唐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪界轩。 梳的紋絲不亂的頭發(fā)上画饥,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機與錄音浊猾,去河邊找鬼抖甘。 笑死,一個胖子當(dāng)著我的面吹牛葫慎,可吹牛的內(nèi)容都是我干的衔彻。 我是一名探鬼主播薇宠,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼米奸!你這毒婦竟也來了昼接?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤悴晰,失蹤者是張志新(化名)和其女友劉穎慢睡,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铡溪,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡漂辐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了棕硫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片髓涯。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哈扮,靈堂內(nèi)的尸體忽然破棺而出纬纪,到底是詐尸還是另有隱情,我是刑警寧澤滑肉,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布包各,位于F島的核電站,受9級特大地震影響靶庙,放射性物質(zhì)發(fā)生泄漏问畅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一六荒、第九天 我趴在偏房一處隱蔽的房頂上張望护姆。 院中可真熱鬧,春花似錦掏击、人聲如沸卵皂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽渐裂。三九已至,卻和暖如春钠惩,著一層夾襖步出監(jiān)牢的瞬間柒凉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工篓跛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留膝捞,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像蔬咬,于是被迫代替她去往敵國和親鲤遥。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容