fragment 知识点全方位覆盖

时间:2021-7-21 作者:qvyue

1. 基础使用

1.1 静态使用

标签,,name属性:指定package.name:表示加载对应的fragment

1.2 动态使用

fragment 知识点全方位覆盖
fragment-base.png

1.3 v4包下fragment的使用

v4包向下兼容,兼容android3.0以前的fragment

fragment 知识点全方位覆盖
fragment-v4.png

1.4 生命周期

fragment 知识点全方位覆盖
fragment-lifecycle.png

总结

  • 创建阶段:从下往上,即:activity声明周期在前
  • 切换阶段:被动先行,即:fragment间切换时候,要隐藏的先行,要显示的后行。。也可以形象的理解:坑位就一个,只有先离开,其他才能进来
  • 前后台/销毁:从上往下,即:activity在下,fragment在上,activity相当于一个容易,fragment在视图层上层,先从上层销毁。也可以理解:容器先死了,孩子谁处理

2. fragment的通信

参考ios的思想:
正向传值:用属性传值,也就是用target的暴露方法
反向传值:用代理或block或通知

2.1 activity 向 fragment传值

  • fragment未创建时候
ac: fg.setArguments(bundle)
fg: getArguments() 
  • fragment已经存在
方法一:
fragment暴露public方法给ac调用

方法二:被动方案,即fragment主动从ac拿数据
1:接口方案。fg持有接口,调用接口方法,ac实现接口方法,return 数据。
2:ac暴露方法

方法三:
无敌方案:EventBus

2.2 fragment 向 activity传值

方法一:
ac暴露方法给fg调用

方法二:
接口方案:同ac向fg传值。。反向传值,更适合

方法三:
无敌方案:EventBus。。反向传值,更适合

2.3 fragment间通信

方法一:暴露方法

方法二:接口

方法三:EventBus

3. 进阶

3.1 fg 页面跳转

fg中startActivityForResult,onActivityResult能接收回调

3.2 fg 嵌套

正确选择是使用getFragmentManager()还是getChildFragmentManager()

对于宿主Activity,getSupportFragmentManager()获取的FragmentActivity的FragmentManager对象;

对于Fragment,getFragmentManager()是获取的是父Fragment(如果没有,则是FragmentActivity)的FragmentManager对象,而getChildFragmentManager()是获取自己的FragmentManager对象

3.3 fg 恢复及重叠问题

善用findFragmentByTag获取到恢复的fg,然后结合hide和show控制状态

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity);

    TargetFragment targetFragment;
    HideFragment hideFragment;
  
    if (savedInstanceState != null) {  // “内存重启”时调用
    //注意:其实ac销毁并恢复时,fg是被保存到savedInstanceState的,所以,如果不为空,就不用重新建fg,onSaveInstanceState已经保存了fg
    
        targetFragment = getSupportFragmentManager().findFragmentByTag(TargetFragment.class.getName);
        hideFragment = getSupportFragmentManager().findFragmentByTag(HideFragment.class.getName);
        // 解决重叠问题
        getFragmentManager().beginTransaction()
                .show(targetFragment)
                .hide(hideFragment)
                .commit();
    }else{  // 正常时
        targetFragment = TargetFragment.newInstance();
        hideFragment = HideFragment.newInstance();

        getFragmentManager().beginTransaction()
                .add(R.id.container, targetFragment, targetFragment.getClass().getName())
                .add(R.id,container,hideFragment,hideFragment.getClass().getName())
                .hide(hideFragment)
                .commit();
    }
}

3.4 Fragment 回收和恢复问题

场景或原因:内存吃紧被回收,或屏幕旋转导致的重建

fg是有宿主的,回收和恢复跟ac也是有关联的。回收时候,onSaveInstanceState保存了fg及一些其他状态。恢复时候,需要判断savedInstanceState是否为空,如果不为空,说明保存的有之前的fg,不用重新创建,fg内,系统会默认使用fragment的无参构造方法创建fragment,所以要恢复fg的参数,需要重新走解析mArguments参数的流程

onSaveInstanceState方法里会将我当前Activity内的Fragment存入HashMap中再存到Bundle对象中,然后当我Activity被销毁后返回回来时,系统会从onCreate方法和onRestoreInstanceState方法中将我们之前存入的Fragment取出来进行显示
fragment 知识点全方位覆盖
fragment-save.png

