Android技能树 — Fragment总体小结

2019-08-20 11:14:55 浏览数 (1)


很久没有写文章了,没其他原因,就是因为懒。

因为最近的APP开发,使用的是单Activity 多Fragment的方式,不同于以前基本界面都是Activity的方式,所以Fragment用了很多,想到自己以前也写了很多相关的基础知识,Fragment却从来没有写过,所以就打算补上一篇fragment的基础总结。

老样子,先上脑图:

我们就按照脑图的顺序一样样来看Fragment的基础知识。


正文:

1.Fragment的添加

我们知道Fragment是一个"碎片(或者片段)",添加在Activity中。如果我现在问你,Activity要显示一个按钮Button,你会怎么做?

1. 直接在Layout.xml中添加**<Button/>**

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@ id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        />
</LinearLayout>

2. 在代码中动态添加,比如我们添加到一个LinearLayout中:

代码语言:javascript复制
Button button = new Button(mActivity);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(params);
container.addView(button);

所以Fragment也很简单,就把它当做一个简单的View(但其实更像是“子 Activity”),然后添加方式也是一样。

1. 直接在Layout.xml中添加:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <fragment
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

2. 直接在代码中添加:

代码语言:javascript复制
Fragment one = new FragmentOne();//自定义的Fragment类
//要先获取FragmentManager对象
FragmentManager fragmentManager = getSupportFragmentManager();
//开启一个FragmentTransaction事务
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(R.id.framelayout_view,one).commit();

其中添加到R.id.framelayout_view的这个idViewGourp可以是<FrameLayout/>,也可以是其他的比如<LinearLayout/>等。


2. Fragment的基本操作

看到我们上面的动态代码添加的时候需要获取FragmentTransactionFragmentManager

2.1 FragmentManager相关

1. getFragmentManager():

获取Fragment父容器的管理器,但是现在该方法在Activity中已经被标记不推荐使用了。

代码语言:javascript复制
 /**
     * Return the FragmentManager for interacting with fragments associated
     * with this activity.
     *
     * @deprecated Use {@link android.support.v4.app.FragmentActivity#getSupportFragmentManager()}
     */
    @Deprecated
    public FragmentManager getFragmentManager() {
        return mFragments.getFragmentManager();
    }
复制代码

2. getSupportFragmentManager():

v4包下的这个方法,与上一个效果一样,不过是Android推荐使用的方法(毕竟可以兼容Android所有版本)

3. getChildFragmentManager():

我们提过,Fragment更像是一个“子Activity”,那你说"子Activity"中能否再添加Fragment,答案当然是可以。那么在Fragment内部中的Fragment的管理器,就需要使用getChildFragmentManager()来获取了。

2.2 FragmentTransaction相关

我们可以看到有添加删除各种方法操作。

1. attach/detach方法:

  • detach(Fragment fragment) : 分离指定Fragment的UI视图
  • attach(Fragment fragment) : 重新关联一个Fragment(当这个Fragment的detach执行之后)
  • 当Fragment被detach后,Fragment的生命周期执行完onDestroyView就终止了,这意味着Fragment的实例并没有被销毁,只是UI界面被移除了(注意和remove是有区别的)。
  • 当Fragment被detach后,执行attach操作,会让Fragment从onCreateView开始执行,一直执行到onResume。
  • attach无法像add一样单独使用,单独使用会抛异常。方法存在的意义是对detach后的Fragment进行界面恢复。

2.add/remove方法:

我想这二个是用的最多的了,add()和remove()是将fragment添加和移除. remove()比detach()要彻底一些, 如果不加入到回退栈中, remove()的时候, fragment的生命周期会一直走到onDetach();如果加入了回退栈,则会只执行到onDestoryView(),Fragment对象还是存在的。

add一个fragment,如果加到的是同一个id的话,有点像我们的Activity栈,启动多个Activity时候,Activity一个个叠在上面,fragment也是类似,一个个fragment叠在上面。

3.replace方法:

replace = remove add , 所以可以理解为先把相同id下的Fragment移除掉,然后再加入这个当前的fragment。

所以如果你觉得Fragment存在太多,影响性能,可以用replace来切换各个界面,就可以保证当前只有一个Fragment,但是因为每次切换后,Fragment都会重建,所以如果这个界面有网络请求相关的,你就会发现这个界面又重新去请求网络接口了,显得很多此一举。

4.hide/show方法:

就是字面意思,让一个Fragment隐藏,让一个Fragment显示。你可以理解为Button设置了View.GONE和View.VISIBLE。常常配合有多个Fragment及有TAB等切换方式的时候,选中某个按钮,然后根据相应的让对应的Fragment显示,其他Fragment隐藏。

5.commit/commitAllowingStateLoss:

