如何在两个Fragment之间切换,而无需每次重新创建这些Fragment?

50

我正在开发一个安卓应用程序,使用导航抽屉在两个片段之间切换。但是,每次切换时,片段都会完全重新创建。

以下是我的主活动代码。

/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        selectItem(position);
    }
}

private void selectItem(int position) {
    android.support.v4.app.Fragment fragment;
    String tag;
    android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();

    switch(position) {
        case 0:
            if(fragmentManager.findFragmentByTag("one") != null) {
                fragment = fragmentManager.findFragmentByTag("one");
            } else {
                fragment = new OneFragment();
            }
            tag = "one";
            break;
        case 1:
            if(fragmentManager.findFragmentByTag("two") != null) {
                fragment = fragmentManager.findFragmentByTag("two");
            } else {
                fragment = new TwoFragment();
            }
            tag = "two";
            break;
    }

    fragment.setRetainInstance(true);
    fragmentManager.beginTransaction().replace(R.id.container, fragment, tag).commit();

    // update selected item and title, then close the drawer
    mDrawerList.setItemChecked(position, true);
    setTitle(mNavTitles[position]);
    mDrawerLayout.closeDrawer(mDrawerList);
}
我已经设置了一些调试日志,每次调用selectItem时,一个片段被销毁,而另一个片段被创建。
有没有什么办法可以防止这些片段被重新创建,只是重复使用它们呢?
有没有什么办法可以防止这些片段被重新创建,只是重复使用它们呢?

如果你愿意放弃导航抽屉,你可以使用Viewpager。向侧面滑动不会破坏片段。然而,这可能不是有效的解决方案。 - Dhaval
12个回答

55

在 @meredrica 指出 replace() 会破坏片段后,我回顾了 FragmentManager 文档。这是我想出的解决方案,看起来可以使用。

/* The click listener for ListView in the navigation drawer */
private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        selectItem(position);
    }
}

private void selectItem(int position) {
    android.support.v4.app.FragmentManager; fragmentManager = getSupportFragmentManager();

    switch(position) {
        case 0:
            if(fragmentManager.findFragmentByTag("one") != null) {
                //if the fragment exists, show it.
                fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("one")).commit();
            } else {
                //if the fragment does not exist, add it to fragment manager.
                fragmentManager.beginTransaction().add(R.id.container, new OneFragment(), "one").commit();
            }
            if(fragmentManager.findFragmentByTag("two") != null){
                //if the other fragment is visible, hide it.
                fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("two")).commit();
            }
            break;
        case 1:
            if(fragmentManager.findFragmentByTag("two") != null) {
                //if the fragment exists, show it.
                fragmentManager.beginTransaction().show(fragmentManager.findFragmentByTag("two")).commit();
            } else {
                //if the fragment does not exist, add it to fragment manager.
                fragmentManager.beginTransaction().add(R.id.container, new TwoFragment(), "two").commit();
            }
            if(fragmentManager.findFragmentByTag("one") != null){
                //if the other fragment is visible, hide it.
                fragmentManager.beginTransaction().hide(fragmentManager.findFragmentByTag("one")).commit();
            }
            break;
    }

    // update selected item and title, then close the drawer
    mDrawerList.setItemChecked(position, true);
    setTitle(mNavTitles[position]);
    mDrawerLayout.closeDrawer(mDrawerList);
}

我也添加了这一部分,但我不确定它是否必要。

@Override
public void onDestroy() {
    super.onDestroy();
    FragmentManager fragmentManager = getSupportFragmentManager();
    if(fragmentManager.findFragmentByTag("one") != null){
        fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("one")).commit();
    }
    if(fragmentManager.findFragmentByTag("two") != null){
        fragmentManager.beginTransaction().remove(fragmentManager.findFragmentByTag("two")).commit();
    }
}

