一劳永逸,如何在后台堆栈中正确保存Fragments的实例状态?

The*_*Vee 465 android android-fragments

我在SO上发现了许多类似问题的实例,但遗憾的是没有答案符合我的要求.

我有纵向和横向的不同布局,我正在使用后台堆栈,这两个都阻止我setRetainState()使用配置更改例程使用和技巧.

我在TextViews中向用户显示某些信息,这些信息不会保存在默认处理程序中.仅使用活动编写我的应用程序时,以下工作正常:

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}
Run Code Online (Sandbox Code Playgroud)

使用Fragments,这仅适用于非常特定的情况.具体来说,可怕的破坏是替换片段,将其放入后栈,然后在显示新片段时旋转屏幕.根据我的理解,旧片段onSaveInstanceState()在被替换时没有接收到调用但是以某种方式Activity与之相关联,并且此方法稍后在其View不再存在时调用,因此将我TextView的任何结果查找为a NullPointerException.

另外,我发现保留对我的引用对于s TextViews来说并不是一个好主意Fragment,即使它是正常Activity的.在这种情况下,onSaveInstanceState()实际上保存状态但是如果我在隐藏片段时旋转屏幕两次,则问题会重新出现,因为它onCreateView()不会在新实例中被调用.

我想把状态保存onDestroyView()到某个Bundle类型的类成员元素中(它实际上是更多的数据,而不仅仅是一个TextView)并保存,onSaveInstanceState()但还有其他缺点.首先,如果该片段当前显示,调用两个函数的顺序是相反的,所以我需要考虑两种不同的情况.必须有一个更清洁,更正确的解决方案!

Tha*_*hHH 516

要正确保存实例状态,Fragment请执行以下操作:

1.在片段中,通过覆盖onSaveInstanceState()和恢复来保存实例状态onActivityCreated():

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's state here
    }

}
Run Code Online (Sandbox Code Playgroud)

2.而且很重要的一点,在活动中,你必须保存片段的实例在onSaveInstanceState()和恢复onCreate().

class MyActivity extends Activity {

    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.

  • 什么是mContent? (73认同)
  • 这与问题无关,当片段被放到backstack时不会调用onSaveInstance (41认同)
  • @wizurd mContent是一个Fragment,它引用了活动中当前片段的实例. (14认同)
  • 你能解释一下这将如何保存后端堆栈中片段的实例状态吗?这就是OP所要求的. (12认同)
  • 这对我来说非常合适!没有变通方法,没有黑客攻击,只是这样才有意义.谢谢你,成功搜索了几个小时.在片段中保存值的SaveInstanceState(),然后将片段保存在包含片段的Activity中,然后恢复:) (6认同)
  • 可能必须从`getFragment()`转换,即`mContent =(ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState,"mContent");; (6认同)
  • 为什么我们需要在activity的onSaveInstanceState和onCreate中工作?将片段调用super将不会自动执行. (4认同)
  • 有人可以解释这个答案与保存在backstack中的片段状态有什么关系? (3认同)
  • @ThanhHH在`mContent`中恢复片段实例后,在哪里进一步使用`mContent`? (3认同)
  • 非常感谢你提到`putFragment`和`getFragment`.那些方法还不够出名! (3认同)
  • 在方向改变之后,它不存储后堆栈中的片段 (2认同)
  • 只有第二点足以保存片段状态. (2认同)
  • 我认为片段的变量应该在onCreate中恢复而不是在onActivityCreated中恢复.事实上,有些情况下没有调用onActivityCreated(即片段被恢复但是在BackStack中)但你需要正确恢复你的vars. (2认同)

The*_*Vee 82

这就是我现在使用的方式......它非常复杂但至少它可以处理所有可能的情况.如果有人有兴趣.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}
Run Code Online (Sandbox Code Playgroud)

或者,始终可以将数据显示在被动Views中的变量中,并View仅使用s来显示它们,使两者保持同步.不过,我不认为最后一部分很干净.

  • 这是迄今为止我发现的最好的解决方案,但仍然存在一个(有点奇特的)问题:如果你有两个片段,`A`和`B`,其中`A`目前在backstack上,而'B`是可见的,如果旋转显示_twice_,则会丢失'A`(不可见的)状态.问题是`onCreateView()`在这种情况下不会被调用,只有`onCreate()`.所以稍后,在`onSaveInstanceState()`中没有用于保存状态的视图.一个人必须存储然后保存在`onCreate()`中传递的状态. (67认同)
  • @devconsole我希望我能为你发表5条评论!这种旋转两次的事情已经杀了我好几天了. (6认同)
  • 为了帮助节省他人的时间,`App.VSTUP`和`App.STAV`都是字符串标签,代表他们试图获取的对象.示例:`savedState = savedInstanceState.getBundle(savedGamePlayString);`或`savedState.getDouble("averageTime")` (6认同)