我估计很多人认识这个commitAllowingStateLoss大部分是因为自己的代码有闪退异常:

代码语言:javascript复制
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你执行Fragment事务,就会抛出该异常。然后网上有很多教程,叫你提交的时候使用commitAllowingStateLoss()方法,虽然说不会抛出错误,但是如果在Activity已经保存状态完之后提交了它,到时候Ativity意外崩溃,再恢复数据的时候就不会恢复在Activity保存状态之后提交的fragment的更新,造成状态丢失了。

额外补充: 1.commit()方法并不立即执行transaction中包含的动作,而是把它加入到UI线程队列中. 如果想要立即执行,可以在commit之后立即调用FragmentManager的executePendingTransactions()方法.commit()方法必须在状态存储之前调用,否则会抛出异常,如果觉得状态丢失没关系, 可以调用commitAllowingStateLoss(). 但是除非万不得已, 一般不推荐用这个方法, 会掩盖很多错误.

6. addToBackStack:

我们可以看到FragmentTransaction里面有加入回退栈方法,但是没有退出的方法:popBackStack。这是因为这个方法在FragmentManager里面。

也就是如下图:

一般反应是,addToBackStack和popBackStack不是应该像上面的类似add和remove一样,都一个层级的吗??所以popBackStack不也应该是FragmentTransaction下的一个方法???

所以我们单从图片所示就能知道,popBackStackFragmentTransaction是一个层级,所以popBackStack操作的其实也是《fragment事务》(FragmentTransaction),所以可以理解为addToBackStack把我们前面的FragmentTransaction事务(比如add,remove,replace等一系列操作)加入到了回退栈(!!!记住不是把fragment加入到了回退栈),而popBackStack是操作回退栈里面的事务。

当然具体的源码过程分析,细讲的话又是很多,都可以另外专门写一篇文章,所以直接借鉴网上别人已经写好的文章:

Fragment那点事①Fragment栈管理

额外补充: 1.加入回退栈:remove掉的fragment执行onDestoryView,并没有执行onDestory,fragment实例对象还是存在,当回退时候,fragment从onCreateView处执行 未加入回退栈:remove掉的fragment 执行 onDestoryView和onDestory,彻底销毁移除


3.Fragment中获取Context

我们可以直接在fragment代码里面直接使用getActivity()getContext()方法。

但是有时候获取为空,所以一般我们使用的是:

代码语言:javascript复制
Class xxxFragment extends Fragment {

    private Context mContext;
    
    //'高版本后,都是回调这个方法'
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        mContext = context;
    }
    
    //'API低于 23 的版本的时候,是会回调这个方法'
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        mContext = activity;
    }
}

4.Fragment配合ViewPager

ViewPager配合Fragment的时候,主要使用FragmentPagerAdapterFragmentStatePagerAdapter这二个Adapter。其实使用很简单(一般的最最简单的写法):

代码语言:javascript复制
public class FragmentAdapter extends FragmentPagerAdapter{
    private ArrayList<Fragment> list;

    //通过构造获取fragment集合
    public Fragment_pager(FragmentManager fm,ArrayList<Fragment> list) {
        super(fm);
        this.list=list;
    }
    //设置具体position的fragment
    @Override
    public Fragment getItem(int position) {
        // TODO Auto-generated method stub
        return list.get(position);
    }
    //设置有多少个fragment
    @Override
    public int getCount() {
        // TODO Auto-generated method stub
        return list.size();
    }
}
复制代码

然后ViewPager.setAdapter(xxxx);

但是大家会奇怪为啥有二个Adapter:FragmentPagerAdapterFragmentStatePagerAdapter,他们的区别我们可以看具体的源码:

FragmentPagerAdapter源码:

代码语言:javascript复制
public abstract class FragmentPagerAdapter extends PagerAdapter {


    //'初始化创建Item:'
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        long itemId = this.getItemId(position);
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = this.mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
        
            //'后面使用fragment是通过FragmentTransaction.attach方式加进来的,'
            //'只是重新绘制了UI,fragment对象还在。'
            this.mCurTransaction.attach(fragment);
        } else {
        
            //'我们知道刚返回fragment使用的是getItem(position)方法'
            //'我们可以看到第一次使用fragment是通过FragmentTransaction.add方式加进来的'
            fragment = this.getItem(position);
            this.mCurTransaction.add(container.getId(), fragment, makeFragmentName(container.getId(), itemId));
        }

        if (fragment != this.mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            fragment.setUserVisibleHint(false);
        }

        return fragment;
    }
    
    
    //'销毁item:'
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }
        
        //'我们可以看到FragmentTransaction只是单纯的detach了fragment,视图不在了,但是fragment对象还在'
        this.mCurTransaction.detach((Fragment)object);
    }
    
}

