Android中FragmentPagerAdapter對Fragment的緩存(二)


上一篇我們談到了,當應用程序恢復時,由於FragmentPagerAdapter對Fragment進行了緩存的讀取,導致其並未使用在Activity中新創建的Fragment實例。今天我們來看如何解決這種情況。

 根據上篇Blog的描述,我們不難發現,目前需要解決的問題有以下兩個:

 1. 緩存Fragment內部成員變量缺失的問題。

 2. 新Fragment的創建和緩存Fragment使用之間的矛盾。

 下面先來解決第一個問題,緩存Fragment內部成員變量缺失。上篇Blog中,Fragment當中,有一個成員變量mText,是通過setter的方式在創建Fragment之初設置進去的。但是在經歷了一系列的存儲和恢復操作過后,其值在最終卻為空,導致了程序展示的異常。那么能不能讓mText也在Fragment中同步緩存和恢復呢?

 最先能想到的方法,就是通過Fragment的onSaveInstanceState方法在進程被殺掉時存儲,當恢復時通過onCreateView的savedInstanceState參數取出;代碼如下:

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
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去獲取這些參數。這就保證了在恢復過后,我們需要傳入的參數可以重新被設置。一番改造之后如下:

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
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里面去手動保存了。

[代碼]java代碼:

?
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代碼如下:

[代碼]java代碼:

?
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方法,如下:

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
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的代碼如下:

[代碼]java代碼:

?
01
02
03
04
05
06
07
08
09
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緩存的分析就是這么多了,歡迎指正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM