Android使用Fragment切换tab

目前很多应用都是采用的底部导航+Tab的方式.目前项目中采用的是RadioButton + FrameLayout + Fragment的实现方式,所以总结一下遇到的几个问题.

  1. Activity重建导致Fragment重叠.

    • 复现方式: 系统销毁Activity,如改变字体大小.
    • 问题原因: Activity销毁时,系统会保存该Activity状态,而onCreate(Bundle state)会导致Activity的
    • 解决方法:
      • 在Activity销毁时不保存状态.如重写onSaveInstanceState (Bundle outState)并且不调用super方法,即在Activity销毁时不保存状态.但是该方式会导致Application创建两次.
      • 在Activity创建时,调用super.onCreate(null),即不根据其保存的状态来恢复Activity状态,而是直接重新创建.这算是一种偷懒的办法吧.
      • 在利用 FragmentManager addFragment 时,将每个 Fragment 设置不同的tag. 然后在 Activity 的 onCreate (Bundle savedInstanceState) 方法中, 利用 FragmentManager 根据不同 tag 来获取已经创建并添加了的 Fragemnt, 而不用重复创建.
  2. FragmentTransaction 在 commit 时, 如果 Activity 正在销毁, 会导致崩溃.

    异常为: java.lang.IllegalStateException: Can not perform this action after onSaveInstance!

    意思就是不能在 onSaveInstance() 后调用 commit 方法, 而 onSaveInstance() 是做状态保存工作, 调用时间由系统决定的, 它只能保证在 onStop() 前调用.

    • 最好在 FragmentActivity#onResumeFragments()Activity#onPostResume() 中调用 commit . 这两个方法能确保当前 Activity 的状态已经恢复.
    • 如果确保这个 commit 丢失也没关系, 可以使用FragmentTransaction#commitAllowingStateLoss();
  1. Fragment中调用getContext()getActivity()为null导致崩溃.

    • 复现方式: detach一个Fragment,然后异步函数调用getContext().

    • 问题原因: onDetach()调用后,getContext()会置为null.

    • 解决方法:

      • 在BaseFragment中维护一个mContext,然后重写onAttach(Context context)方法:

          @Override
          public void onAttach(Activity activity) {
              super.onAttach(activity);
              this.mContext = activity;
          }
        
          @Override
          public Context getContext() {
              if (super.getContext() != null) {
                  return super.getContext();
              }
              return mContext;
          }

        然后尽量调用getContext()方法.

      • 因为getActivity()方法不能重写,所以只好加上判断:

          if(isAdded()) {
              //XXXX
          }
  2. 如果使用 ViewPager + Fragment, ViewPager 在初始化时会缓存多个 Fragment, 这里可以通过 setUserVisibleHint 来判断 Fragment 是否可见.一个简单封装的 Fragment 如下:

     public abstract class BaseFragment extends Fragment {
    
         //必须有空的构造函数
         public BaseFragment() {
    
         }
    
         //避免ViewPager在一开始创建
         private boolean hasLazyLoad = false;
    
         public void setHasLazyLoad(boolean hasLazyLoad) {
             this.hasLazyLoad = hasLazyLoad;
         }
    
         /**
          * 懒加载,防止ViewPager重复创建
          */
         protected void onLazyLoad() {
    
         }
    
         @Override
         public void setUserVisibleHint(boolean isVisibleToUser) {
             super.setUserVisibleHint(isVisibleToUser);
             if (getUserVisibleHint() && !hasLazyLoad) {
                 onLazyLoad();
                 hasLazyLoad = true;
             }
         }
    
         @Override
         public void onDestroyView() {
             super.onDestroyView();
             hasLazyLoad = false;
         }
     }

    注意需要在 onDestroyView()中复位标志位,因为 ViewPager 的切换会导致 Fragment 的销毁.

  3. 在 Fragment 中嵌套 Fragment 时, 最好使用 getChildFragmentManager() 来获取 FragmentManager 示例,同时在 Fragment 销毁时, 利用反射将其置为空. 否则会导致错误 java.lang.IllegalStateException: Activity has been destroyed :

     @Override
     public void onDestroyView() {
         super.onDestroyView();
    
         //解决嵌套 Fragment 的bug
         try {
             Field childFragmentManager = Fragment.class.getDeclaredField("mChildFragmentManager");
             childFragmentManager.setAccessible(true);
             childFragmentManager.set(this, null);
    
         } catch (NoSuchFieldException e) {
             throw new RuntimeException(e);
         } catch (IllegalAccessException e) {
             throw new RuntimeException(e);
         }
     }