web-dev-qa-db-ja.com

結局のところ、どのように正しくバックスタックにフラグメントのインスタンスの状態を保存するには?

私は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になります。 。

また、自分のTextViewsへの参照を保持することは、たとえFragmentで問題ない場合でも、Activitysでは得策ではないことがわかりました。その場合、onSaveInstanceState()は実際に状態を保存しますが、フラグメントが隠されているときに画面をtimesに回転させると、新しいインスタンスでonCreateView()が呼び出されないため、問題が再び発生します。

onDestroyView()の状態をいくつかのBundle_クラスメンバー要素(実際には1つのTextViewだけではなくもっと多くのデータ)に保存し、thatonSaveInstanceState()に保存することを考えましたが、他に欠点があります。主に、フラグメントが現在表示されているの場合、2つの関数を呼び出す順序が逆になるため、2つの異なる状況を考慮する必要があります。より清潔で正しい解決策があるはずです。

440
The Vee

Fragmentのインスタンス状態を正しく保存するには、次のようにします。

1. フラグメントで、onSaveInstanceState()をオーバーライドしてインスタンス状態を保存し、onActivityCreated()に復元します。

@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()に復元する必要があります。

public void onCreate(Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        //Restore the fragment's instance
        mContent = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
        ...
    }
    ...
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    //Save the fragment's instance
    getSupportFragmentManager().putFragment(outState, "myFragmentName", mContent);
}

お役に立てれば。

501
ThanhHH

これは私が現時点で使用している方法です...それは非常に複雑ですが、少なくともそれはすべての可能な状況を処理します。誰かが興味を持っている場合に備えて。

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を使用して、2つのことを同期させることは常に可能です。ただし、最後の部分はそれほどきれいではありません。

79
The Vee

最新のサポートライブラリでは、ここで説明した解決策はもう必要ありません。 Activityを使って好きなようにあなたはあなたのFragmentTransactionのフラグメントで遊ぶことができます。あなたのフラグメントがidかtagで識別できることを確認してください。

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

ただし、フラグメントの非表示状態を復元するときには、現在 バグ があります。あなたの活動の中で断片を隠しているのなら、この場合は手動でこの状態を復元する必要があります。

52
Ricardo Lage

私が思いついた解決策は、Vasekとdevconsoleから派生した、この記事で提示されたすべてのケースを処理するためのものです。フラグメントが表示されていないときに電話が2回以上回転されたときにも、このソリューションは特殊なケースを処理します。

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

状態を復元したい場所は、savedStateバンドルを使用するだけです。

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

DroidT のおかげで、私はこれを作りました:

フラグメントが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
}
3
Noturno