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

我写了一个在两个片段之间切换的虚拟活动。当你从片段 A 到片段 B 时,片段 A 会被添加到回栈中。然而,当我返回到 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);
}
}
}

添加了一些日志消息:

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

它从来不会调用 FragmentA.onSaveInstanceState,当您回击时,它会创建一个新的 FragmentA。但是,如果我在 FragmentA 上并锁定屏幕,那么会调用 FragmentA.onSaveInstanceState。太奇怪了... 我是不是错误地认为添加到后面堆栈的片段不需要重新创建?医生是这么说的:

然而,如果在删除片段时调用 addToBackStack () , 然后停止该片段,并在用户导航时恢复该片段 回来。

164020 次浏览

如果从回栈返回到片段,它不会重新创建片段,而是重新使用相同的实例,并从片段生命周期中的 onCreateView()开始,请参见 片段生命周期

因此,如果你想存储状态,你应该使用实例变量和 没有依赖于 onSaveInstanceState()

我想还有另外一种方法可以达到你想要的效果。 我没有说这是一个完整的解决方案,但它在我的情况下服务的目的。

我所做的不是替换碎片,而是添加目标碎片。 所以基本上你要用 add()方法代替 replace()

我还做了什么。 我隐藏了当前的片段,并将其添加到回栈中。

因此,它将新片段重叠在当前片段上,而不会破坏其视图。(检查它的 onDestroyView()方法没有被调用。另外,将它添加到 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();

AFAIK 系统只在视图被破坏或未创建时调用 onCreateView()。 但在这里,我们通过不从内存中删除视图来保存视图。因此它不会创建新视图。

当你从目的地片段回来时,它会弹出最后一个 FragmentTransaction删除顶部片段,这将使最顶部(源代码片段)的视图出现在屏幕上。

注释: 正如我所说,这不是一个完整的解决方案,因为它没有删除源代码片段的视图,因此占用了比平常更多的内存。但是,还是要为目的服务。此外,我们使用一个完全不同的机制隐藏视图,而不是替换它是非传统的。

所以这不是关于如何维护状态,而是关于如何维护视图。

与苹果的 UINavigationControllerUIViewController相比,谷歌在 Android 软件架构方面做得并不好。Android 关于 Fragment的文档帮助不大。

当您从片段 A 输入片段 B 时,现有的片段 A 实例不会被销毁。当您按下 Back in FragmentB 并返回到 FragmentA 时,我们不会创建一个新的 FragmentA 实例。将调用现有的 FragmentA 实例的 onCreateView()

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

下面的代码运行良好。它不仅保持了片段状态,而且还减少了 RAM 和 CPU 负载(因为我们只在必要时使布局膨胀)。我不能相信谷歌的样本代码和文档从来没有提到它,但 总是充气布局

版本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;
}
}

——2005年5月3日最新情况: ——

正如注释中提到的,有时 _rootView.getParent()onCreateView中为空,这会导致崩溃。正如 dell116所建议的那样,版本2在 onDestroyView ()中移除了 _ rootView。在 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();
}
}

警告! ! !

这是一个黑客! 虽然我在我的应用程序中使用它,你需要仔细测试和阅读评论。

只有在配置发生更改时才调用 onSaveInstanceState()

由于从一个片段转换到另一个片段,因此没有配置更改,所以没有对 onSaveInstanceState()的调用。哪个州没有被拯救?你能具体说说吗?

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

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

假设您有当前的片段 A,并希望显示片段 B。 总结后果:

  • Place ()-移除片段 A,并将其替换为片段 B。片段 A 一旦再次出现在前面,就会被重新创建
  • Add ()-(create 和) add 一个片段 B,它与片段 A 重叠,片段 A 仍然在后台活动
  • Remove ()-可以用来删除片段 B 并返回到 A 片段 B 将在稍后调用时重新创建

因此,如果您想同时“保存”这两个片段,只需使用 hide ()/show ()来切换它们。

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

在这里,由于片段中的 onSaveInstanceState在您将片段添加到回栈中时不会调用。恢复后的片段生命周期开始于 onCreateView和结束于 onDestroyView,而 onSaveInstanceStateonDestroyViewonDestroy之间调用。我的解决方案是在 ABc6中创建实例变量和 init。示例代码:

