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

543

我在SO上找到很多类似的问题,但是没有答案符合我的要求。

我有不同的纵向和横向布局,并且我正在使用返回栈,这两个因素都防止我使用setRetainState()和配置更改程序的技巧。

我在TextView中向用户展示 certain information,它们不会在默认处理程序中保存。当只使用Activities编写我的应用程序时,以下方法效果良好:

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

对于Fragment,这仅在非常特定的情况下有效。具体而言,在替换片段、将其放入后退堆栈并在显示新片段时旋转屏幕时会出现严重问题。据我所知,当被替换时,旧片段不会收到调用onSaveInstanceState()的通知,但仍然与Activity相关联,当其View不存在时稍后才调用此方法,因此查找任何TextView都会导致NullPointerException

此外,我发现即使在Activity中是可以的,在Fragment中保留对我的TextViews的引用也不是一个好主意。在这种情况下,onSaveInstanceState()实际上保存了状态,但如果我在片段隐藏时旋转屏幕两次,则问题会重新出现,因为它的onCreateView()在新实例中没有被调用。

我考虑在onDestroyView()中将状态保存到某个Bundle类型的类成员元素中(实际上是更多数据,而不只是一个TextView),并在onSaveInstanceState()中保存那个,但还有其他缺点。主要是,如果当前正在显示片段,则两个函数的调用顺序会颠倒,因此我需要考虑两种不同的情况。一定有更简洁和正确的解决方案!

7个回答

584

要正确保存 Fragment 的实例状态,您应该执行以下操作:

1. 在 Fragment 中,通过覆盖 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);
    }

}

8
这对我非常有效!没有任何变通方法或技巧,就是这样很合理。感谢您的帮助,让我成功地节省了数小时的搜索时间。在片段中使用SaveInstanceState()来保存您的值,然后将片段保存在持有该片段的Activity中,最后进行恢复 :) - MattMatt
14
@mContent 是一个 Fragment,它是对活动中当前片段实例的引用。 - ThanhHH
6
可能需要对getFragment()返回的结果进行强制类型转换,例如 mContent = (ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState, "mContent"); - ross studtman
15
你能解释一下如何在返回栈中保存一个片段的实例状态吗?这就是原帖作者所问的。 - hitmaneidos
64
此内容并非问题相关,当将片段放入返回堆栈时,不会调用onSaveInstanceState方法。 - Tadas Valaitis
显示剩余16条评论

92

这是一个非常老的回答。

我不再为Android编写代码,因此不能保证最近版本中的功能,并且不会有任何更新。

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

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来显示它们,保持两者同步。不过我认为这种方法不够简洁。


72
这是我目前找到的最佳解决方案,但仍存在一个(有些奇特的)问题:如果你有两个片段AB,其中A当前在后退栈中且B可见,则如果您将显示旋转“两次”,则会丢失A的状态(不可见)。问题在于,在这种情况下,onCreateView()不会被调用,只有onCreate()被调用。所以后来在onSaveInstanceState()中没有视图可以保存其状态。必须在onCreate()中存储并保存状态。 - devconsole
8
@devconsole,我真希望我能为你这条评论点赞5次!这个旋转两次的问题困扰了我好几天。 - DroidT
非常感谢您提供的精彩答案!但我有一个问题。在这个片段中,最好在哪里实例化模型对象(POJO)? - Renjith
@devconsole 你的评论救了我的一天..比所有回答问题的答案都更有帮助..非常感谢!! - PunitD
9
为了帮助他人节省时间,App.VSTUPApp.STAV都是字符串标记,代表它们所要获取的对象。例如:savedState = savedInstanceState.getBundle(savedGamePlayString);savedState.getDouble("averageTime") - hallmanitor
显示剩余3条评论

67

在最新的支持库中,不再需要讨论这里提到的任何解决方案。您可以使用FragmentTransaction自由地操纵Activity的片段。只需确保您的片段可以通过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() doc 的说明如下:“这是因为框架会保存您当前片段的状态(...)”。我也已经使用这种预期行为编写我的应用程序有一段时间了。 - Ricardo
1
@Ricardo 如果使用ViewPager,这是否适用? - Derek Beattie
2
通常是这样的,除非您更改了“FragmentPagerAdapter”或“FragmentStatePagerAdapter”的默认行为。例如,如果您查看“FragmentStatePagerAdapter”的代码,则会发现“restoreState()”方法从创建适配器时传递的“FragmentManager”中恢复片段。 - Ricardo
5
我认为这篇回答是原问题的最佳答案,同时也是我认为最符合Android平台工作方式的答案。我建议将答案标记为“已采纳”,以更好地帮助未来的读者。 - dbm
显示剩余3条评论

17

我只想提供我从Vasek和devconsole得出的解决方案,它可以处理此帖子中提出的所有情况。此解决方案还可以处理当手机旋转多次而片段不可见时的特殊情况。

这是我存储Bundle以备后用的位置,因为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中的相同bundle作为传出bundle来解决这个问题。

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

现在无论你想在哪里恢复状态,只需使用savedState捆绑包即可。

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

你能告诉我...这里的"MyObject"是什么? - kavie
2
任何你想要的都可以。这只是一个例子,代表着将保存在bundle中的某些内容。 - DroidT

5

感谢DroidT的帮助,我做到了这一点:

我发现如果片段没有执行onCreateView(),它的视图就不会被实例化。所以,如果回退栈上的片段没有创建它的视图,我保存最后存储的状态,否则我用我想要保存/恢复的数据构建自己的Bundle。

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) 在你的Fragment中,必须拥有以下代码:

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

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

    //restore your data here

    return true;
}

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

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}

0

使用这个:

  private var mData: String? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState != null) {
            mData= savedInstanceState.getString("Data")
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putString("mData", Data)
    }

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

1
与原问题无关。 - Jorge E. Hernández

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接