添加到后台堆栈时,如何维护片段状态?

Eri*_*ric 150 android android-fragments back-stack

我写了一个在两个片段之间切换的虚拟活动.当您从FragmentA转到FragmentB时,FragmentA会被添加到后台堆栈中.但是,当我返回FragmentA(通过按回)时,会创建一个全新的FragmentA,并且它所处的状态将丢失.我感觉我跟这个问题一样,但我已经包含了一个完整的代码示例来帮助解决问题:

public class FooActivity extends Activity {
  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    final FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(android.R.id.content, new FragmentA());
    transaction.commit();
  }

  public void nextFragment() {
    final FragmentTransaction transaction = getFragmentManager().beginTransaction();
    transaction.replace(android.R.id.content, new FragmentB());
    transaction.addToBackStack(null);
    transaction.commit();
  }

  public static class FragmentA extends Fragment {
    @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
      final View main = inflater.inflate(R.layout.main, container, false);
      main.findViewById(R.id.next_fragment_button).setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
          ((FooActivity) getActivity()).nextFragment();
        }
      });
      return main;
    }

    @Override public void onSaveInstanceState(Bundle outState) {
      super.onSaveInstanceState(outState);
      // Save some state!
    }
  }

  public static class FragmentB extends Fragment {
    @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
      return inflater.inflate(R.layout.b, container, false);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

添加了一些日志消息:

07-05 14:28:59.722 D/OMG     ( 1260): FooActivity.onCreate
07-05 14:28:59.742 D/OMG     ( 1260): FragmentA.onCreateView
07-05 14:28:59.742 D/OMG     ( 1260): FooActivity.onResume
<Tap Button on FragmentA>
07-05 14:29:12.842 D/OMG     ( 1260): FooActivity.nextFragment
07-05 14:29:12.852 D/OMG     ( 1260): FragmentB.onCreateView
<Tap 'Back'>
07-05 14:29:16.792 D/OMG     ( 1260): FragmentA.onCreateView
Run Code Online (Sandbox Code Playgroud)

它永远不会调用FragmentA.onSaveInstanceState,它会在你回击时创建一个新的FragmentA.但是,如果我在FragmentA上并锁定屏幕,则会调用FragmentA.onSaveInstanceState.太奇怪了...我错了预期一个片段被添加到后面的堆栈而不需要重新创建?以下是文档所说的内容:

然而,如果您在删除片段时调用addToBackStack(),则片段将停止,并且如果用户导航回复则将恢复该片段.

Jan*_*enk 116

如果从后向堆栈返回一个片段,它不会重新创建片段,而是重新使用相同的实例,并从onCreateView()片段生命周期开始,请参阅片段生命周期.

因此,如果要存储状态,则应使用实例变量而不是依赖onSaveInstanceState().

  • 该文件的当前版本与此声明相矛盾.流程图显示了您所声明的内容,但页面主区域中的文本显示onCreateView()仅被称为片段显示的_first_time:http://developer.android.com/guide/components/fragments.html I我现在正在解决这个问题,从backstack返回片段时我没有看到任何调用的方法.(Android 4.2) (30认同)
  • 试图记录它的行为.在显示片段时始终调用onCreateView(). (9认同)
  • 这对我不起作用.返回到片段时,我的实例变量为null!我该如何保存州? (7认同)
  • @ColinM.解决问题的方法是什么? (3认同)
  • 所以如果我们不应该在保存实例上中继,应该如何保存片段状态和数据? (3认同)

Vin*_*uan 78

相较于苹果UINavigationControllerUIViewController,谷歌并没有在Android软件架构做得很好.Android的文档Fragment并没有多大帮助.

从FragmentA输入FragmentB时,不会销毁现有的FragmentA实例.当您在FragmentB中按Back并返回FragmentA时,我们不会创建新的FragmentA实例.onCreateView()将调用现有的FragmentA实例.

关键是我们不应该在FragmentA中再次膨胀视图onCreateView(),因为我们正在使用现有的FragmentA实例.我们需要保存并重用rootView.

以下代码运行良好.它不仅保持片段状态,还减少了RAM和CPU负载(因为我们只在必要时扩充布局).我无法相信Google的示例代码和文档从未提及它,但总是夸大布局.

版本1(不要使用版本1.使用版本2)

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // (it will be added back).
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        return _rootView;
    }
}
Run Code Online (Sandbox Code Playgroud)

------ 2005年5月3日更新:-------

正如所提到的评论,有时_rootView.getParent()是空的onCreateView,这会导致崩溃.版本2删除onDestroyView()中的_rootView,如dell116建议的那样.在Android 4.0.3,4.4.4,5.1.0上测试.

版本2

public class FragmentA extends Fragment {
    View _rootView;
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (_rootView == null) {
            // Inflate the layout for this fragment
            _rootView = inflater.inflate(R.layout.fragment_a, container, false);
            // Find and setup subviews
            _listView = (ListView)_rootView.findViewById(R.id.listView);
            ...
        } else {
            // Do not inflate the layout again.
            // The returned View of onCreateView will be added into the fragment.
            // However it is not allowed to be added twice even if the parent is same.
            // So we must remove _rootView from the existing parent view group
            // in onDestroyView() (it will be added back).
        }
        return _rootView;
    }

    @Override
    public void onDestroyView() {
        if (_rootView.getParent() != null) {
            ((ViewGroup)_rootView.getParent()).removeView(_rootView);
        }
        super.onDestroyView();
    }
}
Run Code Online (Sandbox Code Playgroud)

