方法一: 布局里的标签 标识符: tag, id, 如果都没有, container的id将会被使用.
方法二: 动态添加 动态添加利用了一个transaction:
FragmentManager fragmentManager = getFragmentManager(); Fragment fragment = fragmentManager.findFragmentByTag(FragmentB.TAG); if (null == fragment) { FragmentB fragmentB = new FragmentB(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); fragmentTransaction.add(R.id.fragment_container, fragmentB, FragmentB.TAG) .commit(); }commit()方法并不立即执行transaction中包含的动作,而是把它加入到UI线程队列中. 如果想要立即执行,可以在commit之后立即调用FragmentManager的executePendingTransactions()方法.
commit()方法必须在状态存储之前调用,否则会抛出异常,如果觉得状态丢失没关系,可以调用commitAllowingStateLoss(). 但是除非万不得已, 一般不推荐用这个方法, 会掩盖很多错误.
Activity的back stack: 系统维护, 每个task一个back stack. Fragment的back stack: 宿主activity掌管, 每个activity一个.
通过调用addToBackStack(),commit()的一系列转换作为一个transaction被存储在back stack中, 用户按Back键, 从栈中pop出一个transaction, 逆转操作, 可以返回上一个转换前的状态.
一个transaction可以包含多种操作, 并且不局限于对同一个Fragment, 所以每一个transaction实际上可以是一系列对多个fragment的操作的组合. 加入到back stack中去的时候, 是把这一系列的组合作为一个原子, 加入到back stack中.
所有的Fragment都必须有一个public的无参构造函数, 因为framework经常会在需要的时候重新创建实例(状态恢复时), 它需要的就是这个构造. 如果无参构造没有提供,会有异常.
所以不要给Fragment写有参数的构造函数, 也不要企图搞个什么单例的Fragment. 这些都是反设计的.
参数传递的正确姿势:
public static FragmentWithParameters newInstance(int num) { FragmentWithParameters fragmentWithParameter = new FragmentWithParameters(); Bundle args = new Bundle(); args.putInt(NUM, num); fragmentWithParameter.setArguments(args); return fragmentWithParameter; } @Overridepublic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); num = getArguments() != null ? getArguments().getInt(NUM) : 0; }这里是提供了一个静态方法, 也可以new出对象后自己set Bundle参数.
除了DialogFragment和嵌套Fragment需要与自己的parent fragment通信以外, 一般的fragment是不与其他fragment有任何通信的. 因为要求应尽量独立, 模块化, 可复用. fragment与自己的parent activity (除了嵌套和dialog的情况外, 这个parent通常是activity) 有直接通信, 一般以这三种方式:
在构造fragment的时候, 通过Bundle传递参数.parent可以直接调用fragment的public方法, 这里也可以传递一些参数.Listener, 也即parent实现的callback接口, fragment可以在自己内部调用, 这里fragment也可以传递参数出去.对于DialogFragment来说, 可以通过一个public的set方法将外面的target设置进去. 比如用Fragment的这个方法: setTargetFragment()
例子 对于嵌套(nested)Fragment, 通信方式与上面普通的fragment类似, 只不过parent此时不是activity而是一个fragment. 后面会单独有一个文章说嵌套Fragment的使用, 敬请期待.
Fragment的生命周期首先和Activity的生命周期密切相关, 如果activity stopped,其中所有的fragment都不能start; 如果activity destroyed, 其中所有的fragment都会被destroyed. 只有activity在resumed状态下,fragment的生命周期可以独立改变,否则它被activity控制.
上面这个图来自于: https://corner.squareup.com/2014/10/advocating-against-android-fragments.html 这里还有一个更吊的图: https://github.com/xxv/android-lifecycle
FragmentTransaction 中对Fragment有如下几种操作:
attach(), detach()add(), remove(), show(), hide(), replace()除了replace()以外其他都是成对的.
其中attach()和detach()不是很常用. 调用detach()之后, fragment实际的生命周期会走到onDestroyView(), 但不会走onDestroy()和onDetach(), 也即fragment本身并没有被销毁, 只是view被销毁了. 这和addToBackStack()的情况一样, 尽管调用detach()的时候没有addToBackStack(), 仍然只是走到view被销毁的阶段.
add()和remove()是将fragment添加和移除. remove()比detach()要彻底一些, 如果不加入到back stack, remove()的时候, fragment的生命周期会一直走到onDetach().
show()和hide()是用来设置fragment的显示和隐藏状态, 这两个方法并不对应fragment的状态变化,只是将view设置为visible和gone,然后调用onHiddenChanged()的回调.
实际上replace() == remove() + add(), 所以它的反操作也是replace(), 只不过把add和remove的东西交换一下.
关于replace()和show(), hide()的选择, 要根据实际使用情形来定. replace()的好处是会减少内存占用, 但是返回时需要重新走完初始化的过程. show()和hide()只是控制了fragment的显示和隐藏, 不会改变生命周期状态, 也即fragment始终是处于running状态的, 被保持在内存中, 适用于频繁切换的情形.
前面说过, replace() == remove() + add() 新的fragment将取代在容器布局中的fragment, 如果没有,将直接添加新的fragment.
是否添加到back stack对fragment的生命周期是有影响的. remove()或者replace()的时候,如果commit()之前没有调用addToBackStack(),那个旧fragment将会被destroyed和detach; 即完全销毁和移除.
如果调用了addToBackStack(),旧的fragment会处在stopped状态,调用到onDestroyView(), 可以通过返回键来resume. 这个时候对于旧的Fragment来说, 成员变量依然在,但是View被销毁了. 所以返回时它的生命周期从onCreateView()开始重建View.
-------------------------------------------------------------------------------------------------嵌套Fragments (Nested Fragments), 是在Fragment内部又添加Fragment. 使用时, 主要要依靠宿主Fragment的 getChildFragmentManager() 来获取FragmentManger. 虽然看起来和在activity中添加fragment差不多, 但因为fragment生命周期及管理恢复模式不同, 其中有一些需要特别注意的地方. 本文内容还包括了从Fragment迁移到v4.Fragment代码中需要改动的一些地方.
嵌套Fragments Nested Fragments 是Android 4.2 API 17 引入的. 目的: 进一步增强动态复用. 如果要在Android 4.2之前使用, 可以用support library v4的版本, 后面会有详细的迁移过程介绍.
在宿主fragment里调用getChildFragmentManager() 即可用它来向这个fragment内部添加fragments.
Fragment videoFragment = new VideoPlayerFragment(); FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); transaction.add(R.id.video_fragment, videoFragment).commit();同样, 对于内部的fragment来说, getParentFragment() 方法可以获取到fragment的宿主fragment.
getChildFragmentManager()是fragment中的方法, 返回的是管理当前fragment内部子fragments的manager. getFragmentManager()在activity和fragment中都有. 在activity中, 如果用的是v4 support库, 方法应该用getSupportFragmentManager(), 返回的是管理activity中fragments的manager. 在fragment中, 还叫getFragmentManager(), 返回的是把自己加进来的那个manager.
也即, 如果fragment在activity中, fragment.getFragmentManager()得到的是activity中管理fragments的那个manager. 如果fragment是嵌套在另一个fragment中, fragment.getFragmentManager()得到的是它的parent的getChildFragmentManager().
总结就是: getFragmentManager()是本级别管理者, getChildFragmentManager()是下一级别管理者. 这实际上是一个树形管理结构.
把Fragment迁移到v4版本, 需要改动如下地方:
import android.app.Fragment; -> import android.support.v4.app.Fragment; Activity -> FragmentActivity / AppCompatActivity activity.getFragmentManager() -> getSupportFragmentManager() Loader, LoaderManager, LoaderCursor也需要改成v4包的. activity.getLoaderManager() -> getSupportLoaderManager()Fragment中onTrimMemory()方法不见了 以前是这个方法
@Overridepublic void onTrimMemory(int level) { super.onTrimMemory(level); imageLoader.trimMemory(level); }v4版本需要改成这个
@Overridepublic void onLowMemory() { super.onLowMemory(); imageLoader.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE); }把嵌套Fragment放在布局里 -> InflateException in Binary XML
看起来嵌套fragment的使用除了要用getChildFragmentManager()以外, 其他跟之前似乎没什么区别. 如果嵌套的fragment不需要太多控制, 固定地占据了一块地方, 你可能想当然地为了省事就把它放进了xml布局文件里, 写个标签. 运行一下初看起来似乎没什么错, run一下也能显示出来, 但是千万不要这样做, 多玩两下更复杂的你就知道了.
上面官网介绍时就有这么一句:
Note: You cannot inflate a layout into a fragment when that layout includes a <fragment>. Nested fragments are only supported when added to a fragment dynamically.人家这么说肯定是有原因的哇, 下面我来告诉你我知道的问题: 如果Fragment被嵌套写在了布局里, inflate到这个标签的时候就相当于将它加进了FragmentManager里. 如果嵌套的parent fragment因为需要重建View而重新走了onCreateView()方法, 再次inflate, 此时就会抛出异常: InflateException in Binary XML
之前为什么可以呢? 非嵌套的情况, fragment直接加在activity里, 如果需要重新inflate, 必定是在onCreate()里, activity是重新建的, 所以没有问题, 因为不存在fragmentManager中已经持有同一个fragment的问题.
举一个例子: 在嵌套的情况下, 如果FragmentE布局里有FragmentA, 这时候我们需要叠加一个FragmentD. 用了replace(), 并且addToBackStack(). 当D显示的时候, E实际上View是被销毁的, 然后back回来, 重建View, 即FragementE需要重新从onCreateView ()开始走生命周期, 走到inflate的时候又看到了fragmentA的标签. 但是这时候A实际上还在FragmentManager里面, 所以就会抛出如下的异常: android.view.InflateException: Binary XML file line # XX: Binary XML file line #XX: Error inflating class fragment 崩溃的位置就在parent fragment(FragmentE) inflate的时候. 打印具体的异常栈信息可以看到:
at com.example.ddmeng.helloactivityandfragment.fragment.FragmentE.onCreateView(FragmentE.java:35)at android.app.Fragment.performCreateView(Fragment.java:2220)at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:973)at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148)at android.app.FragmentManagerImpl.popBackStackState(FragmentManager.java:1587)at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:578)at android.support.v4.app.BaseFragmentActivityEclair.onBackPressedNotHandled(BaseFragmentActivityEclair.java:27)at android.support.v4.app.FragmentActivity.onBackPressed(FragmentActivity.java:189)Caused by: java.lang.IllegalArgumentException: Binary XML file line #16: Duplicate id 0x7f0c0059, tag null, or parent id 0xffffffff with another fragment for com.example.ddmeng.helloactivityandfragment.fragment.FragmentAat android.app.FragmentManagerImpl.onCreateView(FragmentManager.java:2205)实验例子代码
解决上面的问题有各种方法, 最常规的做法是, 使用动态添加:
Fragment fragmentA = getChildFragmentManager().findFragmentByTag(NESTED_FRAGMENT_TAG); if (fragmentA == null) { Log.i(LOG_TAG, "add new FragmentA !!"); fragmentA = new FragmentA(); FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction(); fragmentTransaction.add(R.id.fragment_container, fragmentA, NESTED_FRAGMENT_TAG).commit(); } else { Log.i(LOG_TAG, "found existing FragmentA, no need to add it again !!"); }如果你的子fragment非要加在布局里不可, 而你的程序确实会有重建父fragment view的情形. 为了避免上面的异常, 你也可以这样做(tricky and not recommended):
public void removeChildFragment(Fragment parentFragment) { FragmentManager fragmentManager = parentFragment.getChildFragmentManager(); Fragment child = fragmentManager.findFragmentById(R.id.child); if (child != null) { fragmentManager.beginTransaction() .remove(child) .commitAllowingStateLoss(); } }在parentFragment的onCreateView()方法中inflate之前和onSaveInstanceState()方法中做save工作之前调用它. 这两个地方是发生异常的地方, 只要在其之前remove就好.
把fragment放在一个动态布局里 -> java.lang.IllegalArgumentException: No view found for id
发现这个错误是因为项目中的一个子Fragment是添加在RecyclerView里面的一块的. RecyclerView要等到Loader的数据取到了之后再populate每一块的布局. 还是上面的流程, 启动父fragment, load数据, 添加子fragment, 这都没有问题. 但是一旦如果是上面的replace()加addToBackStack() , 并且再次返回, 就会出现异常.
因为当重建View的时候, fragmentManager其中是持有child fragment的, 但是找不到它的container, 于是就会抛出异常. 我也同样做了一个小实验, 在我的demo程序里: HelloActivityAndFragment Nested Fragment in Dynamic Container: 在Fragment F中, 先添加一个FrameLayout, 再把child fragment A加进去. 然后在Activity中, 用D replace F, 按back键返回, 就会有crash:
java.lang.IllegalArgumentException: No view found for id 0x7f0c0062 (com.example.ddmeng.helloactivityandfragment:id/frame_container) for fragment FragmentA{b37763 #0 id=0x7f0c0062 FragmentA} at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:965) at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148) at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1130) at android.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManager.java:1953) at android.app.Fragment.performActivityCreated(Fragment.java:2234) at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:992) at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148) at android.app.BackStackRecord.popFromBackStack(BackStackRecord.java:1670) at android.app.FragmentManagerImpl.popBackStackState(FragmentManager.java:1587) at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:578) at android.app.Activity.onBackPressed(Activity.java:2503)这是因为返回的时候FragmentManager找不到对应的container了. 所以应该避免这种做法, 尽量把fragment加进parent的根布局里, 而不是某个动态添加的布局.
另附大神工具类Fragmentation.:https://github.com/YoKeyword/Fragmentation 有关于fragment的一些坑及解决版本,推荐start
-------------------------------------------------------------------------------------------------总结来说, 就是Activity的销毁, 分为彻底销毁和留下数据的销毁两种.
彻底销毁是指用户主动去关闭或退出这个Activity. 此时是不需要状态恢复的, 因为下次回来又是重新创建全新的实例. 留下数据的销毁是指系统销毁了activity, 但是当用户返回来时, 会重新创建它, 让用户觉得它一直都在.
屏幕旋转重建可以归结为第二种情况, 打开Do not keep activities开关, 切换activities也是会出现第二种情况. 打开Do not keep activities开关就是为了模拟内存不足时的系统行为, 这里有一篇分析
实际上系统已经帮我们做好了View层面基本的恢复工作, 主要是依靠下面两个方法:
@Overrideprotected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // 在onStop()之前调用, 文档中说并不保证在onPause()的之前还是之后// 我的试验中一般是在onPause()之后 } @Overrideprotected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); // 在onStart() 之后 }Bundle其中包含了activity中的view和fragment的各种信息, 所以调用基类的方法就可以完成基本的view层面的恢复工作. 注意这两个方法并不是activity的生命周期回调, 对于activity来说它们不是一定会发生的. 另外需要注意的是, View必须要有id才能被恢复.
举一个实例来说明: Activity A start B, 那么A的onSaveInstanceState()会在onStop()之前调用, 以防A被系统销毁. 但是在B中按下back键finish()了自己后, B被销毁的过程中, 并没有调用onSaveInstanceState(), 是因为B并没有被压入task的back stack中, 也即系统知道B并不需要储存自己的状态. 正常情况下, 返回到A, A没有被销毁, 也不会调用onRestoreInstanceState(), 因为所有的状态都还在, 并不需要重建.
如果我们打开了Do not keep activities开关, 模拟系统内存不足时的行为, 从A到B, 可以看到当B resume的时候A会一路走到onDestroy(), 而关掉B之后, A会从onCreate()开始走, 此时onCreate()的参数bundle就不为空了, onStart()之后会调用onRestoreInstanceState()方法, 其参数bundle中内容类似于如下:
Bundle[{android:viewHierarchyState=Bundle[mParcelledData.dataSize=272]}]其中包含了View的状态, 如果有Fragment, 也会包含Fragment的状态, 其实质是保存了FragmentManagerState, 内容类似于如下:
Bundle[{android:viewHierarchyState=Bundle[{android:views={16908290=android.view.AbsSavedState$1@bc382e7, 2131492950=CompoundButton.SavedState{4034f96 checked=true}, 2131492951=android.view.AbsSavedState$1@bc382e7}}], android:fragments=android.app.FragmentManagerState@bacc717}]对于上面的例子来说, B什么时候会调用onSaveInstanceState()呢? 当从A打开B之后, 按下Home键, B就会调用onSaveInstanceState(). 因为这时候系统不知道用户什么时候会返回, 有可能会把B也销毁了, 所以保存一下它的状态. 如果下次回来它没有被重建, onRestoreInstanceState()就不会被调用, 如果它被重建了, onRestoreInstanceState()才会被调用.
activity的onSaveInstanceState()和onRestoreInstanceState()方法在如下情形下会调用:
屏幕旋转重建: 先save再restore.启动另一个activity: 当前activity在离开前会save, 返回时如果因为被系统杀死需要重建, 则会从onCreate()重新开始生命周期, 调用onRestoreInstanceState(); 如果没有重建, 则不会调用onCreate(), 也不会调用onRestoreInstanceState(), 生命周期从onRestart()开始, 接着onStart()和onResume().按Home键的情形和启动另一个activity一样, 当前activity在离开前会save, 用户再次点击应用图标返回时, 如果重建发生, 则会调用onCreate()和onRestoreInstanceState(); 如果activity不需要重建, 只是onRestart(), 则不会调用onRestoreInstanceState().activity的onSaveInstanceState()和onRestoreInstanceState()方法在如下情形下不会调用:
用户主动finish()掉的activity不会调用onSaveInstanceState(), 包括主动按back退出的情况.新建的activity, 从onCreate()开始, 不会调用onRestoreInstanceState().如上, 系统已经为我们恢复了activity中的各种view和fragment, 那么我们自己需要保存和恢复一些什么呢? 答案是成员变量值.
因为系统并不知道你的各种成员变量有什么用, 哪些值需要保存, 所以需要你自己覆写上面两个方法, 然后把自己需要保存的值加进bundle里面去. 具体例子, 这里Activity的重新创建有, 我就不重复了. 重要的是不要忘记调用super的方法, 那里有系统帮我们恢复的工作.
在介绍下面的内容之前, 先介绍一个小工具: Icepick 这个工具的作用是, 在你想保存和重建自己的成员变量数据时, 帮你省去那些put和get方法的调用, 你也不用为每一个字段起一个常量key. 你需要做的就是简单地在你想要保存状态的字段上面加上一个@State 注解. 然后在保存和恢复的时候分别加上一句话:
@Overridepublic void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Icepick.restoreInstanceState(this, savedInstanceState); } @Overridepublic void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); Icepick.saveInstanceState(this, outState); }然后你的成员变量就有了它应该有的值了, DONE!
Fragment的状态比Activity的要复杂一些, 因为它的生命周期状态比较多.
按照上面的思路, 我先去查找Fragment中保存和恢复的回调方法了. Fragment的状态保存回调是这个方法:
public void onSaveInstanceState(Bundle outState) { // may be called any time before onDestroy() }这个方法和之前activity的情况大体是类似的, 它不是生命周期的回调, 所以只在有需要的时候会调到. onSaveInstanceState()在activity调用onSaveInstanceState()的时候发生, 用于保存实例状态.(看它的方法名: instance state). onSaveInstanceState()方法保存的bundle会返回给几个生命周期回调: onCreate(), onCreateView(), onViewCreated()和onActivityCreated().
Fragment并没有对应的onRestoreInstanceState()方法. 也即没有实例状态的恢复回调.
Fragment只有一个onViewStateRestored()的回调方法:
public void onViewStateRestored(@Nullable Bundle savedInstanceState) { // 在onActivityCreated()和onStart()之间调用 mCalled = true; }onViewStateRestored()每次新建Fragment都会发生. 它并不是实例状态恢复的方法, 只是一个View状态恢复的回调.
这里需要注意, Fragment的状态分两个类型: 实例状态和View状态. 这里有个最佳实践: The Real Best Practices to Save/Restore Activity's and Fragment's state 不要把Fragment的实例状态和View状态混在一起处理.
在这里我先上个结论, 把查看源码中Fragment状态保存和恢复的相关方法列出来:
Fragment状态保存入口:
Fragment的状态保存入口有三个:
Activity的状态保存, 在Activity的onSaveInstanceState()里, 调用了FragmentManger的saveAllState()方法, 其中会对mActive中各个Fragment的实例状态和View状态分别进行保存.FragmentManager还提供了public方法: saveFragmentInstanceState(), 可以对单个Fragment进行状态保存, 这是提供给我们用的, 后面会有例子介绍这个. 其中调用的saveFragmentBasicState()方法即为情况一中所用, 图中已画出标记.FragmentManager的moveToState()方法中, 当状态回退到ACTIVITY_CREATED, 会调用saveFragmentViewState()方法, 保存View的状态.moveToState()方法中有很长的switch case, 中间不带break, 基本是根据新状态和当前状态的比较, 分为正向创建和反向销毁两个方向, 一路沿着多个case走下去.
Fragment状态恢复入口:
三个恢复的入口和三个保存的入口刚好对应.
在Activity重新创建的时候, 恢复所有的Fragment状态.如果调用了FragmentManager的方法: saveFragmentInstanceState(), 返回值得到的状态可以用Fragment的setInitialSavedState()方法设置给新的Fragment实例, 作为初始状态.FragmentManager的moveToState()方法中, 当状态正向创建到CREATED时, Fragment自己会恢复View的状态.这三个入口分别对应的情况是: 入口1对应系统销毁和重建新实例. 入口2对应用户自定义销毁和创建新Fragment实例的状态传递. 入口3对应同一Fragment实例自身的View状态重建.
这里对应的是入口1的情况. 当Activity在做状态保存和恢复的时候, 在它其中的fragment自然也需要做状态保存和恢复. 所以Fragment的onSaveInstanceState()在activity调用onSaveInstanceState()的时候一定会发生. 同样的, 如果Fragment中有一些成员变量的值在此时需要保存, 也可以用@State标记, 处理方法和上面一样. 也即, 在Activity需要保存状态的时候, 其中的Fragments的实例状态自动被处理保存.
这里对应的是入口3的情况. 前面介绍过, activity在保存状态的时候, 会将所有View和Fragment的状态都保存起来等待重建的时候使用. 但是如果是单个Activity对应多个Fragments的架构, Activity永远是resume状态, 多个Fragments在切换的过程中, 没有activity的帮助, 如何保存自己的状态?
首先, 取决于你的多个Fragments是如何初始化的. 我做了一个实验, 在activity的onCreate()里面初始化两个Fragment:
private void initFragments() { tab1Fragment = getFragmentManager().findFragmentByTag(Tab1Fragment.TAG); if (tab1Fragment == null) { tab1Fragment = new Tab1Fragment(); } tab2Fragment = getFragmentManager().findFragmentByTag(Tab2Fragment.TAG); if (tab2Fragment == null) { tab2Fragment = new Tab2Fragment(); } }然后点击两个按钮来切换它们, replace(), 并且不加入到back stack中:
@OnClick(R.id.tab1) void onTab1Clicked() { getFragmentManager().beginTransaction() .replace(R.id.content_container, tab1Fragment, Tab1Fragment.TAG) .commit(); } @OnClick(R.id.tab2) void onTab2Clicked() { getFragmentManager().beginTransaction() .replace(R.id.content_container, tab2Fragment, Tab2Fragment.TAG) .commit(); }可以看到, 每一次的切换, 都是一个Fragment的完全destroy, detach和另一个fragment的attach, create, 但是当我在这两个fragment中各自加上EditText, 发现只要EditText有id, 切换过程中EditText的内容是被保存的. 这是谁在什么时候保存并恢复的呢? 我在TextChange的回调里打了断点, 发现调用栈如下: 在FragmentManagerImpl中, moveToState()方法的case Fragment.CREATED中: 调用了: f.restoreViewState(f.mSavedFragmentState); 此时我没有做任何保存状态的处理, 但是断点中可以看出: 虽然mSavedFragmentState是null, 但是mSavedViewState却有值. 所以这个View状态保存和恢复对应的入口即是上面两个图中的入口三.
这是因为我的两个fragment只new了一次, 然后保存了成员变量, 即便是Fragment重新onCreate(), 但是对应的实例仍然是同一个. 这和Activity是不同的, 因为你是无法new一个Activity的.
在上面的例子中, 如果不保存Fragment的引用, 每次都new Fragment, 那么View的状态是不会被保存的, 因为不同实例间的状态传递只有在系统销毁恢复的情况下才会发生(入口一). 如果我们需要在不同的实例间传递状态, 就需要用到下面的方法.
这里对应的是入口2, 不同于入口1和3, 它们是自动的, 入口2是用户主动保存和恢复的情形. 自己主动保存Fragment的状态, 可以调用FragmentManager的这个方法:
public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f);它的实现是这样的:
@Overridepublic Fragment.SavedState saveFragmentInstanceState(Fragment fragment) { if (fragment.mIndex < 0) { throwException(new IllegalStateException("Fragment " + fragment + " is not currently in the FragmentManager")); } if (fragment.mState > Fragment.INITIALIZING) { Bundle result = saveFragmentBasicState(fragment); return result != null ? new Fragment.SavedState(result) : null; } return null; }返回的数据类型是: Fragment.SavedState, 这个state可以通过Fragment的这个方法设置给自己:
public void setInitialSavedState(SavedState state) { if (mIndex >= 0) { throw new IllegalStateException("Fragment already active"); } mSavedFragmentState = state != null && state.mState != null ? state.mState : null; }但是注意只能在Fragment被加入之前设置, 这是一个初始状态. 利用这两个方法可以更加自由地保存和恢复状态, 而不依赖于Activity. 这样处理以后, 不必保存Fragment的引用, 每次切换的时候虽然都new了新的实例, 但是旧的实例的状态可以设置给新实例.
例子代码:
@State SparseArray<Fragment.SavedState> savedStateSparseArray = new SparseArray<>(); void onTab1Clicked() { // save current tab Fragment tab2Fragment = getSupportFragmentManager().findFragmentByTag(Tab2Fragment.TAG); if (tab2Fragment != null) { saveFragmentState(1, tab2Fragment); } // restore last state Tab1Fragment tab1Fragment = new Tab1Fragment(); restoreFragmentState(0, tab1Fragment); // show new tabgetSupportFragmentManager().beginTransaction() .replace(R.id.content_container, tab1Fragment, Tab1Fragment.TAG) .commit(); } private void saveFragmentState(int index, Fragment fragment) { Fragment.SavedState savedState = getSupportFragmentManager().saveFragmentInstanceState(fragment); savedStateSparseArray.put(index, savedState); } private void restoreFragmentState(int index, Fragment fragment) { Fragment.SavedState savedState = savedStateSparseArray.get(index); fragment.setInitialSavedState(savedState); }注意这里用了SparseArray来存储Fragment的状态, 并且加上了@State, 这样在Activity重建的时候其中的内容也能够被恢复.
有一点很特殊的是, 当Fragment从back stack中返回, 实际上是经历了一次View的销毁和重建, 但是它本身并没有被重建. 即View状态需要重建, 实例状态不需要重建.
举个例子说明这种情形: Fragment被另一个Fragment replace(), 并且压入back stack中, 此时它的View是被销毁的, 但是它本身并没有被销毁. 也即, 它走到了onDestroyView(), 却没有走onDestroy()和onDetact(). 等back回来的时候, 它的view会被重建, 重新从onCreateView()开始走生命周期. 在这整个过程中, 该Fragment中的成员变量是保持不变的, 只有View会被重新创建. 在这个过程中, instance state的saving并没有发生.
所以, 很多时候Fragment还需要考虑的是在没有Activity帮助的情形下(Activity并没有可能重建的情形), 自身View状态的保存. 此时要注意一些不容易发现的错误, 比如List的新实例需要重新setAdapter等.
Fragment有一个相关方法: setRetainInstance 这个方法设置为true的时候表示, 即便activity重建了, 但是fragment的实例并不被重建. 注意此方法只对没有放在back stack中的fragment生效. 什么时候要用这个方法呢? 处理configuration change的时候: Handling Configuration Changes with Fragments 这样, 当屏幕旋转, Activity重建, 但是其中的fragment和fragment正在执行的任务不必重建. 更多解释可以参见: http://stackoverflow.com/questions/11182180/understanding-fragments-setretaininstanceboolean http://stackoverflow.com/questions/11160412/why-use-fragmentsetretaininstanceboolean
注意这个方法只是针对configuration change, 并不影响用户主动关闭和系统销毁的情况: 当activity被用户主动finish, 其中的所有fragments仍然会被销毁. 当activity不在最顶端, memory不够了, 系统仍然可能会销毁activity和其中的fragments.
View的状态保存和恢复主要是依赖于下面几个方法: 保存: saveHierarchyState() -> dispatchSaveInstanceState() -> onSaveInstanceState() 恢复: restoreHierarchyState() -> dispatchRestoreInstanceState() -> onRestoreInstanceState() 还有两个重要的前提条件是View要有id, 并且setSavedEnabled()为true.(这个值默认为true). 在系统的widget里(比如TextView, EditText, Checkbox等), 这些都是已经被处理好的, 我们只需要给View赋予id, Activity和Fragment重建的时候会自动恢复其中的状态. (这里的Fragment恢复对应入口一和入口三, 入口二属于跨实例新建的情况).
但是如果你要使用第三方的自定义View, 就需要确认一下它们内部是否有状态保存和恢复的代码. 如果不行你就需要继承该自定义View, 然后实现这两个方法:
//// Assumes that SomeSmartButton is a 3rd Party view that// View State Saving/Restoring are not implemented internally//public class SomeBetterSmartButton extends SomeSmartButton { ... @Overridepublic Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); // Save current View's state herereturn bundle; } @Overridepublic void onRestoreInstanceState(Parcelable state) { super.onRestoreInstanceState(state); // Restore View's state here } ... }WebView的状态保存和恢复不像其他原生View一样是自动完成的. WebView不是继承自View的. 如果我们把WebView放在布局里, 不加处理, 那么Activity或Fragment重建的过程中, WebView的状态就会丢失, 变成初始状态.
在Fragment的onSaveInstanceState()里面可以加入如下代码来保存WebView的状态:
@Overridepublic void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); webView.saveState(outState); }然后在初始化的时候, 增加判断, 不必每次都打开初始链接:
if (savedInstanceState != null) { webView.restoreState(savedInstanceState); } else { webView.loadUrl(TEST_URL); }这样处理以后, 在重新建立的时候, WebView的状态就能恢复到离开前的页面. 不论WebView是放在Activity里还是Fragment里, 这个方法都适用.
但是Fragment还有另一种情况, 即Fragment被压入back stack, 此时它没有被destroy(), 所以没有调用onSavedInstanceState()这个方法. 这种情况返回的时候, 会从onCreateView()开始, 并且savedInstanceState为null, 于是其中WebView之前的状态在此时丢失了. 解决这种情况可以利用Fragment实例并未销毁的条件, 增加一个成员变量bundle, 保存WebView的状态, 最终解决如下:
private Bundle webViewState; @Overridepublic void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); initWebView(); if (webViewState != null) { //Fragment实例并未被销毁, 重新create view webView.restoreState(webViewState); } else if (savedInstanceState != null) { //Fragment实例被销毁重建 webView.restoreState(savedInstanceState); } else { //全新Fragment webView.loadUrl(TEST_URL); } } @Overridepublic void onPause() { super.onPause(); webView.onPause(); //Fragment不被销毁(Fragment被加入back stack)的情况下, 依靠Fragment中的成员变量保存WebView状态 webViewState = new Bundle(); webView.saveState(webViewState); } @Overridepublic void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); //Fragment被销毁的情况, 依靠outState保存WebView状态if (webView != null) { webView.saveState(outState); } } -------------------------------------------------------------------------------------------------Android的ActionBar每个版本都会做一些改变, 所以原生的ActionBar在不同的系统上看起来可能会不一样. 使用support library版本的Toolbar可以让你的应用在多种设备类型上保持一致. support library中总是包含了最新的features. Android从5.0 (API Level 21)开始提供Material Design, 使用v7版本的Toolbar后, 在任何Android 2.1(API Level 7)以上的机器上都可以看到Material Design风格的Toolbar.
1.首先项目gradle中添加:
compile 'com.android.support:appcompat-v7:23.4.0'2.确保Activity继承AppCompatActivity 3.在application设置中使用NoActionBar的主题:
<application android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>4.把Toolbar写在布局中
<android.support.v7.widget.Toolbar android:id="@+id/my_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="4dp" android:theme="@style/ThemeOverlay.AppCompat.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>5.在Activity里面把Toolbar设置成为ActionBar 首先把Toolbar find出来, 然后调用setSupportActionBar方法 把Toolbar设置为自己的ActionBar即可.
public class ToolbarDemoActivity extends AppCompatActivity { @BindView(R.id.toolbar) Toolbar toolbar; @Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_toolbar_demo); ButterKnife.bind(this); setSupportActionBar(toolbar); } }然后就可以随意使用啦, 用getSupportActionBar可以获取ActionBar类型的对象, 从而使用ActionBar的方法.
定义menu:
<?xml version="1.0" encoding="utf-8"?><menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"><item android:id="@+id/action_android" android:icon="@drawable/ic_android_black_24dp" android:title="@string/action_android" app:showAsAction="always" /><item android:id="@+id/action_favourite" android:icon="@drawable/ic_favorite_black_24dp" android:title="@string/action_favourite" app:showAsAction="ifRoom" /><item android:id="@+id/action_settings" android:title="@string/action_settings" app:showAsAction="never" /></menu>然后在代码中inflate和处理它的点击事件:
@Overridepublic boolean onCreateOptionsMenu(Menu menu) { Log.i(TAG, "onCreateOptionsMenu()"); getMenuInflater().inflate(R.menu.menu_activity_main, menu); return super.onCreateOptionsMenu(menu); } @Overridepublic boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_android: Log.i(TAG, "action android selected"); return true; case R.id.action_favourite: Log.i(TAG, "action favourite selected"); return true; case R.id.action_settings: Log.i(TAG, "action settings selected"); return true; default: return super.onOptionsItemSelected(item); } }添加向上返回parent的action:
@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_toolbar_demo); ButterKnife.bind(this); setSupportActionBar(toolbar); // add a left arrow to back to parent activity,// no need to handle action selected event, this is handled by supergetSupportActionBar().setDisplayHomeAsUpEnabled(true); }然后只需要在manifest中指定parent:
<activity android:name=".toolbar.ToolbarDemoActivity" android:parentActivityName=".MainActivity"></activity>在Fragment中使用Toolbar的步骤和Activity差不多. 在Fragment布局中添加一个Toolbar, 然后find它, 然后调用Activity的方法来把它设置成ActionBar:
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);注意此处有一个强转, 必须是AppCompatActivity才有这个方法. 但是此时运行到Fragment之后, 发现Toolbar上的文字和按钮全是Activity传过来的, 这是因为只有Activity的onCreateOptionsMenu()被调用了, 但是Fragment的并没有被调用. 在Fragment中加上这句:
setHasOptionsMenu(true);此时Fragment的onCreateOptionsMenu()回调会被调到了, 但是inflate出的按钮和Activity中的actions加在一起显示出来了. 因为Activity的onCreateOptionsMenu()会在之前调用到. 于是Fragment中的写成这样:
@Overridepublic void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { Log.e(TAG, "onCreateOptionsMenu()"); menu.clear(); inflater.inflate(R.menu.menu_parent_fragment, menu); }即先clear()一下, 这样按钮就只有Fragment中设置的自己的了, 不会有Activity中的按钮.
前面已经介绍过, Fragment可以嵌套使用: Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常见错误. 那么在前面的Fragment中再显示一个子Fragment, 并且又带有一个不一样的Toolbar, 还需要哪些处理呢? 首先, java代码中还是需要有:
setHasOptionsMenu(true) ((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);然后根据是否需要菜单按钮, 覆写onCreateOptionsMenu()方法来inflate自己的menu文件即可. 感觉和在普通的Fragment中使用Toolbar作为ActionBar并没有什么区别. 但是如果你的多个Fragment有不同的Toolbar菜单选项, 如果你没有懂得其中的原理, 可能就会出现一些混乱. 下面来解说一下相关的方法.
一旦调用
((AppCompatActivity) getActivity()).setSupportActionBar(toolbar);就会导致ActivityonCreateOptionsMenu()方法的调用, 而Activity会根据其中Fragment是否设置了setHasOptionsMenu(true)来调用Fragment的 onCreateOptionsMenu()方法, 调用顺序是树形的, 按层级调用, 中间如果有false则跳过.
假设当前Activity, Parent Fragment和Child Fragment中都设置了自己的Toolbar为ActionBar. 在打开Child fragment的时候, onCreateOptionsMenu()的调用顺序是. Activity -> Parent -> Child. 此时parent和child fragment都设置了setHasOptionsMenu(true).
关于这个, 还有以下几种情况:
- 如果Parent的`setHasOptionsMenu(false)`, Child为true, 则Parent的`onCreateOptionsMenu()`不会调用, 打开Child的时候Activity -> Child. - 如果Child的`setHasOptionsMenu(false)`, Parent为true, 则打开Child的时候仍然会调用Activity和Parent的onCreateOptionsMenu()方法. - 如果Parent和Child都置为false, 打开Parent和Child Fragment的时候都会调用Activity的onCreateOptionsMenu()方法.仅仅是child Fragment的show() hide()的切换, activity和parent Fragment的onCreateOptionsMenu()也会重新进入. 这一点我还没有想明白, 是项目中遇到的, 初步推测可能是menu的显隐变化invalidate了menu, 改天有空再试试.
上面的机制常常是导致Toolbar上面的按钮混淆错乱的原因. 举个例子: 如果我们现在Activity和Parent Fragment有不同的Toolbar按钮, 但是Child只有文字, 没有按钮. 很显然我们不需要给child写menu文件, 也不需要覆写child里的onCreateOptionsMenu()方法. 但是此时不管怎样, parent的onCreateOptionsMenu()方法都会被调用, 这样我们打开child的时候, toolbar上就神奇地出现了parent里的按钮. 这种情况如何解决呢? 可以在parent中加一个条件, 当没有child fragment的时候才做inflate的工作:
@Overridepublic void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { Log.e(TAG, "onCreateOptionsMenu()"); menu.clear(); if (getChildFragmentManager().getBackStackEntryCount() == 0) { inflater.inflate(R.menu.menu_parent_fragment, menu); } }另外, 除了setSupportActionBar()之外, 如果我们想主动触发 onCreateOptionsMenu()方法的调用, 可以用 invalidateOptionsMenu()方法.
在Activity和其中的Fragment都有options menu的时候, 需要注意menu item的id不要重复. 以为点击事件的分发也是从Activity开始分发下去的, 如果child fragment中有个选项的id和Activity中一个选项的id重复了, 则在Activity中就会将其处理, 不会继续分发.
之前没有嵌套Fragment的情况下, 只要将Fragment加入到Back Stack中, 那么按下Back键的时候pop动作是系统自动做好的. 虽然在添加child fragment的时候将其加入到back stack中, 但是按back键的时候仍然是将parent fragment弹出, 只剩下Activity. 这是因为back键只检查第一层Fragment的back stack, 对于child fragment, 需要在其parent中自己处理. 比如这样处理:
在Activity中
@Overridepublic void onBackPressed() { Fragment fragment = getSupportFragmentManager().findFragmentById(android.R.id.content); if (fragment instanceof ToolbarFragment) { if (((ToolbarFragment) fragment).onBackPressed()) { return; } } super.onBackPressed(); }其中ToolbarFragment是直接加在Activity中作为parent fragment的. 在parent fragment中(即ToolbarFragment中):
public boolean onBackPressed() { return getChildFragmentManager().popBackStackImmediate(); } ------------------------------------------------------------------------------------------------- 参考资料: CodePath Guides: Creating and Using Fragments Guide: Nested Fragments 从源码角度剖析Fragment核心知识点 Fragment源码阅读笔记 Android中保存和恢复Fragment状态的最好方法 WebView的状态保存和恢复 The Real Best Practices to Save/Restore Activity's and Fragment's state