如何正确将片段的实例状态保存在后堆栈中?


488

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

使用Fragments,这仅在非常特殊的情况下有效。具体来说,令人震惊的是替换片段,将其放回堆栈中,然后在显示新片段时旋转屏幕。据我了解,旧片段onSaveInstanceState()在被替换时不会收到调用,但仍以某种方式保持链接,Activity而当该方法View不再存在时,稍后会调用此方法,因此将我TextView的任何结果都查找到中NullPointerException

另外,我发现即使TextViews使用Fragments也可以,但Activity对s 保持引用不是一个好主意。在这种情况下,onSaveInstanceState()实际上可以保存状态,但是如果在隐藏片段时旋转屏幕两次,问题就会再次出现,因为onCreateView()在新实例中不会调用该片段。

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


1
这也是一个很好的例子,同时也有详细的解释。emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
我第二个emunee.com链接。它为我解决了棍子UI问题!
重演者罗布(Rob)

Answers:


541

要正确保存实例状态,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
    }

}

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

}

希望这可以帮助。


7
这对我来说很完美!这样就没有任何变通办法,没有黑客攻击。谢谢您的帮助,这使您数小时的搜索成功了。您的值的SaveInstanceState()片段,然后将片段保存在保存该片段的Activity中,然后还原:)
MattMatt 2014年

77
什么是mContent?
2014年

14
@wizurd mContent是一个Fragment,它是对活动中当前片段实例的引用。
ThanhHH 2014年

13
您能否解释一下这将如何将片段的实例状态保存到后堆栈中?那就是OP所要求的。
hitmaneidos 2014年

51
这与问题无关,将片段放入堆栈时不会调用onSaveInstance
Tadas Valaitis 2015年

87

这是我目前使用的方式...非常复杂,但至少可以处理所有可能的情况。如果有人感兴趣。

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

    /* (...) */

}

或者,总是有可能将View变量中的数据保持在被动s中,并且View仅将s用于显示它们,以使两者保持同步。不过,我认为最后一部分不是很干净。


68
这是到目前为止我找到的最好的解决方案,但是仍然存在一个(某种特殊的)问题:如果您有两个片段,A并且和BA当前在后退堆栈上并且B可见,那么您将失去A(一)如果您旋转显示器两次。问题在于onCreateView(),仅在这种情况下不会被调用onCreate()。因此,以后onSaveInstanceState()没有视图可以保存状态。必须先存储然后保存传入的状态onCreate()
devconsole 2013年

7
@devconsole我希望我能给你5条赞成票!这种轮换两次的事情已经杀死了我好几天了。
DroidT

感谢您的好评!我有一个问题。在此片段中实例化模型对象(PO​​JO)的最佳位置在哪里?
伦吉斯,2015年

7
为了帮助节省时间给别人,App.VSTUP并且App.STAV是代表的对象,他们正试图获得这两个字符串变量。范例:savedState = savedInstanceState.getBundle(savedGamePlayString);savedState.getDouble("averageTime")
Tanner Hallman

1
这是一件美事。
伊万(Ivan)

61

在最新的支持库上,这里讨论的解决方案都不再是必需的。您可以根据需要使用自己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);
    }
...
}

但是请注意,还原片段的隐藏状态时当前存在一个错误。如果您在活动中隐藏片段,则在这种情况下,您将需要手动恢复此状态。


2
您在使用支持库时是否注意到了此修复程序,还是在某处阅读了此修复程序?您是否可以提供更多信息?谢谢!
Piovezan

1
@Piovezan可以从文档中隐式推断出来。例如,beginTransaction()文档的内容如下:“这是因为该框架负责将当前片段保存为状态(...)”。我也已经用这种预期的行为来编码我的应用程序了一段时间了。
里卡多

1
如果使用ViewPager,@ Ricardo是否适用?
德里克·比蒂

1
通常是,除非您更改FragmentPagerAdapter或实现的默认行为FragmentStatePagerAdapterFragmentStatePagerAdapter例如,如果查看的代码,您将看到该restoreState()方法从FragmentManager创建适配器时作为参数传递的片段中还原片段。
里卡多

4
我认为这是对原始问题的最佳答案。我认为,它也是与Android平台的工作方式最匹配的一种。我建议将此答案标记为“已接受”,以更好地帮助将来的读者。
dbm

17

我只想给出我想出的解决方案,该解决方案可以处理我从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);
    }
}

由于在特殊的循环情况下不会调用destroyView,因此可以确定,如果它创建了状态,则应该使用它。

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

这部分将是相同的。

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

现在,这里是棘手的部分。在我的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);
}

现在,无论您想恢复状态到哪里,都可以使用saveStateState捆绑包

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

你能告诉我...什么是“ MyObject”?
kavie 2014年

2
您想要的任何东西。这只是一个表示将保存在分发包中的内容的示例。
DroidT 2014年

3

感谢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();

}

2)在您的片段中,您必须具有以下内容:

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

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

    //restore your data here

    return true;
}

3)例如,您可以在onActivityCreated中调用hasSavedState:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}

-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

1
与原始问题无关。
豪尔赫·埃尔南德斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.