我们可以看到fragment并没有真的销毁,FragmentPageAdapter则适用于固定的,少量的Fragment情况,例如和TabLayout共同使用时。

FragmentStatePagerAdapter源码:

代码语言:javascript复制
public abstract class FragmentStatePagerAdapter extends PagerAdapter {


    
    @NonNull
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        Fragment fragment;
        if (this.mFragments.size() > position) {
            fragment = (Fragment)this.mFragments.get(position);
            if (fragment != null) {
                return fragment;
            }
        }

        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        fragment = this.getItem(position);
        if (this.mSavedState.size() > position) {
            SavedState fss = (SavedState)this.mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }

        while(this.mFragments.size() <= position) {
            this.mFragments.add((Object)null);
        }

        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        this.mFragments.set(position, fragment);
        
        //'我们可以看到fragment都是add进来的'
        this.mCurTransaction.add(container.getId(), fragment);
        return fragment;
    }
    
    
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment)object;
        if (this.mCurTransaction == null) {
            this.mCurTransaction = this.mFragmentManager.beginTransaction();
        }

        while(this.mSavedState.size() <= position) {
            this.mSavedState.add((Object)null);
        }

        this.mSavedState.set(position, fragment.isAdded() ? this.mFragmentManager.saveFragmentInstanceState(fragment) : null);
        this.mFragments.set(position, (Object)null);
        
        
        //'可以看到都是通过remove的方式移除了'
        this.mCurTransaction.remove(fragment);
    }

    

}

所以我们知道了FragmentStatePagerAdapter是真的会把fragment对象都销毁,所以如果fragment数量很多的话,使用这个会更好,因为fragment存在太多,对应用性能造成很大影响,所以要remove掉fragment。


5.无UI的fragment:

5.1 使用Fragment 保持需要恢复对象

调用setRetainInstance(true)方法可保留fragment,如下:

代码语言:javascript复制
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setRetainInstance(true);
    ...........
}

比如旋转屏幕,已保留的fragment不会随着activity一起被销毁(但会销毁fragment的视图); 相反,它会一直保留(进程不消亡的前提下),并在需要时原封不动地传递给新的Activity。

所以我们比如一些对象可以保持在fragment中,这时候Activity重新恢复后,其他对象可以从fragment中找回。

可以大概看下其他作者文章介绍:

Fragment调用setRetainInstance的原理

5.2 类似RxPermission用于处理回调

RxPermission里有一个Fragment用于分发权限回调。这个是什么意思??

我们知道原生请求权限:

代码语言:javascript复制
//发出权限请求:
int requestCode = 1;
requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE},requestCode);


//权限处理结果回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}

是不是觉得在要看复写这个回调方法很麻烦???而且没有美感。

而RxPermission是这样申请权限的:

代码语言:javascript复制
RxPermissions rxPermissions = new RxPermissions(this);

rxPermissions.requestEach(
        //请求的权限
        Manifest.permission.CAMERA,
        Manifest.permission.READ_PHONE_STATE)
        .subscribe(new Consumer<Permission>() {
        @Override
        public void accept(@io.reactivex.annotations.NonNull Permission permission) throws Exception {
            //权限通知回调
        }
});

感觉就是一步呵成的感觉,很棒。但是RxPermission只是对系统的原生权限申请做了封装而已,那系统的原本的回调函数:onRequestPermissionsResult去哪里了呢???

代码语言:javascript复制
public class RxPermissionsFragment extends Fragment {
    
    .......
    .......
    .......
    
    //'申请权限'
    @TargetApi(23)
    void requestPermissions(@NonNull String[] permissions) {
        this.requestPermissions(permissions, 42);
    }
    
    //'申请权限后结果回调'
    @TargetApi(23)
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 42) {
            boolean[] shouldShowRequestPermissionRationale = new boolean[permissions.length];

            for(int i = 0; i < permissions.length;   i) {
                shouldShowRequestPermissionRationale[i] = this.shouldShowRequestPermissionRationale(permissions[i]);
            }

            this.onRequestPermissionsResult(permissions, grantResults, shouldShowRequestPermissionRationale);
        }
    }
    
    //'回调后的具体处理方法'
    void onRequestPermissionsResult(String[] permissions, int[] grantResults, boolean[] shouldShowRequestPermissionRationale) {
        int i = 0;

        for(int size = permissions.length; i < size;   i) {
            this.log("onRequestPermissionsResult  "   permissions[i]);
            PublishSubject<Permission> subject = (PublishSubject)this.mSubjects.get(permissions[i]);
            if (subject == null) {
                Log.e(RxPermissions.TAG, "RxPermissions.onRequestPermissionsResult invoked but didn't find the corresponding permission request.");
                return;
            }

            this.mSubjects.remove(permissions[i]);
            boolean granted = grantResults[i] == 0;
            
            //'subject主动调用onNext方法发送结果'
            subject.onNext(new Permission(permissions[i], granted, shouldShowRequestPermissionRationale[i]));
            subject.onComplete();
        }

    }

    
    ......
    ......
    ......  

 
}

