ChatGPT解决这个技术问题 Extra ChatGPT

如何在后台堆栈中正确保存 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));
}

使用 Fragment,这仅适用于非常特定的情况。具体来说,最糟糕的是替换一个片段,将其放入后堆栈,然后在显示新片段时旋转屏幕。据我了解,旧片段在被替换时没有收到对 onSaveInstanceState() 的调用,但仍以某种方式与 Activity 链接,并且稍后在其 View 不再存在时调用此方法,因此寻找任何我的 TextView 的结果变成了 NullPointerException

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

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

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

A
Anye

要正确保存 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 是一个片段,它是对活动中当前片段实例的引用。
可能必须从 getFragment()mContent = (ContentFragment)getSupportFragmentManager().getFragment(savedInstanceState, "mContent");
你能解释一下这将如何将片段的实例状态保存在后堆栈中吗?这就是OP所要求的。
这与问题无关,将片段放入后台时不会调用 onSaveInstance
T
The Vee

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

我不再为 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 中,并仅使用 View 来显示它们,从而使两者保持同步。不过,我不认为最后一部分很干净。


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

在最新的支持库中,这里讨论的解决方案都不再需要了。您可以使用 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。如果您在活动中隐藏片段,则在这种情况下您需要手动恢复此状态。


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

我只想提供我想出的解决方案,它可以处理我从 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”是什么?
任何你想要的。这只是一个示例,表示将保存在捆绑包中的内容。
T
The Guy with The Hat

感谢 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
}

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

与原始问题无关。