FragmentPagerAdapter使用及深思

2019-12-27  本文已影响0人  Magic旭

API熟悉

  1. private static String makeFragmentName(int viewId, long id)
    该私有变量方法主要用于在定义fragmentManager如果寻找内存中已经存在的Fragment实例对象。
//其实这里可以自己定义自己的查询路径,下面会一一介绍
private static String makeFragmentName(int viewId, long id) {
        return "android:switcher:" + viewId + ":" + id;
}

  1. public long getItemId(int position)
    getItemId主要是提供给上面方法的id参数,这里系统默认的是用position来做id值。这里我们开发者可以直接覆盖系统值,返回一个我们fragment页面独特的id作为fragmentManager寻找内存中fragment的依据。
public long getItemId(int position) {
        return (long)position;
}
//例如,这里我可以通过页面的id值作为查询路径的id
override fun getItemId(position: Int): Long {
        return fragments[position].tag
}
  1. public Object instantiateItem(ViewGroup container, int position)
    该方法的回调在ViewPager的addNewItem(int position, int index)里面被调用,主要就是ViewPager添加新的Item时候,会从adapter里面取对应position的ItemView。
//ViewPager不属于这篇文章的范畴
@Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        //上面介绍到的方法2
        final long itemId = getItemId(position);
        //上面介绍到的方法1(假如方法2自己覆盖了id,也是可以通过自己的id参与的path的查找当中的)
        String name = makeFragmentName(container.getId(), itemId);
        //同一个Manager实例情况下,通过tag查找内存中的fragment实例
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            mCurTransaction.attach(fragment);
        } else {
            //假如通过tag找不到对应的fragment,则把回调返回给开发者,让开发者提供想要的fragment页面,这里在方法4介绍
            fragment = getItem(position);
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId));
        }
        //这里就属于setUserVisibleHint的回调了
        if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            FragmentCompat.setUserVisibleHint(fragment, false);
        }
        return fragment;
    }
  1. public abstract Fragment getItem(int var1);
    当FragmentManager中通过tag找不到对应的fragment时候,将提供新页面的方法回调给开发者,让开发者提供新页面。

FragmentManager如何存储Fragment

有兴趣可以自己到FragmentManager源码看下,这里不做详细讲解,源码里面储存和查找都是基于mAdds的List数组里面的。

//添加方法(还有onAttach方法等都会往mAdded集合里面添加fragment)
public void addFragment(Fragment fragment, boolean moveToStateNow) {
……
        synchronized (mAdded) {
               mAdded.add(fragment);
         }
……
}

//查找
public Fragment findFragmentByTag(String tag) {
        if (tag != null) {
            // First look through added fragments.
            for (int i=mAdded.size()-1; i>=0; i--) {
                Fragment f = mAdded.get(i);
                if (f != null && tag.equals(f.mTag)) {
                    return f;
                }
            }
        }
    ……
        return null;
    }

实践

错误使用场景
  1. 场景是FragmentA+ViewPager嵌套2个FragmentB。当框架整体重建后,重新new了一个FragmentA,然后我的代码里面在onCreate方法里又重新创建的个List<Fragment>的数组。导致又创建了2个新的FragmentB添加到FragmentManagerImpl类里面的mAdds数组中了。所以当通过findFragmentByTag寻找某个fragment时候,因为是倒序查找的,自然找到了新的FragmentB。
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ……
        fragments.add(ChannelTitleMode(resources.getString(R.string.xxxx), FragmentB1()))
        fragments.add(ChannelTitleMode(resources.getString(R.string.xxxx), FragmentB2()))
        
    }
  1. 导致问题:因为FragmentPagerAdapter内部已经通过tag值找到了FragmentB1和FragmentB2,而且还不是旧的FragmentB1和FragmentB2。找到之后FragmentManager只是调用attach方法,不会用add方法,自然FragmentB1和FragmentB2的onAttach方法不会被调用。导致原来页面onAttach方法获取对应的对象引用为空,因为onAttach方法不被调用了。
    详情请参考:FragmentManager各种方法对应的Fragment生命周期
正确使用场景
  1. FragmentPagerAdapter里面List数组储存的应该是<name,tag>,而不是<name,fragment>。这样可以通过tag查询到旧的fragment,里面的引用和变量不会改变。
const val CHANNEL_FRAGMENT_TAG = 1L
const val CATEGORY_FRAGMENT_TAG = 2L

class XXXXHomePagerAdapter(
    //data class ChannelTitleModel(val title: String, val tag: Long)
   //这里的数据结构很简单,就是用<name,tag>形式作为内容了,不再用<name,fragment>的形式
    private val fragments: List<ChannelTitleModel>,
    private val fm: FragmentManager,
    private val pageId: Int
) : FragmentPagerAdapter(fm) {
    override fun getPageTitle(position: Int): CharSequence? {
        return fragments[position].title
    }

    override fun getItem(position: Int): Fragment {
        return generateFragment(fragments[position].tag)
    }

    override fun getCount(): Int {
        return fragments.size
    }

    private fun generateFragment(tag: Long): Fragment {
        return when (tag) {
            CHANNEL_FRAGMENT_TAG -> XXXXPageFragment()
            else -> XXXXXFragment()
        }
    }

    fun getTagFragment(position: Int): Fragment? {
        val tag = fragments[position].tag
        return fm.findFragmentByTag(makeFragmentName(pageId, tag))
    }
    //这一步比较关键,要重写源码的position作为id,用我们自身定义的tag作为查找的id
    override fun getItemId(position: Int): Long {
        return fragments[position].tag
    }

    private fun makeFragmentName(viewId: Int, id: Long): String {
        return "android:switcher:$viewId:$id"
    }
}

深思

  1. 为什么出现这种情况,说到底还是自己当初大学接触android时候没看源码,没养成看源码的习惯。导致大学中如果使用FragmentPagerAdapter的习惯保留到工作来了。(还是自己菜逼)
  2. 使用FragmentPagerAdapter的时候,千万别在你的Fragment的时候重新new Fragment,然后又塞给adapter,这样子是浪费内存资源的错误行为。
  3. 在Fragment+ViewPager嵌套多个fragment的时候,你的fragmentManager应该是childFragmentManager。
    温馨补充:1、getFragmentManager belong to Activity;2、getChildFragmentManager belong to Fragment
    例如:TO display Fragment1 on MainActivity we must use getSupportFragmentManager()
    TO display Fragment2 from Fragment1 we have 2 way USE getFragmentManager()
上一篇下一篇

猜你喜欢

热点阅读