上一篇我们谈到了,当应用程序恢复时,由于FragmentPagerAdapter对Fragment进行了缓存的读取,导致其并未使用在Activity中新创建的Fragment实例。今天我们来看如何解决这种情况。
根据上篇Blog 的描述,我们不难发现,目前需要解决的问题有以下两个:
1. 缓存Fragment内部成员变量缺失的问题。
2. 新Fragment的创建和缓存Fragment使用之间的矛盾。
下面先来解决第一个问题,缓存Fragment内部成员变量缺失。上篇Blog 中,Fragment当中,有一个成员变量mText,是通过setter的方式在创建Fragment之初设置进去的。但是在经历了一系列的存储和恢复操作过后,其值在最终却为空,导致了程序展示的异常。那么能不能让mText也在Fragment中同步缓存和恢复呢?
最先能想到的方法,就是通过Fragment的onSaveInstanceState方法在进程被杀掉时存储,当恢复时通过onCreateView的savedInstanceState参数取出;代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public View onCreateView ( LayoutInflater inflater , ViewGroup container , Bundle savedInstanceState ) {
...
if ( savedInstanceState != null ) {
mText = savedInstanceState . getString ( SAVED_KEY_TEXT );
}
...
}
@Override
public void onSaveInstanceState ( Bundle outState ) {
super . onSaveInstanceState ( outState );
outState . putString ( SAVED_KEY_TEXT , mText );
}
这种Activity和Fragment通用的方法,无疑是应用被杀掉时我们存储数据比较好的选择。不过还有其他方式吗?
目前,mText是通过setter向Fragment设置的,这样做从实现来讲没有问题,不过其实并不是Android官方文档推荐的最佳实践; 官方文档上不推荐使用setter或者重写默认构造器的方式来传递参数:
It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments().
原因是,当Fragment重新被恢复时,不会去重新调用这些setter/有参构造方法; 而是会调用onCreateView,我们却可以在其中重新调用getArguments去获取这些参数。这就保证了在恢复过后,我们需要传入的参数可以重新被设置。一番改造之后如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TestFragment fragmentOne = new TestFragment ();
Bundle bundleOne = new Bundle ();
bundleOne . putString ( TestFragment . PARAM_KEY_TEXT , "One" );
fragmentOne . setArguments ( bundleOne );
TestFragment fragmentTwo = new TestFragment ();
Bundle bundleTwo = new Bundle ();
bundleTwo . putString ( TestFragment . PARAM_KEY_TEXT , "Two" );
fragmentTwo . setArguments ( bundleTwo );
TestFragment fragmentThree = new TestFragment ();
Bundle bundleThree = new Bundle ();
bundleThree . putString ( TestFragment . PARAM_KEY_TEXT , "Three" );
fragmentThree . setArguments ( bundleThree );
这样传入的参数,就不需要在onSaveInstanceState里面去手动保存了。
1
2
3
4
5
6
7
public View onCreateView ( LayoutInflater inflater , ViewGroup container , Bundle savedInstanceState ) {
View view = inflater . inflate ( R . layout . fragment_test , container , false );
TextView textView = ( TextView ) view . findViewById ( R . id . center_text_view );
mText = ( getArguments () != null ) ? getArguments (). getString ( PARAM_KEY_TEXT ) : "" ;
textView . setText ( mText );
return view ;
}
第一个问题到这里就处理好了,接下来看看第二个问题:怎样解决onCreate中新实例化的Fragment,与Adapter中FragmentManager中取出的Fragment不一致的冲突。
虽然mText找回来了,但是如果我们需要对Activity中实例化的Fragment做一些进一步的操作,比如传入一些Listener之类的事情,就会遇到一些麻烦,因为毕竟我们处理的这些Fragment,实际上并不是当前展示在屏幕上的Fragment。
上篇Blog 中讲到,FragmentPagerAdapter使用container.getId()与getItemId拼接的字符串作为FragmentManager中缓存的Key,FragmentPagerAdapter代码如下:
1
2
3
4
5
6
7
8
String name = makeFragmentName ( container . getId (), itemId );
Fragment fragment = mFragmentManager . findFragmentByTag ( name );
...
private static String makeFragmentName ( int viewId , long id ) {
return "android:switcher:" + viewId + ":" + id ;
}
从上面的代码来看,其实要避免缓存和新创建的Fragment不一致,最简单的方式是,通过重写getItemId()方法,让每次打开应用返回不同的值(比如随机数之内的),让FragmentPagerAdapter找不到之前的缓存,就会使用我们新传入的实例了。
不过这样做,看起来既不优雅,也不靠谱。毕竟Android官方给我们提供了这样一种缓存机制,那我们还是应该考虑怎样利用才好。
1. 既然有缓存,那我们不必在Activity中每次都去新创建Fragment实例了。从源码中可以看出,每次如果FragmentPagerAdapter需要新实例化Fragment的话,都回去调用getItem方法,所以,可以考虑把Fragment的实例化工作放到getItem当中去。
2. 考虑到后面我们会使用到这些Fragment实例,可以考虑在instantiateItem当中去获取并存放在数组当中。这里选择到instantiateItem,而不是getItem方法中去取的原因是:如果一旦出现有缓存的情况,FragmentPagerAdapter并不会调用getItem方法,如下:
1
2
3
4
5
6
7
8
9
10
11
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 ));
}
按照上面两点想法,经过改造的Adapter的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class CustomPagerAdapter extends FragmentPagerAdapter {
private static final int COUNT = 3 ;
private Fragment [] mFragments ;
private Context mContext ;
public CustomPagerAdapter ( Context context , FragmentManager fm ) {
super ( fm );
this . mContext = context ;
this . mFragments = new Fragment [ COUNT ];
}
@Override
public Fragment getItem ( int position ) {
String text ;
switch ( position ) {
case 0 :
text = "One" ;
break ;
case 1 :
text = "Two" ;
break ;
case 2 :
text = "Three" ;
break ;
default :
text = "" ;
}
Bundle bundle = new Bundle ();
bundle . putString ( TestFragment . PARAM_KEY_TEXT , text );
return Fragment . instantiate ( mContext , TestFragment . class . getName (), bundle );
}
@Override
public int getCount () {
return COUNT ;
}
@Override
public long getItemId ( int position ) {
return position ;
}
@Override
public Object instantiateItem ( ViewGroup container , int position ) {
Fragment fragment = ( Fragment ) super . instantiateItem ( container , position );
mFragments [ position ] = fragment ;
return fragment ;
}
public Fragment [] getFragments () {
return mFragments ;
}
}
有一点需要注意的是,mFragment数组需要在每个页面都实例化好了之后才会填充完成,需要注意调用的时机。
FragmentPagerAdapter对Fragment缓存的分析就是这么多了,欢迎指正。