private boolean isDataLoading = true;
private ArrayList<String> listData;
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
isDataLoading = false;
// init list at once when create fragment
listData = new ArrayList();
}

看看 onActivityCreated:

public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if(isDataLoading){
fetchData();
}else{
//get saved instance variable listData()
}
}


private void fetchData(){
// do fetch data into listData
}
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener()
{
@Override
public void onBackStackChanged()
{
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
{
//setToolbarTitle("Main Activity");
}
else
{
Log.e("fragment_replace11111", "replace");
}
}
});




YourActivity.java
@Override
public void onBackPressed()
{
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.Fragment_content);
if (fragment instanceof YourFragmentName)
{
fragmentReplace(new HomeFragment(),"Home Fragment");
txt_toolbar_title.setText("Your Fragment");
}
else{
super.onBackPressed();
}
}




public void fragmentReplace(Fragment fragment, String fragment_name)
{
try
{
fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.Fragment_content, fragment, fragment_name);
fragmentTransaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
fragmentTransaction.addToBackStack(fragment_name);
fragmentTransaction.commitAllowingStateLoss();
}
catch (Exception e)
{
e.printStackTrace();
}
}

我的问题是相似的,但我克服了我没有保持碎片活着。假设您有一个有两个片段的活动-F1和 F2。F1开始时,让 say in 包含一些用户信息,然后在一些条件下 F2弹出,要求用户填写 附加属性-他们的电话号码。接下来,您希望该电话号码弹回到 F1并完成注册,但您意识到所有以前的用户信息丢失,您没有他们以前的数据。片段是从头开始重新创建的,即使在 onSaveInstanceState中保存了这些信息,在 onActivityCreated中也会返回 null。

解决方案: 将所需信息保存为调用活动的实例变量,然后将该实例变量传递到片段中。

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);


Bundle args = getArguments();


// this will be null the first time F1 is created.
// it will be populated once you replace fragment and provide bundle data
if (args != null) {
if (args.get("your_info") != null) {
// do what you want with restored information
}
}
}

接下来是我的示例: 在显示 f2之前,我使用回调将用户数据保存在实例变量中。然后我启动 F2,用户填写电话号码并按保存。我在活动中使用另一个回调,收集这些信息并替换我的片段 F1,这次是我可以使用的 已经包数据。

@Override
public void onPhoneAdded(String phone) {
//replace fragment
F1 f1 = new F1 ();
Bundle args = new Bundle();
yourInfo.setPhone(phone);
args.putSerializable("you_info", yourInfo);
f1.setArguments(args);


getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f1).addToBackStack(null).commit();


}
}

关于回调的更多信息可以在这里找到: https://developer.android.com/training/basics/fragments/communicating.html

第一个 : 只要使用 add 方法而不是 FragmentTransaction 类的 place 方法,那么你就必须通过 addToBackStack 方法添加 second Fragment 来堆栈

第二个 : 在反向单击时,必须调用 popBackStackImate ()

Fragment sourceFragment = new SourceFragment ();
final Fragment secondFragment = new SecondFragment();
final FragmentTransaction ft = getChildFragmentManager().beginTransaction();
ft.add(R.id.child_fragment_container, secondFragment );
ft.hide(sourceFragment );
ft.addToBackStack(NewsShow.class.getName());
ft.commit();
                                

((SecondFragment)secondFragment).backFragmentInstanceClick = new SecondFragment.backFragmentNewsResult()
{
@Override
public void backFragmentNewsResult()
{
getChildFragmentManager().popBackStackImmediate();
}
};

使用以下代码替换碎片:

Fragment fragment = new AddPaymentFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.frame, fragment, "Tag_AddPayment")
.addToBackStack("Tag_AddPayment")
.commit();

活动的 onBackPress ()是:

  @Override