方案:newInstance方案创建fragment

解析:创建fg时候setArguments传参,回收时候ac保存fg及mArguments等,恢复时候才能重新获取mArguments

public static OneFragment newInstance(Class param){
    OneFragment oneFragment = new OneFragment();

    Bundle bundle = new Bundle();
    bundle.putInt("someArgs", param);

    oneFragment.setArguments(bundle);
    return oneFragment;
}

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    if (savedInstanceState==null) {
        oneFragment = OneFragment.newInstance();
        ft.add(R.id.fl_content, oneFragment, "OneFragment"); 
    }else {
        oneFragment = (OneFragment) getSupportFragmentManager().findFragmentByTag("OneFragment");
    }
    
    //重建的时候,可以从保存的Arguments中重新获取初始化参数
    Bundle bundle = getArguments();
}

3.5 ViewPager + Fragment 方案

使用FragmentPagerAdapter+ViewPager时,切换回上一个Fragment页面时(已经初始化完毕),不会回调任何生命周期方法以及onHiddenChanged(),只有setUserVisibleHint(boolean isVisibleToUser)会被回调。。另外,setUserVisibleHint(boolean isVisibleToUser) 方法总是会优先于 Fragment 生命周期函数的调用,所以如果你想进行一些懒加载,需要在这里处理。

在给ViewPager绑定FragmentPagerAdapter时,
new FragmentPagerAdapter(fragmentManager)的FragmentManager,一定要保证正确,如果ViewPager是Activity内的控件,则传递getSupportFragmentManager(),如果是Fragment的控件中,则应该传递getChildFragmentManager()。只要记住ViewPager内的Fragments是当前组件的子Fragment这个原则即可。

3.6 ~PagerAdapter 注意点

FragmentPagerAdapter与FragmentStatePagerAdapter有什么区别

主要区别就在与对于fragment是否销毁,下面细说:

FragmentPagerAdapter:对于不再需要的fragment,选择调用detach方法,仅销毁视图,并不会销毁fragment实例。

FragmentStatePagerAdapter:会销毁不再需要的fragment,当当前事务提交以后,会彻底的将fragment从当前Activity的FragmentManager中移除,state标明,销毁时,会将其onSaveInstanceState(Bundle outState)中的bundle信息保存下来,当用户切换回来,可以通过该bundle恢复生成新的fragment,也就是说,你可以在onSaveInstanceState(Bundle outState)方法中保存一些数据,在onCreate中进行恢复创建。

如上所说,使用FragmentStatePagerAdapter当然更省内存,但是销毁新建也是需要时间的。一般情况下,如果你是制作主页面,就3、4个Tab,那么可以选择使用FragmentPagerAdapter,如果你是用于ViewPager展示数量特别多的条目时,那么建议使用FragmentStatePagerAdapter

另外,viewPager.setOffscreenPageLimit 这个离屏缓存,默认是1,所以,即使用的FragmentPagerAdapter,也要设置这个参数。否则,也只会保存两个fg存活

你不需要考虑在“内存重启”的情况下,去恢复的Fragments的问题,因为FragmentPagerAdapter已经帮我们处理啦。

3.7 Fragment 懒加载方案

学习文章:Androidx 下 Fragment 懒加载的新实现

原文:https://www.jianshu.com/p/2201a107d5b5

  • add-show-hide 方案
fragment 知识点全方位覆盖
fg-ash-1.png
fragment 知识点全方位覆盖
fg-ash-2.png
fragment 知识点全方位覆盖
fg-ash-3.png
  • viewpager + fragment 方案
fragment 知识点全方位覆盖
vp-fg-lazy-1.png
fragment 知识点全方位覆盖
vp-fg-lazy-2.png
fragment 知识点全方位覆盖
vp-fg-lazy-3.png
fragment 知识点全方位覆盖
vp-fg-lazy-4.png

通过上述两种方案fg的生命周期,可以形成下面的懒加载方案

