【问题标题】:How can I switch between two fragments, without recreating the fragments each time?如何在两个片段之间切换,而无需每次都重新创建片段?
【发布时间】:2014-05-07 22:22:41
【问题描述】:

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

这是我主要活动的代码。

/* 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。侧向滑动不会破坏碎片。但是它可能不是有效的解决方案。

标签: android android-fragments


【解决方案1】:

@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有必要你发现了吗?
  • 查看这篇文章,如果您有两个以上的片段,如何获取当前可见的片段。我喜欢 Matt Mobrea 的回答:stackoverflow.com/questions/9294603/…
  • 感谢您编写解决方案。你介意分享你的xml文件吗? R.id.container 后面的元素是如何声明的?
  • 其实我刚刚测试过,效果很好。所以是的,它仍然有效。
  • 只想提一下,根据文档: 注意:当您删除或替换片段并将事务添加到后台堆栈时,被删除的片段将停止(而不是销毁)。如果用户返回以恢复片段,它会重新启动。如果您不将事务添加到后台堆栈,则片段在删除或替换时将被销毁。 -- developer.android.com/training/basics/fragments/…
【解决方案2】:

使用带有标签的attach/detach方法:

Detach 会破坏视图层级,但会保留状态,就像在后台堆栈上一样;这将使“不可见”片段的内存占用更小。但请注意,您需要正确实现片段生命周期(首先应该这样做)

从 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 相比,此选项的开销似乎会稍微少一些。
  • Android 可能会在分离时 gc 未使用的片段,如果您隐藏/显示它,您将对它有一个强引用,因此这种方法更节省内存。
  • gc?抱歉,相对新手
  • @Burkely91Garbage collector - 自动清理未使用的资源
  • 谢谢,在我的情况下使用附加和分离更好,因为 findFragmentById 提供了最新的附加片段。因此,如果我使用显示/隐藏,我无法获得可见片段,因为隐藏的片段仍然附加到容器。
【解决方案3】:

替换方法会破坏你的片段。一种解决方法是将它们设置为Visibility.GONE,另一种(不太容易)方法是将它们保存在一个变量中。如果这样做,请确保不会左右泄漏内存。

【讨论】:

  • 感谢您指出 replace() 会破坏片段。
【解决方案4】:

我以前是这样干的:

        if (mPrevFrag != fragment) {

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

        }

(您需要在此解决方案中跟踪您之前的片段)

【讨论】:

    【解决方案5】:

    我猜你不能直接操纵Fragments 的生命周期机制。你可以findFragmentByTag 这个事实并不是很糟糕。这意味着 Fragment 对象没有完全重新创建,如果它已经提交的话。现有的Fragment 只是通过了每个Fragment 具有的所有生命周期步骤——这意味着只有 UI 被“重新创建”。

    这是一种非常方便和有用的内存管理策略——在大多数情况下是合适的。 Fragment 消失了,拥有必须使用的资源才能释放内存。

    如果您停止使用此策略,您的应用程序的内存使用量可能会严重增加。

    尽管如此,仍有保留的片段,其生命周期有点不同,与它们所附加的 Activity 不对应。通常,它们用于保留一些您想要保存的内容,例如,manage configuration changes

    但是,片段 [重新] 创建策略取决于上下文 - 即您想要解决的问题,以及您愿意接受的权衡取舍。

    【讨论】:

    • 恕我直言,这不能回答问题。关于片段的生命周期漫无边际。然后提示可能保留的片段会很有用 - 尽管没有关于如何使用的指导。
    【解决方案6】:

    只需找到调用 getFragmentById("id of your container") 的当前片段,然后将其隐藏并显示所需的片段。

    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,它会破坏片段。
    【解决方案7】:

    与 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();
    

    【讨论】:

      【解决方案8】:

      使用扩展在 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
      

      【讨论】:

        【解决方案9】:

        这是我在 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)
            }
        }
        

        【讨论】:

          【解决方案10】:

          试试 Visible 属性怎么样?

          【讨论】:

            【解决方案11】:

            这是一个有点晚的反应。 如果您为片段使用视图寻呼机,请将片段的屏幕外页面限制设置为创建的片段数。

            mViewPager.setOffscreenPageLimit(3); // number of fragments here is 3
            

            【讨论】:

              猜你喜欢
              • 2016-02-18
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多