你发现onDestroy是否必要吗? - Lion789
@Christian R.id.container 可能只是一个 LinearLayout,但是这个问题/答案已经两年了。代码已经不存在,也可能不再相关。抱歉。 - Tester101
这段代码为什么不再适用了?你有更好的解决方案吗? - Tushar Kathuria
2
实际上,我刚刚测试过它,并且它运行得非常好。所以是的,它仍在工作。 - Tushar Kathuria
1
根据文档的说明,需要提到: 注意:当您移除或替换一个片段并将事务添加到返回栈时,被移除的片段会停止(而非销毁)。如果用户导航回来以恢复该片段,它会重新启动。如果您不将事务添加到返回栈,那么在移除或替换时该片段会被销毁。-- https://developer.android.com/training/basics/fragments/fragment-ui.html - Burkely91
显示剩余10条评论

19
使用带标记的附加/分离方法:

分离将销毁视图层次结构但保留状态,就像在返回堆栈上一样;这将使“不可见”的片段具有较小的内存占用。但请注意,您需要正确实现片段生命周期(首先应该这样做)。

从UI中分离给定的片段。这与放入后退堆栈时的状态相同:从UI中删除片段,但其状态仍由片段管理器积极管理。进入此状态时将销毁其视图层次结构。

第一次添加片段

FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.add(android.R.id.content, new MyFragment(),MyFragment.class.getSimpleName());
t.commit();

然后你将其分离。
FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.detach(MyFragment.class.getSimpleName());
t.commit();

如果切换回来,重新附加它,状态将被保留。

FragmentTransaction t = getSupportFragmentManager().beginTransaction();
t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
t.commit();

但是你总是要检查片段是否已经添加,如果没有则添加,否则只需附加它:

if (getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()) == null) {
    FragmentTransaction t = getSupportFragmentManager().beginTransaction();
    t.add(android.R.id.content, new MyFragment(), MyFragment.class.getSimpleName());
    t.commit();
} else {
    FragmentTransaction t = getSupportFragmentManager().beginTransaction();
    t.attach(getSupportFragmentManager().findFragmentByTag(MyFragment.class.getSimpleName()));
    t.commit();
}

您能否预见使用此选项相比隐藏/显示选定片段的其他建议会有什么缺点?对我来说,这个选项似乎会比维护片段的状态和UI稍微少一些开销。 - Burkely91
5
如果将Android Fragment从视图中分离(detached),它可能会被垃圾回收(gc)。如果你隐藏/显示它,你就会持有对它的一个强引用(strong reference),因此这种方法更加内存高效。 - Patrick
gc?抱歉,我是相对新手。 - Burkely91
垃圾回收器 - 自动清理未使用的资源。 - Otziii
谢谢,对于我的情况使用attach和detach更好,因为findFragmentById会返回最新附加的片段。因此,如果我使用show/hide,我无法获取可见片段,因为隐藏的片段仍然附加到容器中。 - rasitayaz

5

replace方法会破坏你的片段。一种解决方法是将它们设置为Visibility.GONE,另一种(不太容易的)方法是将它们保存在一个变量中。如果你选择后者,请确保不会造成内存泄漏。


感谢指出replace()会破坏片段。 - Tester101

4
我之前是这样做的:

我之前是这样做的:

        if (mPrevFrag != fragment) {

            // Change
            FragmentTransaction ft = fragmentManager.beginTransaction();
            if (mPrevFrag != null){
                ft.hide(mPrevFrag);
            }
            ft.show(fragment);
            ft.commit();
            mPrevFrag = fragment;

        }

(您需要在此解决方案中跟踪先前的片段)
Translated: (在这个解决方案中,您需要跟踪之前的片段)

2
我猜你不能直接操纵你的Fragments生命周期机制。能够使用findFragmentByTag并不是非常糟糕。它意味着如果已经提交,则不必完全重新创建Fragment对象。现有的Fragment只需通过每个Fragment具有的所有生命周期步骤即可 - 这意味着仅重建UI。

这是一种非常方便和实用的内存管理策略,并且在大多数情况下都是合适的。Fragment消失后,必须利用资源来释放内存。

如果你停止使用此策略,则应用程序的内存使用可能会急剧增加。

