踩坑,Fragment使用遇到那些坑

一、 Fragment为什么要用newInstance来初始化:

我们利用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使用遇到那些坑_第1张图片

咦,貌似没什么问题啊,你特么在逗我?别急,我们把屏幕横过来,再来看:继续上运行图:

踩坑,Fragment使用遇到那些坑_第2张图片

看出来差别了吗?

上面那个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重建时,我们设置的参数就没有了

二、 Fragment中调用getActivity()时,报空指针异常:

一般的,我们在代码中请求网络数据后,由于是在子线程中得到的结果,更新UI界面时,要在UI线程中进行,如果是在fragment中,则需要执行以下代码:

// 在UI线程中展示吐司
private void showToast(String text) {
    getActivity().runOnUiThread(() ->
            Toast.makeText(getActivity(),text,Toast.LENGTH_SHORT).show()
    );
}

乍一看,代码貌似没什么问题,运行起来也能正常显示,但是这样写,在系统可用内存较低时,会频繁触发crash(尤其在低内存手机上经常遇到),此时,再执行这样的代码,就会报空指针异常了,下面来分析具体原因:

首先,我们来看一下fragment的声明周期:

踩坑,Fragment使用遇到那些坑_第3张图片

重点关注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不调用onResume或者onPause方法:

首先,来看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:执行对用户可不见时的代码
    }
}

四、Fragment在kotlin语言中使用kotlin-android-extensions报错的问题:

自从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在对用户可见时,再进行页面加载以及相关逻辑的运行,此时,就需要考虑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() {

    }

六、Android 9 中的Fragment懒加载

其实,这段应该是看做对于第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 的构造方法也被压制了,如图所示

踩坑,Fragment使用遇到那些坑_第4张图片点进去发现,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部分简介好多有木有?

七、Fragment执行Notifidatasetchanged不生效

这个问题在网上有好多种说法,大部分说法都是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提供的模板,看看它们是怎么写的,然后再根据这个思路进行开发,也是一种不错的选择呢

 

你可能感兴趣的:(踩坑,Fragment使用遇到那些坑)