abstract class LazyFragment extends Fragment {
    //是否执行了懒加载
    private boolean isLoaded = false;
    /**
     * 当前Fragment是否对用户可见
     */
    private boolean isVisibleToUser = false;
    /**
     * 当使用ViewPager+Fragment形式会调用该方法时,setUserVisibleHint会优先Fragment生命周期函数调用,
     * 所以这个时候就,会导致在setUserVisibleHint方法执行时就执行了懒加载,
     * 而不是在onResume方法实际调用的时候执行懒加载。所以需要这个变量
     */
    private boolean isCalledResume = false;

    /**
     * 是否调用了setUserVisibleHint方法。处理show+add+hide模式下,默认可见 Fragment 不调用
     * onHiddenChanged 方法,进而不执行懒加载方法的问题。
     */
    private boolean isCallUserVisibleHint = false;

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        this.isVisibleToUser = isVisibleToUser;
        this.isCallUserVisibleHint = true;
        judgeLazyInit();
    }

    @Override
    public void onResume() {
        super.onResume();
        this.isCalledResume = true;
        if (!isCallUserVisibleHint) {
            //处理这个,是因为add-show-hide模式管理fg方案下,不走setUserVisibleHint,并且第一个默认显示的fg不回调onHiddenChanged
            // 所以表示:不走setUserVisibleHint的就是走onHiddenChanged的add-show-hide方案
            // 但是默认第一个显示的fg不走onHiddenChanged方案,所以才有了这个onresume中的判断逻辑
            this.isVisibleToUser = !isHidden();
        }
        judgeLazyInit();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        this.isLoaded = false;
        this.isCalledResume = false;
        this.isVisibleToUser = false;
        this.isCallUserVisibleHint = false;
    }

    @Override
    public void onHiddenChanged(boolean hidden) {
        super.onHiddenChanged(hidden);
        // add-show-hide 方案才会回调这个方法
        // 把字段归并给回调setUserVisibleHint的情况。
        this.isVisibleToUser = !hidden;
        judgeLazyInit();
    }

    private void judgeLazyInit() {
        //isVisibleToUser 是表示肉眼可见,是包括了两种情况,add-show-hide方案的情况,和vp+fg的方案
        if (!this.isLoaded && this.isVisibleToUser && this.isCalledResume) {
            lazyInit();
            this.isLoaded = true;
        }
    }

    abstract void lazyInit();
}

分析理解:

  • viewpager + fg的方案,所有fg的setUserVisibleHint是先于其他生命周期运行的。
  • 但是只有当fg显示或在预缓存限制内的fg才会走fg的生命周期。
  • 只有当isVisibleToUser为true(表示当期窗口显示的fg,肉眼可见的) 并且 已经onresume的(比如第二个第三个等:isVisibleToUser为true是在onresume之后的)触发懒加载。
  • 所以综合来看,两个回调都有可能要触发加载,所以,才有了lazyload的判断条件。
  • 另外,兼容add-show-hide方案,其实onHiddenChanged和setUserVisibleHint对应是两种方案的两种回调,上面把标记字段统一为了isVisibleToUser。
  • add-show-hide方案中,默认显示的第一个fg onHiddenChanged不回调

3.8 androidx中viewpager的setMaxLifecycle 懒加载方案

学习文章:Androidx 下 Fragment 懒加载的新实现

原文:https://www.jianshu.com/p/2201a107d5b5

androidx中,FragmentPagerAdapter构造方法多个@Behavior int behavior参数:

  • BEHAVIOR_SET_USER_VISIBLE_HINT:跟原Support下的ViewPager一致
  • BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT:表示滑动切换fragment时候,只有显示可见的fragment才回调onresume,而不再像上一节方案中fragment的生命周期那么不友好

既然只有当显示的fragment才回调onresume,那么懒加载方案就简单了:

abstract class LazyFragment : Fragment() {

    private var isLoaded = false

