Fragment那点事⑤保存与恢复实践

文章里所有分析都是根据Android Sdk 25.3.1 v4包

经过前面的分析之后现在有这么一个需求:

  1. APP 总共 3 个 Tab,首页(HomePageFragment),分类(CategoryFragment),我的(MineFragment)这 3 个 Tab 为第一级。
  2. TopicListFragment 根据穿件来不同的 tab 字段可以分别加载首页、分类(热门,精华,分享,招聘)页面的帖子列表。
  3. 要实现 Activity 被重建的时候所有帖子列表页面维持不变,不重新加载新的数据、帖子列表懒加载。
  4. Activity 重建用屏幕旋转来模拟。

效果图

通过分析可以发现,最外层分别为 首页,分类,我的 3 个 Fragment。在首页嵌套一个 TopicListFragment 到 HomePageFragment 里去,在分类页面嵌套一个 ViewPager,ViewPager 的每一页都是 TopicListFragment。

MainActivity

根据之前分析的 onSaveInstanceState 在该方法中保存 currPosition 当前可见的 Tab id。

@Override
public void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putInt("currPosition", mCurrPosition);
}

MainActivity.onCreate() 方法中做如下处理,判断 savedInstanceState == null

  • 如果为真,说明没有发生重启,直接初始化 3 个Fragment 实例并 add 进 Activity 中。
  • 如果为假,说明 FragmentActivity 已经帮我们做好了 Fragment 的保存与恢复工作,不用再重复的去添加新的实例到 FragmentManager,直接去 FragmentManager 中去获取这 3 个 Fragment 的实例(在外部获取实例在切换 tab 的时候需要)。
@Override
public void initView(Bundle savedInstanceState) {
  
  if (savedInstanceState == null) {
    mFragments = new BaseFragment[3];
    mFragments[0] = HomePageFragment.newInstance();
    mFragments[1] = CategoryFragment.newInstance();
    mFragments[2] = MineFragment.newInstance();
    FragmentUtils.addMultiple(getSupportFragmentManager(), R.id.content, mCurrPosition, mFragments);
  } else {
    mCurrPosition = savedInstanceState.getInt("currPosition");
    mFragments[0] = findFragment(HomePageFragment.class);
    mFragments[1] = findFragment(CategoryFragment.class);
    mFragments[2] = findFragment(MineFragment.class);

    if (mCurrPosition != 0) {
      updateNavigationBarState(mCurrPosition);
    }
  }
}

下面是 FragmentUtils.addMultiPle() 方法,添加多个 Fragment 到 content 并设置显示和隐藏。并调用 Fragment.setUserVisibleHint 更新 mUserVisibleHint 属性,在 Fragment 中默认该属性为 true。

public static void addMultiple(FragmentManager manager, int containerId, int showPosition, BaseFragment... fragments) {
  FragmentTransaction transaction = manager.beginTransaction();
  for (int i = 0; i < fragments.length; i++) {
    String tag = fragments[i].getClass().getName();
    transaction.add(containerId, fragments[i], tag);
    if (showPosition != i) {
      transaction.hide(fragments[i]);
    }
    fragments[i].setUserVisibleHint(!fragments[i].isHidden());
  }
  transaction.commit();
}

下面是 FragmentUtils.FindFragment() 方法,根据添加 Fragment 的时候设置的 Tag,可以获取 Fragment。

public static <T extends BaseFragment> T findFragment(FragmentManager manager, Class<T> tClass) {
  if (manager.getFragments() == null) {
    return null;
  }
  return (T) manager.findFragmentByTag(tClass.getName());
}

切换 tab 的时候 show 要显示的 Fragment,其余 Fragment 被 hide,因为 show 和 hide 并不会改变 mUserVisibleHint 所以要手动调用 setUserVisibleHint() 。

public static void showHideFragment(FragmentManager manager, Fragment show, Fragment hide, boolean animation, boolean backStack) {
  FragmentTransaction transaction = manager.beginTransaction();
  if (animation) {
    transaction.setCustomAnimations(
      R.anim.fragment_translate_in, R.anim.fragment_translate_out
      ,R.anim.fragment_pop_in,R.anim.fragment_pop_out);
  }
  transaction.show(show);
  if (hide == null) {
    List<Fragment> fragments = manager.getFragments();
    if (fragments != null) {
      for (Fragment fragment : fragments) {
        if (fragment != show) {
          transaction.hide(fragment);
        }
      }
    }
  } else {
    transaction.hide(hide);
  }
  if (backStack) {
    transaction.addToBackStack("showHideFragment");
  }
  transaction.commit();
}

HomePageFragment

在 HomePageFragment.onViewCreated() 方法中的处理和 MainActivity.onCreate() 方法中相似。这个时候 ChildFragmentManager 已经恢复完毕,调用 findFragmentByTag 查看是否包含 TopicListFragment,如果不包含就重新实例化添加。

@Override
public void initView(View root) {
  if (getChildFragmentManager().findFragmentByTag(TopicListFragment.class.getName()) == null) {
    TopicListFragment fragment = TopicListFragment.instance(null);
    fragment.setUserVisibleHint(true);
    FragmentUtils.replace(getChildFragmentManager(), R.id.home_page_content, fragment, false, TopicListFragment.class.getName());
  }
}

TopicListFragment

在 onCreate() 方法中设置 setRetainInstance(true) 由之前的分析可知,当 Activity 发生重启后,该 Fragment 实例会被保存下来,已经获取的帖子数据也不用重新请求,直接通过 RecyclerView 的自我恢复重新填充。

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setRetainInstance(true);
}

CategoryFragment

在 CategoryFragment 中的布局是一个 ViewPager,在 ViewPager 中实现了 View.onSaveInstanceState() 这个方法,在之前介绍 Fragment 保存与恢复中(Fragment 保存内部 View 状态的流程)讲过,部分 View 实现了该方法,可以自行保存状态。那就看看 ViewPager 保存了什么东西

@Override
public Parcelable onSaveInstanceState() {
  Parcelable superState = super.onSaveInstanceState();
  SavedState ss = new SavedState(superState);
  ss.position = mCurItem;
  if (mAdapter != null) {
    ss.adapterState = mAdapter.saveState();
  }
  return ss;
}

通过分析可知,保存了当前页面的 position 并调用 mAdapter.saveState() 方法保存 mAdapter 的状态(FragmentPagerAdapter 并没有保存任何东西返回 null)。

FragmentPagerAdapter

@Override
public Object instantiateItem(ViewGroup container, int position) {
  if (mCurTransaction == null) {
    mCurTransaction = mFragmentManager.beginTransaction();
  }
  final long itemId = getItemId(position);
  
  // Do we already have this fragment?
  String name = makeFragmentName(container.getId(), itemId);
  Fragment fragment = mFragmentManager.findFragmentByTag(name);
  if (fragment != null) {
    if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
    mCurTransaction.attach(fragment);
  } else {
    fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
    mCurTransaction.add(container.getId(), fragment,
                        makeFragmentName(container.getId(), itemId));
  }
  if (fragment != mCurrentPrimaryItem) {
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
  }
  return fragment;
}

由上面 instantiateItem 方法可知,PagerAdapter 是靠宿主的 FragmentManager 来管理 Fragment 的,即所有的 Fragment 都被托管在 CategoryFragment 的 ChildFragmentManager 中,当 findFragmentByTag 不为空时就直接使用该 fragment 实例,反之调用 getItem(position) 实例化一个新的 Fragment。

本文中使用的案例源码地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容