尽管如此,仍然存在保留片段,它们的生命周期有点不同,不对应于它们所附加的Activity。通常,它们用于保留一些想要保存的东西,例如,管理配置更改

然而,[重新]创建片段策略取决于上下文 - 也就是说,您想要解决什么问题以及愿意接受什么权衡。


2
在我看来,这并没有回答问题。只是在闲扯关于片段的生命周期。然后暗示保留片段可能会有用——但没有指导如何使用。 - ToolmakerSteve

2
只需使用getFragmentById("您的容器的id")找到当前片段,然后隐藏它并显示所需的片段即可。
private void openFragment(Fragment fragment, String tag) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        Fragment existingFragment = fragmentManager.findFragmentByTag(tag);
        if (existingFragment != null) {
            Fragment currentFragment = fragmentManager.findFragmentById(R.id.container);
            fragmentTransaction.hide(currentFragment);
            fragmentTransaction.show(existingFragment);
        }
        else {
            fragmentTransaction.add(R.id.container, fragment, tag);
        }
        fragmentTransaction.commit();
    }

是的,解决方案是使用.show和.hide方法。在原始代码中,我使用.replace,这会破坏片段。 - Tester101

1
与Tester101的想法相同,但这是我最终使用的内容。
    FragmentManager fragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

    Fragment oldFragment = fragmentManager.findFragmentByTag( "" + m_lastDrawerSelectPosition );
    if ( oldFragment != null )
        fragmentTransaction.hide( oldFragment );

    Fragment newFragment = fragmentManager.findFragmentByTag( "" + position );
    if ( newFragment == null )
    {
        newFragment = getFragment( position );
        fragmentTransaction.add( R.id.home_content_frame, newFragment, "" + position );
    }

    fragmentTransaction.show( newFragment );
    fragmentTransaction.commit();

0

使用扩展功能在Kotlin中轻松隐藏:

fun FragmentManager.present(newFragment: Fragment, lastFragment: Fragment? = null, containerId: Int) {
    if (lastFragment == newFragment) return

    val transaction = beginTransaction()
    if (lastFragment != null && findFragmentByTag(lastFragment.getTagg()) != null) {
        transaction.hide(lastFragment)
    }

    val existingFragment = findFragmentByTag(newFragment.getTagg())
    if (existingFragment != null) {
        transaction.show(existingFragment).commit()
    } else {
        transaction.add(containerId, newFragment, newFragment.getTagg()).commit()
    }
}

fun Fragment.getTagg(): String = this::class.java.simpleName

使用方法

supportFragmentManager.present(fragment, lastFragment, R.id.fragmentPlaceHolder)
lastFragment = fragment

0

最简单的方法

只需用您自己的代码替换此代码即可

var transition = getSupportFragmentManager().beginTransaction();
var added_frags = getSupportFragmentManager().getFragments();
var isAdded = false;

for (Fragment frag : added_frags) {
    if (frag.getClass() == fragment.getClass()) {
        isAdded = true;
        transition.show(frag);
    } else {
        transition.hide(frag);
    }
}

if (!isAdded) {
    transition.add(R.id.container, fragment);
}

transition.commit();

fragment 是你想要替换的 Fragment


0

这是我在 Kotlin 中使用的简单 2 片段案例:

private val advancedHome = HomeAdvancedFragment()
private val basicHome = HomeBasicFragment()

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Attach both fragments and hide one so we can swap out easily later
    supportFragmentManager.commit {
        setReorderingAllowed(true)
        add(R.id.fragment_container_view, basicHome)
        add(R.id.fragment_container_view, advancedHome)
        hide(basicHome)
    }

    binding.displayModeToggle.onStateChanged {
        when (it) {
            0 -> swapFragments(advancedHome, basicHome)
            1 -> swapFragments(basicHome, advancedHome)
        }
    }
    ...
}

使用这个FragmentActivity扩展:

fun FragmentActivity.swapFragments(show: Fragment, hide: Fragment) {
    supportFragmentManager.commit {
        show(show)
        hide(hide)
    }
}

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