フラグメントのインスタンス状態をバックスタックに正しく保存するにはどうすればよいですか?


489

SOで同様の質問の多くの事例を見つけましたが、残念ながら私の要件を満たす回答はありません。

縦向きと横向きのレイアウトが異なり、バックスタックを使用しています。これにより、setRetainState()構成変更ルーチンを使用できなくなり、トリックを使用できなくなります。

デフォルトのハンドラーに保存されない特定の情報をTextViewでユーザーに表示します。アクティビティのみを使用してアプリケーションを作成する場合、以下がうまく機能しました。

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ます

また、sを使用しても問題TextViewsがなかったFragmentとしても、s を使用した参照を維持することは良い考えではありませんActivity。その場合、onSaveInstanceState()実際には状態が保存されますが、フラグメントが非表示のときに画面を2度回転するonCreateView()と、新しいインスタンスで呼び出されないため、問題が再発します。

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


1
こちらも非常に良い例で、詳細な説明もあります。emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
私はemunee.comリンクを2番目にします。それは私のためにスティックUIの問題を解決しました!
Reenactor Rob 2017

回答:


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()、フラグメントを保持するアクティビティにフラグメントを保存し、次に復元します:)
MattMatt

77
mContentとは何ですか?
wizurd 2014

14
@wizurd mContentはフラグメントであり、アクティビティ内の現在のフラグメントのインスタンスへの参照です。
ThanhHH 2014

13
これがフラグメントのインスタンス状態をバックスタックに保存する方法を説明できますか?それはOPが尋ねたものです。
hitmaneidos 2014年

51
これは質問とは関係ありません。フラグメントがバックスタックに置かれたときにonSaveInstanceが呼び出されません
Tadas Valaitis

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をそれらの表示にのみ使用して、2つの同期を保つことは常に可能です。でも、最後の部分はとてもきれいだとは思いません。


68
これは私がこれまでに見つけた最善の解決策であるが、1つの(ややエキゾチックな)問題の残りがまだある:あなたは二つの断片、持っている場合ABAbackstackに現在あるとB表示され、その後、あなたがの状態失うA(目に見えないし1)ディスプレイを2回回転させる場合。問題はonCreateView()、このシナリオでは呼び出されないことonCreate()です。したがって、後で、onSaveInstanceState()状態を保存するビューはありません。渡された状態を保存してから保存する必要がありonCreate()ます。
devconsole 2013

7
@devconsoleこのコメントに対して5票の投票をお願いします。この2度のローテーションは何日も私を殺しています。
DroidT 2013年

すばらしい答えをありがとう!ただ一つ質問があります。このフラグメントのモデルオブジェクト(POJO)をインスタンス化するのに最適な場所はどこですか?
Renjith、2015年

7
他の人のための時間節約のために、App.VSTUPそしてApp.STAV、彼らが取得しようとしているオブジェクトを表す文字列の両方のタグです。例:savedState = savedInstanceState.getBundle(savedGamePlayString);またはsavedState.getDouble("averageTime")
Tanner Hallman

1
これは美しさです。
Ivan

61

最新のサポートライブラリでは、ここで説明するソリューションは必要ありません。Activityを使用して、好きなようにのフラグメントで遊ぶことができますFragmentTransaction。フラグメントがIDまたはタグで識別できることを確認してください。

を呼び出すたびにフラグメントを再作成しない限り、フラグメントは自動的に復元されますonCreate()。代わりに、がsavedInstanceStatenullでないかどうかを確認し、この場合は作成されたフラグメントへの古い参照を見つける必要があります。

次に例を示します。

@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
この修正は、サポートライブラリの使用中に気付いたものですか、それともどこかで読んだものですか?それについて提供できる情報は他にありますか?ありがとう!
ピオベザン

1
@Piovezanは、ドキュメントから暗黙的に推測される可能性があります。たとえば、beginTransaction()ドキュメントは次のように読みます:「これは、フレームワークが現在のフラグメントを状態(...)に保存するためです」。私もかなり長い間、この予想される動作でアプリをコーディングしています。
Ricardo

1
@RicardoこれはViewPagerを使用している場合に適用されますか?
Derek Beattie、2015

1
FragmentPagerAdapterまたはの実装でデフォルトの動作を変更しない限り、通常はありますFragmentStatePagerAdapter。たとえばのコードをFragmentStatePagerAdapter見ると、アダプタの作成時にrestoreState()FragmentManagerメソッドがパラメータとして渡したフラグメントを復元していることがわかります。
Ricardo

4
この貢献は、元の質問に対する最良の答えだと思います。また、私の意見では、Androidプラットフォームの動作と最もよく一致しています。将来の読者のために、この回答を「承認済み」としてマークすることをお勧めします。
dbm 2017年

17

この投稿で紹介した、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);
    }
    ...
}

教えてもらえますか?ここで「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
}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.