警告!!!

这是一个黑客!虽然我在我的应用程序中使用它,但您需要仔细测试和阅读注释.

  • 持有对整个片段的根视图的引用是一个坏主意IMO.如果你不断地向backstack添加几个片段并且它们都持有它的rootview(它具有相当大的内存占用量),那么你很可能最终得到OutOfMemoryError,因为所有片段都拥有rootview引用和GC cant收集它.我认为更好的方法是一直膨胀视图(并让Android系统处理其视图创建/销毁)和onActivityCreated/onViewCreated检查您的数据是否为空.如果是,则加载它,否则将数据设置为视图. (38认同)
  • *不要这样做!*创建Fragment的视图层次结构时,它包含对当时保存片段的Activity的内部引用.发生配置更改时,通常会重新创建活动.重新使用旧布局可以将zombie Activity与其引用的任何对象一起保存在内存中.浪费这样的内存会妨碍性能,并使您的应用程序成为不在前台时立即终止的最佳候选者. (15认同)
  • 在其他地方的空指针异常 (4认同)
  • 我同意@traninho.并且不明白为什么有太多的投票... (4认同)
  • @AllDayAmazing这是一个好点.说实话,我现在很困惑.任何人都可以尝试解释为什么持有对片段的rootview的引用是不行的,但只为rootview的任何子节点提供引用(无论如何都引用了rootview)是否正常? (4认同)
  • 远离这个,除非你想浪费5个小时找出什么是你的代码......然后才发现这是原因.现在我必须重构一堆东西,因为我使用了这个hack.使用fragmentTransaction.add会更好,如果你想让片段的UI在将另一个用户带到视图中时保持不变(甚至在顶部).fragmentTransaction.replace()意味着破坏片段的视图.....不要对抗系统. (2认同)
  • @VinceYuan - 我测试了Android 5.1上最新的v7-appcompat库,这留下了6个应该在我的活动的FragmentManager中删除的片段实例.即使GC能够正确处理它(我不相信它),这也会对你的应用程序以及整个设备造成不必要的内存压力.简单地使用.add()完全消除了对所有这些hacky代码的需求.这样做完全违背了FragmentTransaction.replace()首先要做的事情. (2认同)

kau*_*edi 51

我想有另一种方法可以实现您的目标.我不是说它是一个完整的解决方案,但它符合我的目的.

我所做的不是替换片段,而是添加了目标片段.所以基本上你将使用add()方法代替replace().

我还做了什么.我隐藏了我当前的片段,并将其添加到backstack.

因此,它在当前片段上重叠新片段而不破坏其视图.(检查它的onDestroyView()方法是否被调用.Plus添加它给backstate了我恢复片段的优势.

这是代码:

Fragment fragment=new DestinationFragment();
FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction ft=fragmentManager.beginTransaction();
ft.add(R.id.content_frame, fragment);
ft.hide(SourceFragment.this);
ft.addToBackStack(SourceFragment.class.getName());
ft.commit();
Run Code Online (Sandbox Code Playgroud)

AFAIK系统仅onCreateView()在视图被销毁或未创建时调用.但是在这里我们通过不从内存中删除它来保存视图.因此它不会创建新视图.

当你从Destination Fragment返回时,它将弹出最后一个FragmentTransaction删除顶部片段,这将使最顶层(SourceFragment)视图出现在屏幕上.

评论:正如我所说,这不是一个完整的解决方案,因为它不会删除源片段的视图,因此占用比平时更多的内存.但仍然有用.同时我们使用完全不同的隐藏视图而不是替换的机制这是非传统的.

所以它不是关于你如何维持状态,而是关于你如何维护视图.


Man*_*ngh 9

我会建议一个非常简单的解决方案。

获取 View 引用变量并在 OnCreateView 中设置 view。检查此变量中是否已存在视图,然后返回相同的视图。

   private View fragmentView;

   public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);

        if (fragmentView != null) {
            return fragmentView;
        }
        View view = inflater.inflate(R.layout.yourfragment, container, false);
        fragmentView = view;
        return view;
    }
Run Code Online (Sandbox Code Playgroud)


Teo*_*nke 6

我在包含地图的片段中遇到了这个问题,该地图有太多设置细节需要保存/重新加载.我的解决方案是基本上保持这个片段一直活跃(类似于@kaushal提到的).

假设您有当前的片段A并想要显示片段B.总结后果:

  • replace() - 删除片段A并将其替换为片段B.片段A将再次带到前面重新创建
  • add() - (创建和)添加片段B,它与片段A重叠,片段A在后台仍然有效
  • remove() - 可用于删除片段B并返回到A.片段B将在稍后调用时重新创建

因此,如果您想要将两个片段保存为"已保存",只需使用hide()/ show()切换它们即可.

优点:简单而简单的方法来保持多个碎片运行
缺点:您使用更多的内存来保持所有碎片的运行.可能遇到问题,例如显示许多大位图


use*_*311 5

onSaveInstanceState() 仅在配置更改时调用。

由于从一个片段更改为另一个片段,因此没有配置更改,因此没有调用onSaveInstanceState()。什么状态没有被保存?可以指定吗?

如果您在 EditText 中输入一些文本,它将自动保存。任何没有任何 ID 的 UI 项都是不应保存其视图状态的项。