当重新附加时,Fragment 会创建/恢复一个重复的视图

6

我有一个 ViewPager,其中包含 6 个页面,OffscreenPageLimit=2(易于重现我的错误)。这 6 个页面中的所有数据都来自服务器。在 onCreateView 中,我向服务器发送请求,并在收到数据后刷新 UI。

当我选择第一个选项卡并快速切换到最后几次时,某些页面会显示错误。此时,片段中的字段 mMainLayout 不为空。

例如,在我的第一页中有一个 ListView。当页面出错时,另一个 ListView 出现在正确的 ListView 上方。当我尝试滚动 ListView 时,只有右侧的那个(底部的)会移动。

我知道我的响应侦听器持有 mMainLayout 和一些其他视图的引用。我在方法 onCreateView 中创建了一个新的 mMainLayout,我认为片段在重新连接/还原时使用新的 mMainLayout 并删除旧的(或将其从容器中移除)。但我错了。

我知道 FragmentPagerAdapterinstantiateItem 中附加/重新附加片段,并在 destroyItem 中分离片段。适配器没有删除片段,也没有创建新片段。片段保留视图和视图状态本身。

我从容器中删除了 mMainLayout,将片段中的所有视图设置为空值,在 onDestroyView 中不保存任何内容。但错误仍然很容易重现。

活动:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    registerReceiver(receiver, new IntentFilter("com.souyidai.intent.action.log_fragment"));
    FragmentManager fragmentManager = getFragmentManager();
    mResources = getResources();
    mMainPagerAdapter = new MainPagerAdapter(fragmentManager);
    mPager = (ViewPager) findViewById(R.id.pager);
    mPager.setAdapter(mMainPagerAdapter);
    mPager.setOffscreenPageLimit(CACHE_SIZE);
    mIndicator = (TabPageIndicator) findViewById(R.id.indicator);
    mIndicator.setViewPager(mPager);
    ...
}

private class MainPagerAdapter extends FragmentPagerAdapter {
    public MainPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        MainConfig.TabItem tab = mTabs.get(position);
        String tabType = tab.getTabType();
        String code = tab.getCode();
        MainConfig.TabItem.SubTabItem subTabItem = tab.getSubitem().get(0);
        Fragment fragment = MainFragment.newInstance(code, subTabItem);
        return fragment;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        MainConfig.TabItem tab = mTabs.get(position);
        return tab.getTitle();
    }

    @Override
    public int getCount() {
        return mTabs.size();
    }
}

片段:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mContainer = container;
    mMainLayout = inflater.inflate(R.layout.fragment_main, container, false);
    ...
}


@Override
public void onDestroyView() {
    super.onDestroyView();
    mContainer.removeView(mMainLayout);
    mContainer = null;
    mMainLayout = null;
    mSwipeRefreshLayout = null;
    mListView = null;
    mHeaderLayout = null;
    mFooterLayout = null;
    ...
}

android.support.v13.app.FragmentPagerAdapter

@Override
public Object instantiateItem(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) {
        FragmentCompat.setMenuVisibility(fragment, false);
        FragmentCompat.setUserVisibleHint(fragment, false);
    }

    return fragment;
}

我发现这个问题:

Android fragment onCreateView creating duplicate views on top of each other

但是我只在FragmentPagerAdapter.getItem中创建新的片段。所以我们不同。

我通过在onCreateView方法中删除mMainLayout的所有子项(如果mMainLayout不为空),解决了这个问题。然后一切都好了。但我仍然困惑为什么会出现这个bug?

我进行了一些测试。
1. 尝试两次添加片段,应用程序崩溃并记录以下内容:java.lang.IllegalStateException: Fragment already added: ...
2. 尝试两次附加片段,系统不调用Fragment.onCreateFragment.onCreateView


请添加您的代码。 - Johnny Shieh
如果您想保留片段状态,请使用“setRetainInstance(true)”API。当第二次加载相同的片段时,如果在内存中存在,则将从先前的实例加载UI组件,否则将创建新的片段。因此,您不需要在ondestroyview方法中将所有UI组件设置为null。 - Keyur Thumar
@keyur9779 我不想保留我的片段状态,但看起来片段仍然从上一个状态中恢复。 - android_su
1
为什么要重写instantiateItem方法?在FragmentPagerAdapter或FragmentStatePagerAdapter中,你只需要重写getItem(以及getCount())方法,以便从片段列表中返回特定位置的片段。 - Pravin Divraniya
@android_su 我遇到了相同的问题,并通过检查baseview是否为空来解决,只有在它为空时才填充它,否则返回现有的布局实例。如果有帮助,您可以看看我的答案。 - Swapnil
显示剩余5条评论
2个回答

3

这种情况是由于自定义实现 instantiateItem() 导致的。 我注意到了一件事。你在检查片段是否已经添加时出现了问题。

@Override
public Object instantiateItem(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);
        //make sure you are not attaching already added fragment
        //try with comment and uncomment two lines below
        //if(!fragment.isAdded())
        //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) {
        FragmentCompat.setMenuVisibility(fragment, false);
        FragmentCompat.setUserVisibleHint(fragment, false);
    }

    return fragment;
}

不知道为什么要创建类成员字段mMainLayout

//try to go with default
View view = inflater.inflate(R.layout.fragment_main, container, false);

我建议你学习这个


如果我重复添加一个碎片,应用程序会崩溃并且日志会显示:java.lang.IllegalStateException: Fragment already added: ... - android_su
你说得对,没有必要持有mMainLayout的引用。但是我必须持有一些视图的引用,以便在从服务器获取数据时更新视图。 - android_su
我的应用程序没有崩溃,所以我认为调用fragment.isAdded没有帮助。 - android_su

2

你可以尝试使用以下代码,我曾遇到类似的问题并通过以下更改解决了。在这种情况下,不需要在onDestroyView中使所有引用为空。因为在这种情况下,你需要在使用这些引用的任何其他代码处始终进行空值检查。

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    if(mMainLayout == null)
    {
      mMainLayout = inflater.inflate(R.layout.fragment_main, container,false);
...
    } 
    return mMainLayout;
}

mMainlayout 不为 null 时,意味着您的片段实例已经有了一个 mMainLayout 实例,并且已经添加到 ViewGroup container 中,无需再次添加。您遇到问题是因为您正在将相同的视图再次添加到相同的容器中。


我在我的问题中最后提到我已经像你一样解决了这个问题。我想知道为什么视图/片段会被添加两次。 - android_su
由于相同的布局已经添加到您的ViewGroup容器中,因此我们需要进行空值检查以避免重复添加。 - Swapnil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接