【问题标题】:How to correctly save instance state of Fragments in back stack?如何在后台堆栈中正确保存 Fragments 的实例状态?
【发布时间】:2013-02-25 03:42:38
【问题描述】:

我在 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 不再存在时调用此方法,因此寻找任何我的TextViews 结果为NullPointerException

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

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

【问题讨论】:

  • 我支持 emunee.com 链接。它为我解决了一个 UI 问题!

标签: android android-fragments


【解决方案1】:

要正确保存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.还有重点,在activity中,你必须把fragment的实例保存在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);
    }

}

【讨论】:

  • 这对我来说非常有效!没有变通办法,没有黑客攻击,这样才有意义。谢谢您,使数小时的搜索成功。 SaveInstanceState() 将您的值保存在片段中,然后将片段保存在持有片段的 Activity 中,然后恢复 :)
  • @wizurd mContent 是一个 Fragment,它是对 Activity 中当前 Fragment 实例的引用。
  • 可能必须从getFragment()mContent = (ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState, "mContent"); 强制返回
  • 你能解释一下这将如何将片段的实例状态保存在后台堆栈中吗?这就是 OP 的要求。
  • 这与问题无关,将片段放入backstack时不会调用onSaveInstance
【解决方案2】:

这是一个非常古老的答案。

我不再为 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());
    }

    /* (...) */

}

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

【讨论】:

  • 这是迄今为止我找到的最好的解决方案,但仍然存在一个(有点奇怪)问题:如果你有两个片段,AB,其中A 是当前在 backstack 上并且B 是可见的,那么如果您将显示旋转两次,您将失去A 的状态(不可见的状态)。问题是onCreateView() 在这种情况下没有被调用,只有onCreate()。所以后来,在onSaveInstanceState() 中没有可以保存状态的视图。必须先存储然后保存在onCreate() 中传递的状态。
  • @devconsole 我希望我能给你 5 票赞成这个评论!这种轮换两次的事情已经让我好几天了。
  • 感谢您的精彩回答!我有一个问题。在这个片段中实例化模型对象 (POJO) 的最佳位置在哪里?
  • 为了帮助其他人节省时间,App.VSTUPApp.STAV 都是字符串标签,代表他们试图获取的对象。示例:savedState = savedInstanceState.getBundle(savedGamePlayString);savedState.getDouble("averageTime")
  • 这是一件美丽的事情。
【解决方案3】:

在最新的支持库中,这里讨论的解决方案都不再需要了。您可以使用FragmentTransaction 随意使用Activity 的片段。只需确保可以使用 id 或标签来识别您的片段。

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

这是一个例子:

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

但是请注意,在恢复片段的隐藏状态时,当前存在bug。如果您在 Activity 中隐藏了 Fragment,在这种情况下您需要手动恢复此状态。

【讨论】:

  • 这个修复是您在使用支持库时注意到的,还是您在某处读到过的?你能提供更多关于它的信息吗?谢谢!
  • @Piovezan 它可以从文档中隐式推断出来。例如,beginTransaction() doc 如下所示:“这是因为框架负责将您当前的片段保存在状态 (...) 中”。很长一段时间以来,我也一直在用这种预期的行为编写我的应用程序。
  • @Ricardo 如果使用 ViewPager,这是否适用?
  • 通常是的,除非您更改了FragmentPagerAdapterFragmentStatePagerAdapter 实现的默认行为。例如,如果您查看FragmentStatePagerAdapter 的代码,您会看到restoreState() 方法从您在创建适配器时作为参数传递的FragmentManager 恢复片段。
  • 我认为这个贡献是对原始问题的最佳答案。在我看来,它也是最符合 Android 平台工作方式的一种。我建议将 this 答案标记为“已接受”,以更好地帮助未来的读者。
【解决方案4】:

我只想提供我想出的解决方案,它可以处理我从 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 在这种情况下将为空。我通过重用保存在 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);
}

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

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

【讨论】:

  • 你能告诉我...这里的“MyObject”是什么?
  • 任何你想要的。这只是一个示例,表示将保存在包中的内容。
【解决方案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();

}

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

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

    【讨论】:

    • 与原问题无关。
    猜你喜欢
    • 1970-01-01
    • 2021-02-26
    • 2012-07-06
    • 1970-01-01
    • 1970-01-01
    • 2010-09-14
    • 2011-02-04
    • 2013-04-15
    • 1970-01-01
    相关资源
    最近更新 更多