Ric*_*rdo 56

在最新的支持库中,这里讨论的解决方案都不再是必需的.您可以Activity根据自己的喜好使用自己的片段FragmentTransaction.只需确保您的片段可以使用ID或标记进行标识.

只要您不尝试在每次调用时重新创建片段,片段都将自动恢复onCreate().相反,您应该检查是否savedInstanceState为null,并在这种情况下找到对创建的片段的旧引用.

这是一个例子:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}
Run Code Online (Sandbox Code Playgroud)

但请注意,恢复片段的隐藏状态时存在错误.如果要在活动中隐藏片段,则需要在这种情况下手动恢复此状态.

  • 我认为这个贡献是原始问题的最佳答案.它也是我认为最符合Android平台工作方式的那个.我建议将*this*答案标记为"已接受",以便更好地帮助未来的读者. (4认同)
  • 这是修复了您在使用支持库时注意到的内容,还是您在某处读到过它?你能提供更多关于它的信息吗?谢谢! (2认同)
  • 通常是的,除非您更改了“FragmentPagerAdapter”或“FragmentStatePagerAdapter”实现的默认行为。例如,如果查看“FragmentStatePagerAdapter”的代码,您将看到“restoreState()”方法从创建适配器时作为参数传递的“FragmentManager”恢复片段。 (2认同)

Dro*_*idT 16

我只想提供我想出的解决方案来处理本文中我从Vasek和devconsole派生的所有案例.此解决方案还可以处理特殊情况,当手机不能一次旋转而片段不可见时.

这是我存储捆绑以供以后使用,因为onCreate和onSaveInstanceState是片段不可见时唯一的调用

MyObject myObject;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}
Run Code Online (Sandbox Code Playgroud)

由于在特殊旋转情况下不调用destroyView,我们可以确定如果它创建状态我们应该使用它.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}
Run Code Online (Sandbox Code Playgroud)

这部分是一样的.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}
Run Code Online (Sandbox Code Playgroud)

现在这里是棘手的部分.在我的onActivityCreated方法中,我实例化了"myObject"变量,但是onActivity和onCreateView上的旋转都没有被调用.因此,当方向旋转多次时,myObject在这种情况下将为null.我通过重复使用onCreate中保存的相同包作为外包来解决这个问题.

    @Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}
Run Code Online (Sandbox Code Playgroud)

现在,只要您想要恢复状态,只需使用savedState包

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
Run Code Online (Sandbox Code Playgroud)

  • 任何你想要的东西.它只是一个表示将保存在包中的内容的示例. (2认同)

Not*_*rno 5

感谢DroidT,我做了这个:

我意识到如果 Fragment 不执行 onCreateView(),则不会实例化其视图。所以,如果返回堆栈上的片段没有创建它的视图,我保存最后存储的状态,否则我用我想要保存/恢复的数据构建我自己的包。

1)扩展这个类:

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}
Run Code Online (Sandbox Code Playgroud)

2)在你的片段中,你必须有这个:

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restore your data here

    return true;
}
Run Code Online (Sandbox Code Playgroud)

3)比如可以在onActivityCreated中调用hasSavedState:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Run Code Online (Sandbox Code Playgroud)