    override fun onResume() {
        super.onResume()
        if (!isLoaded) {
            lazyInit()
            Log.d(TAG, "lazyInit:!!!!!!!”)
            isLoaded = true
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        isLoaded = false
    }

    abstract fun lazyInit()
}

但是针对fragment嵌套的情况,上述方案并不完美,因为fragment嵌套情况下,并不是只有显示的fragment才调用onresume。。

fragment 知识点全方位覆盖
flaw-in-androidx-vp.png

所以,针对嵌套的子fragment,增加isHidden的判断

abstract class LazyFragment : Fragment() {

    private var isLoaded = false

    override fun onResume() {
        super.onResume()
        //增加了Fragment是否可见的判断
        if (!isLoaded && !isHidden) {
            lazyInit()
            Log.d(TAG, "lazyInit:!!!!!!!”)
            isLoaded = true
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        isLoaded = false
    }

    abstract fun lazyInit()

}

3.9 ViewPager2 懒加载方案

上面两种懒加载方案都涉及到Fragment生命周期的棘手问题。特别是onResume周期函数,用惯了activity的我们,Fragment的onResume使用就很不舒服,

ViewPager2 本身就支持对实际可见的 Fragment 才调用 onResume 方法

3.10 回退栈相关

学习文章

原文:https://blog.csdn.net/guxiao1201/article/details/40476267

首先需要明确的是,FragmentActivity的FragmentManager是处理Fragment Transaction的而不是处理Fragment。BackStack内部的一个Transaction可以包含一个或多个和Fragment相关的操作

FragmentTransaction默认并不会主动被加入到BackStack中,除非开发者调用了addToBackStack(String tag)方法

和addToBackStack相对应的接口方法是popBackStack(),调用该方法后会将事物操作插入到FragmentManager的操作队列,只有当轮询到该事物时才能执行。所以Google还提供了可以立刻执行的接口popBackStackImmediate()

addToBackStack源码里是将事务加入到BackStackRecord,表示加入维护回退栈记录而已,并不是将事务操作里的fragment加入到回退栈之类的

理解分析: FragmentManager操作单元是Transaction,可以理解为FragmentManager每次操作是从beginTransaction到commit的所有动作,addToBackStack的时候添加上tag以标记这次操作,比如addToBackStack(TagA), addToBackStack(TagB)。表示事务栈内现在操作了两波,TagA和TagB标记的。然后点击返回键,拦截到回退栈里不为空,就popBackStack,弹出去一个事务操作,剩下TagA事务操作结果。效果就等于说现在页面效果是TagA操作后的结果。。例如:add(FragmentA),addToBackstack(TagA),add(FragmentB),addToBackstack(TagB)。这时候fragment先显示的FragmentA,然后显示的FragmentB。popBackStack(),就是把TagB的操作反向处理,TagB事务标记的是添加FragmentB,反向操作就是移除FragmentB,剩下TagA的操作。界面显示的是FragmentA

注意API:

  • popBackStack() 是从事务栈弹出顶层事务

  • popBackStack(String tag) 是从事务栈弹出tag及以上的所有事务

  • popBackStack(String tag, int flag) 是从事务栈弹出tag以上的所有事务,是否弹出tag事务,取决于flag

    • flag:POP_BACK_STACK_INCLUSIVE 就是表示tag事务一块弹出
    • 一般传0,表示不包括
    public static final int POP_BACK_STACK_INCLUSIVE = 1
  • popBackStack(int id, int flag) 同上原理,事务commit的时候会返回个id

以上方法是将事务弹出放到事件loop中等着处理,还有立即处理的方法,同上,~Immediate()

深入分析:

学习文章

FragmentManager, BackStackRecord 源码还没有理清楚。。大致就是addBackStack是回退栈一条BackStackRecord记录。。该记录维护的有一个操作OP列表,大致包含操作命令cmd,fragment,一些进出动画。。重点是执行命令和fragment,有这俩,结合当前FragmentManager维护的现有fragments,就能反向操作了。比如addBackStack(“replaceOper”),replace操作就是把老的移除,把新的添加,popBackStack的时候,找到FragmentManager中栈顶的curruntFragment,在回退栈记录里的op里拿到oldFragment,cmd。先把curruntFragment移除,再把oldFragment添加,也就是反向replace

3.11 宿主引用问题

比如fg内网络请求的场景,异步任务,执行完成后回调,这时候宿主可能自己活宿主已经销毁了。所以,异步回调处的处理,要考虑处理自己和宿主是否可用的情况

方案:onAttach中保存ac对象。。Activity ac = (Activity)context

3.12 commit和commitAllowingStateLoss ~ commitNow和commitNowAllowingStateLoss

学习文章

原文:https://blog.csdn.net/zxq614/article/details/85785297

这两种提交事务的区别就在于是否安全的问题:

java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
...

看源码:

final class BackStackRecord extends FragmentTransaction implements
        FragmentManager.BackStackEntry, Runnable {
    ……
    public int commit() {
        return commitInternal(false);
    }

    public int commitAllowingStateLoss() {
        return commitInternal(true);
    }
    int commitInternal(boolean allowStateLoss) {
        ……
        mManager.enqueueAction(this, allowStateLoss);
        ……
    }
    ……
}
public abstract class FragmentManager {
    ……
    boolean mStateSaved;
    ……
    public void enqueueAction(Runnable action, boolean allowStateLoss) {
    // 1: 不允许页面关闭后还执行action的,进行状态检测
        if (!allowStateLoss) {
            checkStateLoss();
        }
    }

    private void checkStateLoss() {
    
        if (mStateSaved) {
        //3:页面已关,再提交,就抛异常
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
    }
    ……
    Parcelable saveAllState() {
        ……
        if (HONEYCOMB) {
            mStateSaved = true;
        }
        ……
    }
    ……
    public void noteStateNotSaved() {
        mStateSaved = false;
    }

    public void dispatchCreate() {
        mStateSaved = false;
        ……
    }

    public void dispatchActivityCreated() {
        mStateSaved = false;
        ……
    }

    public void dispatchStart() {
        mStateSaved = false;
        ……
    }

    public void dispatchResume() {
        mStateSaved = false;
        ……
    }
    ……
    public void dispatchStop() {
        //2:这里标记页面关闭,保存状态了
        mStateSaved = true;
        ……
    }
    ……
}

注意:

  • 使用commit(),异步业务中,可能抛上面的异常
  • 使用commitAllowingStateLoss(),虽然避免了异常问题,但是activity恢复时,提交的事务是丢失的。所以,提交的事务对应的操作是恢复不了的
  • 谷歌推荐使用:commitNow以及commitNowAllowingstateLoss()

3.13 replace的Fragment如何保持状态

学习文章

调用addFragment添加的fragment的View会保存到视图树(ViewTree)中,其中各个控件的状态都会被保存。但如果调用replace()来添加fragment,我们前面讲到过,replace()的实现是将同一个container中的所有fragment视图从ViewTree中全部清空!然后再添加指定的fragment。由于repalce操作会把以前的所有视图全部清空,所以当使用Transaction回退时,也就只有重建每一个fragment视图,所以就导致从replace操作回退回来,所有的控件都被重建,以前的用户输入全部没了。

到这里,大家首先要明白一个问题,repalce()操作,会清空同一个container中的所有fragment视图!注意用词:请空的是fragment的VIEW!fragment的实例并不会被销毁!因为fragment的实例是通过FragmentManager来管理的。当fragment的VIEW被销毁时,fragment实例并不会被销毁。他们两个不是同时的,即在fragment中定义的变量,所上次运行中被赋予的值是一直存在的。那fragment实例什么时候会被销毁呢,当然是在不会被用到的时候才会被销毁。那什么时候不会被用到呢,即不可能再回退到这个操作的时候,就会被销毁。
在上面的例子中,fragment1虽然被fragment2的repalce操作把它的视图给销毁了,但在执行replace操作时,将操作加入到了回退栈,这时候,FragmentManager就知道,用户还可能通过回退再次用到fragment1,所以就会保留fragment1的实例。相反,如果,在执行repalce操作时,没有加入到回退栈,那FragmentManager就肯定也知道,用户不可能再回到上次那个Fragment1界面了,所以它的fragment实例就会在清除fragment1视图的同时也被清除了

解决方案:

方案都是基于replace操作加入到回退栈的

  • 方案一

保存与恢复需要保存的状态:既然fragment实例没有销毁,销毁的知识视图,那么可以将状态保存到属性中啊,重新创建视图的时候再恢复

  • 方案二

给需要保持的状态对应的控件加上id。。

android:id="@+id/id_name"
  • 方案三

保存FragmentView视图

跟方案一差不多,就是将整个fragment的rootview保存到实例中

总结:总之理解了replace的原理,结合回退栈的原理。。怎么保持数据状态,要结合自己的业务需求,能实现的就是实现,不能实现的也可以考虑其他的方案,比如不用replace,show-hide方案也可行啊。

4. app框架方案

YoKeyword/Fragmentation

项目中用的该方案,效果不错

5. 参考学习资料

Fragment全解析系列(一):那些年踩过的坑

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。