我们可以到这个fragment内部已经帮我们复写了请求权限的原生方法和权限回调通知的原生方法。然后再通过subject在结果处发送通知即可。

这里我不会细讲整个RxPermission源码,我以前写过的相关文章,大家可以具体看下:

项目需求讨论 - 动态权限申请分析及相关第三方库源码分析

项目需求讨论 — 手把手带你写RxPermission


6.构造函数和数据传递

6.1 构造函数传递数据

我们知道fragment也就是普通的对象,可以通过new的方式,我们平常使用对象传递值都是可以直接在构造函数里面定义参数值,直接赋值进去,那fragment是否可以这样??答案是可以的,但是不推荐。

代码语言:javascript复制
public class FragmentOne extends Fragment {
    
    //'在其他地方直接FragmentOne one = new FragmentOne("青蛙要fly");进行值传递'
    //'但是我们不推荐这样'
    public FragmentOne(String value) {

    }
    
    
    //'而是通过bundle来传递,Fragment.setArguments(Bundle)赋值进去'
    public static FragmentOne newInstance(Bundle args) {
        FragmentOne fragment = new FragmentOne();
        if(args != null){
            fragment.setArguments(args);    
        }
        return fragment;
    }
    
}

原因:我们可以知道Activity重新创建时,会重新构建它所管理的Fragment,原先的Fragment的字段值将会全部丢失(因为当切换横竖屏时,Fragment会调用自己的无参构造函数,那么在构造函数传参就会失效),但是通过 Fragment.setArguments(Bundle bundle)方法设置的bundle会保留下来,从而数据又可以恢复,所以尽量使用 Fragment.setArguments(Bundle bundle)方式来传递参数

6.2 其他数据传递方式

Activity 与 Fragment 数据传递:

Fragment 与 Fragment 数据传递

重点说下setTargetFragment,因为很多人都不知道。

我们的目标:FragmentA 启动FragmentB ,然后FragmentB做完事情,返回结果给FragmentA

代码语言:javascript复制
FragmentB.setTargetFragment(FragmentA);

然后在B中:
getTargetFragment().onActivityResult(getTargetRequestCode(), resultOK, i);


然后再FragmentA中:
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
}

7. Fragment重建恢复数据

推荐下面这篇文章:

[译] 保存/恢复 Activity 和 Fragment 状态的最佳实践

引用一段话:

完全分开处理Fragment状态和view状态 为了使你的代码变得干净和可扩展,你最好把Fragment状态和View状态分开处理。如果这里有任何属性是属于View的,在View内部进行保存和恢复.如果这里有任何属性是属于Fragment的,在Fragment内部进行保存和恢复。


8.常用监听Fragment显示方法

这块比较基础,就不细讲了。


9.监听Fragment发生变化

回退栈(back stack)状态改变监听:

代码语言:javascript复制
getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {
    @Override
    public void onBackStackChanged() {
    
    }
});
复制代码

注册fragment的生命监听:

代码语言:javascript复制
List<Fragment> fragmentList = new ArrayList<>();

getActivity().getSupportFragmentManager().registerFragmentLifecycleCallbacks(new FragmentManager.FragmentLifecycleCallbacks() {
    @Override
    public void onFragmentViewCreated(@NonNull FragmentManager fm, @NonNull Fragment f, @NonNull View v, @Nullable Bundle savedInstanceState) {
        super.onFragmentViewCreated(fm, f, v, savedInstanceState);
        fragmentList.add(f);
    }

    @Override
    public void onFragmentViewDestroyed(@NonNull FragmentManager fm, @NonNull Fragment f) {
        super.onFragmentViewDestroyed(fm, f);
        fragmentList.remove(f);
    }
}, false);

//'这里我们注意下最后的代码里面的那个false,这个false的意思是:不递归碎片中的碎片了,就是碎片栈中的碎片'

复制代码

10. DialogFragment:

我们知道现在大家已经很少使用了Dialog类,而是使用了DialogFragment,其本质就是个Fragment。

其实这个本来也想多写点,但是我估计这个基本安卓开发都使用过,所以就直接用网上其他作者的基础介绍文章:

Android 必知必会 - DialogFragment 使用总结

同时具体的自定义DialogFragment我以前文章也有写过:

项目需求讨论-仿ios底部弹框实现及分析


结语:

很久没写文章了。一看居然都快半年了......后面准备慢慢的补起自己的博客。有错误的地方欢迎大家指出

0 人点赞