一、簡述
不管AbsListView(ListView、GridView)或是新出的RecyclerView,在使用notifyDataSetChanged方法更新列表數據時,一定要保證數據為同個對象(即hashCode要一致)。對於這個問題的論證,可以去看官方源代碼,或是看我之前寫的一篇博文“解決ListViews適配器notifyDataSetChanged()無效問題”,相信可以幫到你。但是,這個不是本文的重點,本文重點講解在Fragment中,RecyclerView遇到notifyDataSetChanged無效的問題。如果你趕時間,可以直接看第三部分(”總結”)。
二、探索
1、查看數據(mData)是否是同個對象
*tip:java中可以通過打印hashCode的方式判斷mData是否為同個對象。
注意:initData方法在onActivityCreated()中被調用。
public void initData() { if (mData == null) { mData = new ArrayList<>(); } mData.clear(); ... 數據填充 ... if (mAdapter == null) { mAdapter = new LQRAdapterForRecyclerView<String>(getActivity(), mData, R.layout.item_senior) { @Override public void convert(LQRViewHolderForRecyclerView helper, String item, int position) { ... 視圖填充 ... } }; mRvList.setAdapter(mAdapter); LogUtils.sf("setAdapter時mData地址:" + mData.hashCode()); } else { mAdapter.notifyDataSetChanged(); LogUtils.sf("setAdapter時mData地址:" + mData.hashCode()); } }
2、操作與結果
*tip:常規對Fragment的使用,會對其進行緩存,也可能使用單例模式,反正就是短時間內不會重新創建。
①操作一:
打開Activity后,切換Fragment(第一次初始化Fragment)。顯示效果如下:
②操作二:
切換別的Fragment后,再切回剛才的Fragment(此前該Fragment已經在存在,所以不會再次創建)。顯示效果如下:
③看控制台:
可以看到數據對象地址一樣,即為同一個。
3、查看RecyclerView是否是同個對象
說實話,這個是踩坑經驗豐富的網友在群里說的,如果不是他說出來,打死我也沒想到,居然還有這么一個坑。從上面的結果可以看出,adapter中是有數據的沒錯,而且數據地址沒變,所以理應notifyDataSetChanged()方法會生效。但是為什么會這樣呢,這里先賣個關子,先看下面的操作。
①改下上面的代碼,打印RecyclerView的地址。
代碼如下:
public void initData() { if (mData == null) { mData = new ArrayList<>(); } mData.clear(); ... 數據填充 ... if (mAdapter == null) { mAdapter = new LQRAdapterForRecyclerView<String>(getActivity(), mData, R.layout.item_senior) { @Override public void convert(LQRViewHolderForRecyclerView helper, String item, int position) { ... 視圖填充 ... } }; mRvList.setAdapter(mAdapter); LogUtils.sf("setAdapter時Rv:" + mRvList.hashCode()); } else { mAdapter.notifyDataSetChanged(); LogUtils.sf("notify時Rv:" + mRvList.hashCode()); } }
②同上述操作一致。
對同一個Fragment來回切換,看控制台輸出。
果然不一樣!!!
三、總結
為什么在Fragment中RecyclerView的地址會發生變化呢?我們先理清一下Fragment生命周期會陸續調用的幾個方法:
onCreate() -> onCreateView() -> onActivityCreated() -> onDestroy()
中間少了幾個方法,請不用在意,下面貼下我的BaseFragment代碼:
public abstract class BaseFragment extends Fragment { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); init(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //子類不再需要設置布局ID,也不再需要使用ButterKnife.bind() View rootView = inflater.inflate(provideContentViewId(), container, false); ButterKnife.bind(this, rootView); initView(rootView); return rootView; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initData(); initListener(); } @Override public void onDestroy() { super.onDestroy(); } }
當一個Fragment在來回切換時,分別調用的方法如下:
第一次顯示:
onCreate() -> onCreateView() -> onActivityCreated()
第二次顯示:
onCreateView() -> onActivityCreated()
這里不難理解,因為Fragment一般使用的時候會被緩存,所以,當第二次顯示的時候,不會調用onCreate()。只會調用onCreateView()和onActivityCreated(),這也就是RecyclerView地址不一樣的原因所在,因為控件獲取操作是在initView()中進行的,即RecyclerView的獲取操作在onCreateView()中,而Fragment的每次顯示都會調用onCreateView(),所以RecyclerView控件會被再次獲取,即重新創建一個對象(此時hashCode就變化了)。
1、結論:
所以,在Fragment中使用RecyclerView或AbsListView控件的notifyDataSetChanged()方法時,除了保證數據(mData對象)不能變以外,控件本身一樣也不能變。
2、解決方案:
1)方案一:
因為Fragment的onCreateView()和onActivityCreated()方法在每次Fragment顯示的時候會被調用,控件會被重新創建一次,所以,解決方法只能是在這兩個方法中重新對RecyclerView設置適配器,而不要使用notifyDataSetChanged(),故代碼改為如下:
public void initData() { if (mData == null) { mData = new ArrayList<>(); } mData.clear(); ... 數據填充 ... if (mAdapter == null) { mAdapter = new LQRAdapterForRecyclerView<String>(getActivity(), mData, R.layout.item_senior) { @Override public void convert(LQRViewHolderForRecyclerView helper, String item, int position) { ... 視圖填充 ... } }; } mRvList.setAdapter(mAdapter); }
注:只是建議不要在上述兩個生命周期方法中使用notifyDataSetChanged()而已,只要在保證RecyclerView等列表控件設置完適配器后,可以在任意地方繼續使用notifyDataSetChanged()。
2)方案二:
讓rootView作為全局變量,在回調onCreateView()時不再重新創建。
public abstract class BaseFragment extends Fragment { View rootView; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); init(); } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { //子類不再需要設置布局ID,也不再需要使用ButterKnife.bind() if(rootView == null){ rootView = inflater.inflate(provideContentViewId(), container, false); ButterKnife.bind(this, rootView); initView(rootView); } return rootView; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); initData(); initListener(); } @Override public void onDestroy() { super.onDestroy(); } }