public void onBackPressed() {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() > 1) {


fm.popBackStack();
} else {




finish();


}
Log.e("popping BACKSTRACK===> ",""+fm.getBackStackEntryCount());


}
Public void replaceFragment(Fragment mFragment, int id, String tag, boolean addToStack) {
FragmentTransaction mTransaction = getSupportFragmentManager().beginTransaction();
mTransaction.replace(id, mFragment);
hideKeyboard();
if (addToStack) {
mTransaction.addToBackStack(tag);
}
mTransaction.commitAllowingStateLoss();
}
replaceFragment(new Splash_Fragment(), R.id.container, null, false);

完美的解决方案,在堆栈中找到旧的片段,并加载它,如果存在于堆栈中。

/**
* replace or add fragment to the container
*
* @param fragment pass android.support.v4.app.Fragment
* @param bundle pass your extra bundle if any
* @param popBackStack if true it will clear back stack
* @param findInStack if true it will load old fragment if found
*/
public void replaceFragment(Fragment fragment, @Nullable Bundle bundle, boolean popBackStack, boolean findInStack) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
String tag = fragment.getClass().getName();
Fragment parentFragment;
if (findInStack && fm.findFragmentByTag(tag) != null) {
parentFragment = fm.findFragmentByTag(tag);
} else {
parentFragment = fragment;
}
// if user passes the @bundle in not null, then can be added to the fragment
if (bundle != null)
parentFragment.setArguments(bundle);
else parentFragment.setArguments(null);
// this is for the very first fragment not to be added into the back stack.
if (popBackStack) {
fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
} else {
ft.addToBackStack(parentFragment.getClass().getName() + "");
}
ft.replace(R.id.contenedor_principal, parentFragment, tag);
ft.commit();
fm.executePendingTransactions();
}

就像

Fragment f = new YourFragment();
replaceFragment(f, null, boolean true, true);

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

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

   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;
}

Kotlin 和 ViewBinding 解决方案

我正在使用 replace()backstack()方法进行片段事务处理。问题是 backstack()方法调用了上一个片段的 onCreateView,这导致了片段 UI 的重新构建。这里有一个解决方案:

private lateinit var binding: FragmentAdRelevantDetailsBinding


override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?
): View {
if (!this::binding.isInitialized)
binding = FragmentAdRelevantDetailsBinding.inflate(layoutInflater, container, false)
return binding.root
}

正确调用片段生命周期方法并使用 onSavedInstanceState ()可以解决这个问题。

即正确调用 onCreate ()、 onCreateView ()、 onViewCreated ()和 onSavedInstanceState () ,并在 onSaveInstanceState ()中保存 Bundle,然后在 onCreate ()方法中恢复它。

我不知道是怎么做到的,但对我来说没有任何错误。

如果有人能解释一下,我会非常感激的。




public class DiagnosisFragment extends Fragment {
private static final String TITLE = "TITLE";
private String mTitle;
private List mList = null;
private ListAdapter adapter;
public DiagnosisFragment(){}
public DiagnosisFragment(List list, String title){
mList = list;
mTitle = title;
    

}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if(savedInstanceState != null){
mList = savedInstanceState.getParcelableArrayList(HEALTH_ITEMS);
mTitle = savedInstanceState.getString(TITLE);
itemId = savedInstanceState.getInt(ID);
mChoiceMode = savedInstanceState.getInt(CHOICE_MODE);
}
getActivity().setTitle(mTitle);
adapter = (ListAdapter) new HealthAdapter(mList, getContext()).load(itemId);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.diagnosis_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ListView lv = view.findViewById(R.id.subLocationsSymptomsList);
lv.setAdapter(adapter);
}
    

@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
outState.putParcelableArrayList(HEALTH_ITEMS, (ArrayList) mList);
outState.putString(TITLE, mTitle);
}
}


对于那些寻找解决方案的人来说:

@Override
public void onDestroyView() {
Bundle savedState=new Bundle();
// put your data in bundle
// if you have object and want to restore you can use gson to convert it
//to sring
if (yourObject!=null){
savedState.putString("your_object_key",new Gson().toJson(yourObject));
}
if (getArguments()==null){
setArguments(new Bundle());
}
getArguments().putBundle("saved_state",savedState);
super.onDestroyView();
}

以及 onViewCreated ()方法:

Bundle savedState=null;
if (getArguments()!=null){
savedState=getArguments().getBundle("saved_state");
}
if (savedState!=null){
// set your restored data to your view
}