我们利用Android studio新建fragment的时候,利用谷歌提供的模版,可以看到,新建一个fragment时,fragment的初始化,采用的是静态工厂的形式,具体代码如下:
public class BlankFragment extends Fragment {
// TODO: Rename parameter arguments, choosenames that match
// the fragment initialization parameters,e.g. ARG_ITEM_NUMBER
private static final String ARG_PARAM1 = "param1";
private static final String ARG_PARAM2 = "param2";
// TODO: Rename and change types of parameters
private String mParam1;
private String mParam2;
public BlankFragment() {
// Required empty public constructor
}
public static BlankFragment newInstance(String param1, String param2) {
BlankFragment fragment = new BlankFragment();
Bundle args = new Bundle();
args.putString(ARG_PARAM1, param1);
args.putString(ARG_PARAM2, param2);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mParam1 = getArguments().getString(ARG_PARAM1);
mParam2 = getArguments().getString(ARG_PARAM2);
}
}
@Override
public View onCreateView(LayoutInflater inflater,ViewGroup container,
BundlesavedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_blank, container, false);
}
}
从上述代码可以看到,默认的fragment是利用静态工厂的方式对fragment进行初始化的,传入两个参数,但是会有部分人会采用new BlankFragment()的形式对fragment进行初始化,包括我之前也是这么做的,那么,采用new BlankFragment()和Fragment.newInstance()的方式具体有什么不同,下面提供一个小例子来进行说明,上代码:
首先是activity中的布局如下:
上下排版两个layout,用来摆放fragment,上面的fragment通过new Fragement()的方式创建
代码如下:
@SuppressLint("ValidFragment")
public class TopFragment extends Fragment {
private String text = "默认的文字";
public TopFragment() {
}
public TopFragment(String text) {
this.text = text;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment, container, false);
TextView textView = (TextView) view.findViewById(R.id.tv_content);
textView.setText(text);
return view;
}
}
下面的fragment通过Fragment.newInstance()的方式创建,代码如下:
public class BottomFragment extends Fragment {
private static final String PARAM = "param";
private String mText = "默认的文字";
public static BottomFragment newInstance(String text) {
Bundle args = new Bundle();
args.putString(PARAM, text);
BottomFragment fragment = new BottomFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mText = getArguments().getString(PARAM);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment, container, false);
TextView textView = (TextView) view.findViewById(R.id.tv_content);
textView.setText(mText);
return view;
}
}
两个fragment在activity中的调用如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getFragmentManager().beginTransaction().add(R.id.layout_top, new TopFragment("传进来的文字")).commit();
getFragmentManager().beginTransaction().add(R.id.layout_bottom, BottomFragment.newInstance("传进来的文字")).commit();
}
}
下面就是见证奇迹的时刻:运行,上图
咦,貌似没什么问题啊,你特么在逗我?别急,我们把屏幕横过来,再来看:继续上运行图:
看出来差别了吗?
上面那个fragment,通过new Fragment()的方式进行初始化的,它挂掉了,工作不正常了,文字恢复成默认的了,而下面通过Fragment.newInstance()进行初始化的依然坚挺!
好了,下面来分析原因,我们知道,activity在默认情况下,切换横竖屏,activity会销毁重建,依附于上面的fragment也会销毁重建,根据这个思路,我们找到fragment重建时调用的代码:
public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
try {
Class> clazz = sClassMap.get(fname);
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
if (!Fragment.class.isAssignableFrom(clazz)) {
throw new InstantiationException("Trying to instantiate a class " + fname
+ " that is not a Fragment", new ClassCastException());
}
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment) clazz.getConstructor().newInstance();
if (args != null) {
args.setClassLoader(f.getClass().getClassLoader());
f.setArguments(args);
}
return f;
} catch (ClassNotFoundException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (java.lang.InstantiationException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (IllegalAccessException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": make sure class name exists, is public, and has an"
+ " empty constructor that is public", e);
} catch (NoSuchMethodException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": could not find Fragment constructor", e);
} catch (InvocationTargetException e) {
throw new InstantiationException("Unable to instantiate fragment " + fname
+ ": calling Fragment constructor caused an exception", e);
}
}
通过以上代码,我们可以看到,fragment是通过反射进行重建的,而且,只调用了无参构造的方法,这也是有部分人通过new Fragment()的方式构建fragment时,遇到屏幕切换时,fragment会报空指针异常的原因!注意看代码中f.setArguments(args);
也就是说,fragment在初始化之后会把参数保存在arguments中,当fragment再次重建的时候,它会检查arguments中是否有参数存在,如果有,则拿出来再用,所以我们再onCreate()方法里面才可以拿到之前设置的参数,但是:fragment在重建的时候不会调用有参构造,所以,通过new Fragment()的方法来初始化,fragment重建时,我们设置的参数就没有了
一般的,我们在代码中请求网络数据后,由于是在子线程中得到的结果,更新UI界面时,要在UI线程中进行,如果是在fragment中,则需要执行以下代码:
// 在UI线程中展示吐司
private void showToast(String text) {
getActivity().runOnUiThread(() ->
Toast.makeText(getActivity(),text,Toast.LENGTH_SHORT).show()
);
}
乍一看,代码貌似没什么问题,运行起来也能正常显示,但是这样写,在系统可用内存较低时,会频繁触发crash(尤其在低内存手机上经常遇到),此时,再执行这样的代码,就会报空指针异常了,下面来分析具体原因:
首先,我们来看一下fragment的声明周期:
重点关注onAttach()方法和onDetach():
当执行onAttach()时,Fragment已实现与Activity的绑定,在此方法之后调用getActivity()会得到与次Fragment绑定的activity对象;当可用内存过低时,系统会回收Fragment所依附的activity,ye'jiu'sh的onDetach()时,Fragment已实现与Activity解绑,在此方法之后调用getActivity(),由于Fragment已经与Activity解绑,皮之不存毛将焉附?则系统就会返回空指针了。
ok,搞定了具体原因后,再来分析解决办法就变得容易了,我们可以在onAttach()后的任意一个方法执行时,比如onAttach()时,保存一份activity为全局属性,这样一来,下次调用getActivity()时,直接使用我们保存的全局mActivity来代替即可,代码如下:
private Activity mActivity;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mActivity = activity;
}
// 在UI线程中展示吐司
private void showToast(String text) {
mActivity.runOnUiThread(() ->
Toast.makeText(mAcitivty, text, Toast.LENGTH_SHORT).show()
);
}
不过,此方法有个小问题:当系统调用onAttach()时,Fragment与Activity已经分离,此时Fragment定然对用户不可见的,既然不可见,还更新界面做什么呢?岂不白白浪费资源嘛?这个问题,我们再第三节中进行分析解决
首先,来看Fragment的源码:
/**
* Called when the fragment is visible to the user and actively running.
* This is generally
* tied to {@link Activity#onResume() Activity.onResume} of the containing
* Activity's lifecycle.
*/
public void onResume() {
mCalled = true;
}
/**
* Called when the Fragment is no longer resumed. This is generally
* tied to {@link Activity#onPause() Activity.onPause} of the containing
* Activity's lifecycle.
*/
public void onPause() {
mCalled = true;
}
注意看方法上面的注释,调用两个方法,返回的是此Fragment所依附的Activity的声明周期中的onResume()和onPause(),并不是Fragment自身的onResume()和onPause(),那么,如果我们也想实现类似Acitivity的onResume()和onPause(),应该怎么做呢?我们继续翻Fragment的源码,找到这么个方法:
/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = !isVisibleToUser;
}
注释中说的很清楚,此方法是告诉系统,当前Fragment是否对用户可见,其中有一个isVisibleToUser参数,我们可以重写这个方法,通过判断isVisibleToUser的值来确定此Fragment是否对用户可见,从而间接实现onResume()和onPause()的功能,具体代码如下:
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser){
//TODO:执行对用户可见时的代码
}else {
//TODO:执行对用户可不见时的代码
}
}
自从Androidstudio 3.0发布后,kotlin成为Android开发的第一语言,Kotlin由JetBrains公司开发,与Java 100%互通,并具备诸多Java尚不支持的新特性,JetBrains是个拽拽的公司,看不惯eclipse,就开发了Intellij IEDA,感觉eclipse的Android插件不全面,就自己开发了Android studio,不喜欢java的繁琐,就自己搞了kotlin,如果你还记得的话,去年曾有报道称 Google Android 考虑采用苹果的 Swift 语言,而 Swift 就被称为是IOS界的Kotlin,总之:JetBrains公司猛地一塌糊涂,有它的加持,kotlin的发展一路顺风顺水,大有取代java之势!
好了,闲话不多说了,我们来举个栗子吧,上代码:
先看传统的activity中获取控件是怎么写的:
public class MainActivity extends AppCompatActivity {
private TextView mText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mText = (TextView) findViewById(R.id.tv_result);
mText.setText("这里展示结果");
}
}
转换为kotlin后,这样写:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
tv_result.text = "这里展示结果"
}
}
再也不用写该死的findViewById了,控件名字直接拿来用,甚至butterkinfe都下岗了,多方便!这都多亏了kotlin-android-extensions插件,通过这个插件,我们可以直接在kotlin代码中调用xml中的控件,只需要在kotlin代码中引入这么一行就OK:
import kotlinx.android.synthetic.main.activity.*
同样的,我们再Fragment中执行同样的代码:
class ToolsFragment : BaseFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_tools, container, false)
layout_translate.setOnClickListener { startActivity(Intent(mActivity, TranslateActivity::class.java)) }
return view
}
}
运行,咦?挂了,报的什么错,控件找不到?什么鬼,明明跟activity的写法一模一样的啊!
怎么办?继续翻源码呗:
/**
* Called to have the fragment instantiate its user interface view.
* This is optional, and non-graphical fragments can return null (which
* is the default implementation). This will be called between
* {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
*
* If you return a View from here, you will later be called in
* {@link #onDestroyView} when the view is being released.
*
* @param inflater The LayoutInflater object that can be used to inflate
* any views in the fragment,
* @param container If non-null, this is the parent view that the fragment's
* UI should be attached to. The fragment should not add the view itself,
* but this can be used to generate the LayoutParams of the view.
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*
* @return Return the View for the fragment's UI, or null.
*/
@Nullable
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
return null;
}
/**
* Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}
* has returned, but before any saved state has been restored in to the view.
* This gives subclasses a chance to initialize themselves once
* they know their view hierarchy has been completely created. The fragment's
* view hierarchy is not however attached to its parent at this point.
* @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
* @param savedInstanceState If non-null, this fragment is being re-constructed
* from a previous saved state as given here.
*/
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
}
有一个onViewCreated()方法,此方法注释中有讲,当控件加载完成后,立刻调用此方法,这里就需要提到kotlin-android-extensions插件的工作机制了,kotlin-android-extensions在布局文件加载完成后,会生成一个缓存视图,此时我们直接通过控件名字使用控件,其实就是在缓存视图中对控件进行赋值,因此访问速度更快,代码量更少!
那么,在onCreateView()中直接使用控件,为什么就报错了呢?因为在此时,XML布局尚未完全加载到缓存视图中,此时直接使用控件,自然就会报错了,所以,正确的代码应该这么写:
class ToolsFragment : BaseFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.fragment_tools, container, false)
return view
}
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
layout_translate.setOnClickListener {
startActivity(Intent(mActivity,TranslateActivity::class.java))
}
}
}
我们在onViewCreated()后再使用控件,此时,缓存视图已创建完毕并返回给系统,控件就可以正常使用了。
那么,又有人问了,为什么在activity中的onCreateView()中直接使用控件就正常呢?这里涉及到setContentView与LayoutInflater的区别,这里面的机制还是比较复杂的,下次再讲
有时候出于省流量的考虑,或者考虑到性能的关系,我们希望Fragment在对用户可见时,再进行页面加载以及相关逻辑的运行,此时,就需要考虑Fragment的懒加载了,然而,系统并没有给我们提供这样的一个工具,这就需要我们自己来进行实现了,其实我们完全可以参考第三部分的思路来实现,写法如下(kotlin代码):
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if(isVisibleToUser){
onFragmentResume()
}else{
onFragmentPause()
}
}
open protected fun onFragmentResume() {
}
open protected fun onFragmentPause() {
}
然而,当我们真正编译执行后,会发现,系统直接报空指针了——控件找不到,什么情况呢?继续翻源码,注意这么一句话:
/**
* Set a hint to the system about whether this fragment's UI is currently visible
* to the user. This hint defaults to true and is persistent across fragment instance
* state save and restore.
*
* An app may set this to false to indicate that the fragment's UI is
* scrolled out of visibility or is otherwise not directly visible to the user.
* This may be used by the system to prioritize operations such as fragment lifecycle updates
* or loader ordering behavior.
*
* Note: This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
This method may be called outside of the fragment lifecycle.
* and thus has no ordering guarantees with regard to fragment lifecycle method calls.
*
* @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
* false if it is not.
*/
public void setUserVisibleHint(boolean isVisibleToUser) {
if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
&& mFragmentManager != null && isAdded()) {
mFragmentManager.performPendingDeferredStart(this);
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
}
注意看注释:This method may be called outside of the fragment lifecycle. and thus has no ordering guarantees with regard to fragment lifecycle method calls,就是说,不能保证这个方法在声明周期中的顺序,那么,我们打印一下LOG(具体LOG这里就不展示了),可以看到,在Fragment首次初始化的时候,setUserVisibleHint()方法是在onCreate()之前调用的,在随后的fragment来回切换时,也会调用setUserVisibleHint(),这说明,当系统首次调用setUserVisibleHint()时,控件尚未加载完成,如果我们在这时进行控件的相关操作,自然就会报空指针了,那么,怎么解决这个问题呢?我们可以立一个flag,在setUserVisibleHint()被调用时,检查onViewCreated()是否已被调用,如果是,则再根据是否对用户可见,执行相应的UI操作,具体代码如下:
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
isViewCreated = true
// onViewCreated只会调用一次,当调用此方法时,判断是否对用户可见,如果可见,调用懒加载方法
if (mIsVisibleToUser) {
onLazyLoad()
isFirstVisible = false
onFragmentResume()
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
mIsVisibleToUser = isVisibleToUser
if (!isViewCreated) return // 如果视图未创建完成,返回
if (isVisibleToUser) { // 如果对用户可见
onFragmentResume()
if (isFirstVisible) { // 如果是第一次对用户可见,则调用懒加载
onLazyLoad()
isFirstVisible = false
}
} else {
onFragmentPause()
}
}
open protected fun onLazyLoad() {
}
open protected fun onFragmentResume() {
}
open protected fun onFragmentPause() {
}
其实,这段应该是看做对于第5部分的补充,但是为了标注明显,就单独摘出来写了
Android SDK更新到API28以后,再调用setUserVisibleHint时发现会被Deprecated的情况,点进去发现SDK提供了一个新的方法,叫做setMaxLifecycle,查看注释:Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is already above the received state, it will be forced down to the correct state,也就是说我们可以通过通过FragmentTransaction手动干预Fragment的生命周期,比如fragmentTransaction.setMaxLifecycle(cardFragment, Lifecycle.State.RESUMED),可以强制指定fragment走到resume的状态,通过这个方法,我们可以在外部控制此fragment的生命周期状态,来做响应的处理
问:说了那么多,新版的懒加载是咋样的呀?
答:上面的一堆只是解释了setUserVisibleHint被压制的问题,可以看做是通过add、attach之类的方法对Fragment进行处理的补充,对于Fragment搭配FragmentPagerAdapter实现的懒加载,接着往下看:
当我们手动写BaseFragmentPageAdapter 实现 FragmentPagerAdapter 时,发现 FragmentPagerAdapter 的构造方法也被压制了,如图所示
点进去发现,SDK推荐我们使用FragmentPagerAdapter(@NonNull FragmentManager fm, @Behavior int behavior)来进行初始化,这个int behavior就是我们需要关注的重点,它一共有两种参数,BEHAVIOR_SET_USER_VISIBLE_HINT,和BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT,当我们调用FragmentPagerAdapter(FragmentManager)构造方法时,SDK内部实际上是走的FragmentPagerAdapter(FragmentManager,BEHAVIOR_SET_USER_VISIBLE_HINT),此时的逻辑跟我们第5部分中介绍的一致,当我们调用FragmentPagerAdapter(FragmentManager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)时,Indicates that only the current fragment will be in the {@link Lifecycle.State#RESUMED}state. All other Fragments are capped at {@link Lifecycle.State#STARTED}.,也就是说只有当前真正对用户可见的Fragment才会回调onResume方法,ViewPager中其它的Fragment顶多停留在Started状态,这才是我们真正需要的onResume方法,基于此,Fragment中的懒加载我们可以调整为如下写法:
1、首先在FragmentPageAdapter的构造方法中添加一个参数,最终如下所示:
FragmentPagerAdapter(FragmentManager,BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)
2、Fragment的懒加载修改如下所示:
open class BaseFragment : Fragment() {
protected lateinit var mActivity: Activity
protected val mDisposable = CompositeDisposable()
private var isFirstVisible = true
override fun onResume() {
super.onResume()
if (isFirstVisible) {
onLazyLoad()
isFirstVisible = false
return
}
}
protected open fun onLazyLoad() {
}
}
搞定收工,比第5部分简介好多有木有?
这个问题在网上有好多种说法,大部分说法都是getItemPosition时返回POSITION_NONE或者FragmentList全部remove然后重建,这两种方法亲测都有效,但是原理就是暴力销毁所有的Fragment然后重建,由此带来的性能消耗肯定是相当大的,在这里针对各种方法做一个总结,其中有参考别人的demo,也有自己的想法,话不多说,上代码:
首先,依然是从源码入手:
public Object instantiateItem(@NonNull 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);
if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
} else {
fragment.setUserVisibleHint(false);
}
}
return fragment;
}
根据注释,这个是FragmentPagerAdapter初始化Fragment时调用的方法,大致意思是,从FragmentManager中根据tag找fragment,如果找到了,那就直接显示,如果找不到,再重新创建,而这个tag就是通过makeFragmentName()创建的,我们点进去看看makeFragmentName:
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
这个Tag 其实就是viewId + itemId 来计算的,viewId我们改不了,那就修改itemId吧,一般情况下我们都是这么写的:
override fun getItemId(position: Int): Long {
return position.toLong()
}
很简单啊,位置变了,tag就变了,那么数据重新刷新,没毛病,但是位置没变的情况下数据变了,这样写就不会刷新了,
修改一下,我们根据Fragment的hashCode值作为变化的标准,因为数据变了,hashCode也会变,那么代码就变成了:
override fun getItemId(position: Int): Long {
return fragmentList[position].hashCode().toLong()
}
写好了,运行一下,发现数据有变化时,它还是不会刷新,它喵的,太操蛋啦,接着翻源码,发现这么一个东西:
/**
* Called when the host view is attempting to determine if an item's position
* has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given
* item has not changed or {@link #POSITION_NONE} if the item is no longer present
* in the adapter.
*
* The default implementation assumes that items will never
* change position and always returns {@link #POSITION_UNCHANGED}.
*
* @param object Object representing an item, previously returned by a call to
* {@link #instantiateItem(View, int)}.
* @return object's new position index from [0, {@link #getCount()}),
* {@link #POSITION_UNCHANGED} if the object's position has not changed,
* or {@link #POSITION_NONE} if the item is no longer present.
*/
public int getItemPosition(@NonNull Object object) {
return POSITION_UNCHANGED;
}
奶奶个熊的,说的真好听,Fragment有变化了就返回POSITION_NONE,没变化就返回POSITION_UNCHANGED,可是你都不做判断,直接给我返回个POSITION_UNCHANGED是几个意思?
嗯,这也就是有些文章中说的,修改返回值为POSITION_NONE,让它暴力销毁重建,这样就可以刷新数据了,不过这么做不免太浪费性能,我们对这个方法做一下修改,判断Fragment的index和hashCode,如果这俩值都没变化,证明此Fragment不需要刷新,此时我们返回POSITION_UNCHANGED,其它情况下返回POSITION_NONE,根据这个思路,就有了下面的代码:
class BaseFragmentAdapter(fm: FragmentManager, private val fragmentList: List, private val titleList: List = listOf()) :
FragmentPagerAdapter(fm, FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private val mFragPosOldArray = LongSparseArray()
private val mFragPosNewArray = LongSparseArray()
init {
setFragmentPositionOldMap()
setFragmentPositionNewMap()
}
private fun setFragmentPositionOldMap() {
mFragPosOldArray.clear()
fragmentList.forEachIndexed { index, _ ->
mFragPosOldArray.put(getItemId(index), "$index")
}
}
private fun setFragmentPositionNewMap() {
mFragPosNewArray.clear()
fragmentList.forEachIndexed { index, _ ->
mFragPosNewArray.put(getItemId(index), "$index")
}
}
override fun getItem(position: Int): Fragment = fragmentList[position]
override fun getCount(): Int = fragmentList.size
/**
* 此项用于返回 此Fragment是否发生变化,
* POSITION_UNCHANGED : 表示无变化,不需要刷新数据
* POSITION_NONE :表示不存在,需要重建刷新
* 从新的fragmentArray中根据hashCode获取index
* 如果旧的fragmentArray中也有对应的fragment,并且index相等,那么这个fragment就没有变化,不用重新绘制
* 否则的话,就重新绘制
*/
override fun getItemPosition(any: Any): Int {
val index = any.hashCode().toLong()
val position = mFragPosNewArray[index]
if (position.isNullOrEmpty()) {
return POSITION_NONE
}
if (mFragPosOldArray[index] != null && mFragPosOldArray[index] == position) {
return POSITION_UNCHANGED
}
return POSITION_NONE
}
// 取代直接返回position,用于识别是否为同一个fragment
override fun getItemId(position: Int): Long {
return fragmentList[position].hashCode().toLong()
}
override fun notifyDataSetChanged() {
setFragmentPositionNewMap()
super.notifyDataSetChanged()
setFragmentPositionOldMap()
}
override fun getPageTitle(position: Int): CharSequence = titleList[position]
注释已经写的很清楚了,大致解释一下:BaseFragmentAdapter初始化时,创建两个LongSparseArray用来存储Fragment的hash值和索引值,当notifyDataSetChanged执行时,我们LongSparseArray中取出hash值和index进行判断,以此来判断是否需要刷新即可,然后运行安装刷新,perfect!没毛病!问题解决!
一些扩展想法:按照以上思路,Fragment的位置发生了变化但是数据没有发生变化时,也会销毁重建,从这个角度来讲,这种方案似乎也不太完美,不过这种情况太过于极端,基本上遇不到,大家有什么想法,可以评论,我看到后会及时回复的
总结:Fragment在安卓开发中是使用率相当高的一个控件,几乎每一个APP中都能见到它的身影,其重要性不言而喻。另外:谷歌对于每一个API都有详细的注释,平时多翻一翻,会大有收获的;再有:我们也可以利用Android studio提供的模板,看看它们是怎么写的,然后再根据这个思路进行开发,也是一种不错的选择呢