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

本文翻译自:Once for all, how to correctly save instance state of Fragments in back stack?

I have found many instances of a similar question on SO but no answer unfortunately meets my requirements. 我在SO上发现了许多类似问题的实例,但不幸的是,没有答案符合我的要求。

I have different layouts for portrait and landscape and I am using back stack, which both prevents me from using setRetainState() and tricks using configuration change routines. 我对人像和风景有不同的布局,并且正在使用后退堆栈,这既阻止了我使用setRetainState() ,也阻止了使用配置更改例程的技巧。

I show certain information to the user in TextViews, which do not get saved in the default handler. 我在TextViews中向用户显示某些信息,这些信息不会保存在默认处理程序中。 When writing my application solely using Activities, the following worked well: 当仅使用“活动”编写我的应用程序时,以下方法运行良好:

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

With Fragment s, this works only in very specific situations. 对于Fragment ,这仅在非常特殊的情况下有效。 Specifically, what breaks horribly is replacing a fragment, putting it in the back stack and then rotating the screen while the new fragment is shown. 具体来说,令人震惊的是替换片段,将其放回堆栈中,然后在显示新片段时旋转屏幕。 From what I understood, the old fragment does not receive a call to onSaveInstanceState() when being replaced but stays somehow linked to the Activity and this method is called later when its View does not exist anymore, so looking for any of my TextView s results into a NullPointerException . 据我了解,旧的片段在被替换时不会收到对onSaveInstanceState()的调用,而是以某种方式保持链接到Activity并且当其View不再存在时,稍后会调用此方法,因此寻找我的TextView的任何结果进入NullPointerException

Also, I found that keeping the reference to my TextViews is not a good idea with Fragment s, even if it was OK with Activity 's. 另外,我发现对于Fragment ,保留对TextViews的引用不是一个好主意,即使使用Activity也是可以的。 In that case, onSaveInstanceState() actually saves the state but the problem reappears if I rotate the screen twice when the fragment is hidden, as its onCreateView() does not get called in the new instance. 在那种情况下, onSaveInstanceState()实际上保存状态,但是如果在隐藏片段时旋转屏幕两次 ,问题就会再次出现,因为它的onCreateView()不会在新实例中调用。

I thought of saving the state in onDestroyView() into some Bundle -type class member element (it's actually more data, not just one TextView ) and saving that in onSaveInstanceState() but there are other drawbacks. 我想保存在该州的onDestroyView()到一些Bundle型的类成员元素(它实际上是更多的数据,而不仅仅是一个TextView ),并保存 onSaveInstanceState()但也有其他缺点。 Primarily, if the fragment is currently shown, the order of calling the two functions is reversed, so I'd need to account for two different situations. 首先, 如果当前显示的片段,称这两个函数的顺序是相反的,所以我需要考虑两种不同的情况。 There must be a cleaner and correct solution! 必须有一个更清洁,正确的解决方案!

Oh, yes: the / this . 哦,是的: / this


#1楼

参考:https://stackoom.com/question/12FlW/一劳永逸-如何正确地将Fragments的实例状态保存在后台堆栈中


#2楼

This is the way I am using at this moment... it's very complicated but at least it handles all the possible situations. 这是我目前使用的方式...非常复杂,但至少可以处理所有可能的情况。 In case anyone is interested. 如果有人感兴趣。

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

    /* (...) */

}

Alternatively , it is always a possibility to keep the data displayed in passive View s in variables and using the View s only for displaying them, keeping the two things in sync. 另外 ,始终可以将变量在被动View显示的数据保留在变量中,并且仅使用View来显示它们,以使两者保持同步。 I don't consider the last part very clean, though. 不过,我认为最后一部分不是很干净。


#3楼

To correctly save the instance state of Fragment you should do the following: 要正确保存Fragment的实例状态,您应该执行以下操作:

1. In the fragment, save instance state by overriding onSaveInstanceState() and restore in onActivityCreated() : 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
    }

}

2. And important point , in the activity, you have to save the fragment's instance in onSaveInstanceState() and restore in onCreate() . 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);
    }

}

Hope this helps. 希望这可以帮助。


#4楼

I just want to give the solution that I came up with that handles all cases presented in this post that I derived from Vasek and devconsole. 我只想给出我想出的解决方案,该解决方案可以处理我从Vasek和devconsole派生的所有案例。 This solution also handles the special case when the phone is rotated more than once while fragments aren't visible. 当手机旋转不止一次而碎片不可见时,此解决方案还可以处理特殊情况。

Here is were I store the bundle for later use since onCreate and onSaveInstanceState are the only calls that are made when the fragment isn't visible 这是我存储该软件包以供以后使用的原因,因为在片段不可见时会进行唯一的调用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);
    }
}

Since destroyView isn't called in the special rotation situation we can be certain that if it creates the state we should use it. 由于在特殊的循环情况下不会调用destroyView,因此可以确定,如果它创建了状态,则应该使用它。

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

This part would be the same. 这部分将是相同的。

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Now here is the tricky part. 现在这里是棘手的部分。 In my onActivityCreated method I instantiate the "myObject" variable but the rotation happens onActivity and onCreateView don't get called. 在我的onActivityCreated方法中,我实例化了“ myObject”变量,但是旋转发生在onActivity和onCreateView上,但没有被调用。 Therefor, myObject will be null in this situation when the orientation rotates more than once. 因此,当方向旋转一次以上时,myObject在这种情况下将为null。 I get around this by reusing the same bundle that was saved in onCreate as the out going bundle. 我可以通过重复使用在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);
}

Now wherever you want to restore the state just use the savedState bundle 现在,无论您想恢复状态到哪里,都可以使用saveStateState捆绑包

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

#5楼

On the latest support library none of the solutions discussed here are necessary anymore. 在最新的支持库上,这里讨论的解决方案都不再是必需的。 You can play with your Activity 's fragments as you like using the FragmentTransaction . 您可以根据需要使用FragmentTransaction来处理ActivityFragmentTransaction Just make sure that your fragments can be identified either with an id or tag. 只需确保可以使用id或标签识别您的片段。

The fragments will be restored automatically as long as you don't try to recreate them on every call to onCreate() . 只要您不尝试在每次调用onCreate()重新创建片段,这些片段都会自动恢复。 Instead, you should check if savedInstanceState is not null and find the old references to the created fragments in this case. 相反,您应该检查savedInstanceState是否不为null并在这种情况下找到对创建的片段的旧引用。

Here is an example: 这是一个例子:

@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);
    }
...
}

Note however that there is currently a bug when restoring the hidden state of a fragment. 但是请注意,还原片段的隐藏状态时当前存在一个错误 If you are hiding fragments in your activity, you will need to restore this state manually in this case. 如果您在活动中隐藏片段,则在这种情况下,您将需要手动恢复此状态。


#6楼

Thanks to DroidT , I made this: 感谢DroidT ,我做到了:

I realize that if the Fragment does not execute onCreateView(), its view is not instantiated. 我意识到,如果Fragment不执行onCreateView(),则它的视图不会被实例化。 So, if the fragment on back stack did not create its views, I save the last stored state, otherwise I build my own bundle with the data I want to save/restore. 因此,如果后堆栈上的片段未创建其视图,则将保存上一个存储的状态,否则将使用要保存/还原的数据构建自己的捆绑包。

1) Extend this class: 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();

}

2) In your Fragment, you must have this: 2)在您的片段中,您必须具有以下内容:

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

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

    //restore your data here

    return true;
}

3) For example, you can call hasSavedState in onActivityCreated: 3)例如,您可以在onActivityCreated中调用hasSavedState:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值