google-code-prettify

ラベル Fragment の投稿を表示しています。 すべての投稿を表示
ラベル Fragment の投稿を表示しています。 すべての投稿を表示

2015-06-10

Activityの再生成にまつわる処理を書くベストな方法

AndroidでActivityの再生成にまつわる問題をちゃんと解決しようとすると、非常に煩雑なコードになる。

その煩雑さを解消するクラスを作った。今の所、この方法がベストな方法だと思う。
Retain.java

以下、Activityの再生成に関する典型的な対処法とその問題点。

Activityの再生成のパターンは2つある


Activityは、以下の場合に再生成される。
  1. Configuration changeの時(画面の回転など)。プロセスは生きたまま。
  2. バックグラウンドにいる間にOSによってプロセスごと殺された後、アプリに戻ってきた時。
再生成後の新しいinstanceにデータを渡したいときにどう渡すかが問題となる。

方法1. onSaveInstanceStateでbundleに入れる

  • processが死んでいた場合でも、データが残っている。
  • Bundleに入る型しか保持できない(Integer, Boolean, String, Serializableなど)。
    例えば、通信処理スレッドを再生成後のinstanceに引き渡す、ということは直接はできない。
  • onSaveInstanceState()が呼ばれた後には、もうbundleに入れることができない。

方法2. staticな領域に覚えておく

  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityが2つ存在するときに、間違って別のActivityにデータを渡さないようにしなくてはならない。

方法3. retain用のFragmentを使う

setRetainInstance(true)なFragmentを用意して、そのretain用Fragmentにデータを覚えさせておく方法。 http://developer.android.com/guide/topics/resources/runtime-changes.html
  • processが死んでいない場合は、データは残っている。
  • processが死んでいた場合は、データは残っていない。
  • Activityに紐付いたデータ(ThreadにおけるThread Local Storageのように)なので、別のActivityに間違ってデータを渡してしまうことはない。
  • Activityがいなくなると、自動でFragmentも消してくれる。
方法3の問題点
  • あくまでActivityに結びついた情報なので、Activityに含まれるFragmentごとに別々のretain用fragmentをもたせる、と言ったことは非常にしにくい。
  • retain用Fragmentを削除するタイミングが難しい。下手なタイミングでFragmentTransactionをすると、IllegalStateExceptionが出る。
  • FragmentActivity, Fragmentを使わなくてはならない。
  • コードが読みにくい

解決方法

  • processが死んだ場合でも、できるだけ情報を残したい、
  • でも、bundleに入る情報以外も保持したい
  • 3の方法は嫌だ
となると結局1と2のハイブリッドな方法で行くしか無い。 1,2の問題点を解消しつつ、1も2も統一的な方法で扱えるためのクラスを作った。 Retain.java

使い方

private Retain<somedata> retain; // SomeDataは、ParcelableまたはSerializableであること

@Override
protected void onCreate(Bundle savedInstanceState) {
    retain = Retain.forSerializable()
    someData = retain.onCreate(savedInstanceState);
    if (someData == null) {
        someData = new SomeData()
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    retain.onSaveInstanceState(outState, someData);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    retain.onDestroy(this);
}

これだけで、someDataの情報はできるだけ保持されるし、画面回転等にも対処できる。

2013-06-07

onCreateの引数savedInstanceStateは、Fragmentが再生成したかどうかの判断には(厳密には)使えない

Fragmentの再生成が起こるときには、onCreate(Bundle savedInstanceState)のsavedInstanceStateが必ず!=nullになる。

…と思っていたのだが、そうはならない条件があることがわかった。
つまり、Fragmentが再生成されたにも関わらず、savedInstanceState=nullとなる場合がある。

再生成されたのにsavedInstanceState=nullになるのは、以下の2つの条件を満たした場合。
  1. FragmentがUIを持たない。つまり、onCreateViewでnullを返している
  2. onSaveInstanceState(Bundle outState)で、outStateに何もputしていない

要は、Bundleに誰も何もセットしていない場合には、再生成時のsavedInstanceStateはnullになる(空のBundleが渡されてくるわけではない)。

なお、Viewは自らの状態をBundleに保存しているので、FragmentがViewを持つ場合には通常Bundleは空にはならない。そのため、savedInstanceStateがnullかどうかによって、再生成かどうかを判断できる。

2013-04-18

Fragmentでloaderを使っている場合は、setRetainInstance(true)してはいけない

FragmentでsetRetainInstance(true)としていると、loaderのonLoadFinished()が呼ばれないことがある。
そのため、loaderを使っている場合は、setRetainInstance(true)としてはいけない。

自作アプリでloaderとsetRetainInstance(true)を混ぜて使っていて、なぜかonLoadFinished()が呼ばれないことがあるのでググってみたら、Dianne Hackborn(Googleの人)もloaderとsetRetainInstance()を混ぜて使うなと言っていた。

以下の方法で、「onLoadFinished()が呼ばれない場合」を再現できる(GalaxyNexus + JellyBeanで確認)。

ソースコード

Googleによるサンプルコードに従った書き方をしている。onActivityCreated()でinitLoader()をし、onLoaderReset()でswapCursor(null)している。

public class MyFragment extends ListFragment implements LoaderManager.LoaderCallbacks {

    private MyAdapter adapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        adapter = new MyAdapter(getActivity());
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        getLoaderManager().initLoader(0, null, this);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        setListAdapter(adapter);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        setListAdapter(null);
    }

    @Override
    public Loader onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), ...);
    }

    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
        adapter.swapCursor(data);
    }

    @Override
    public void onLoaderReset(Loader loader) {
        adapter.swapCursor(null);
    }
}

手順

  1. 携帯を横(landscape)にしておく。この状態ではListFragmentにCursorの中身が表示されている。
  2. 電源ボタンを押して、スリープに入れる。
  3. 携帯を縦に持ちかえる。電源ボタンを押してスリープを解除し、ロックも解除する。すると、ListFragmentの表示が空になっている。

何が起きているのか

電源ボタンを押してスリープに入れた時に、以下の順でcallbackが呼ばれる。

  1. onLoaderReset
  2. onDestroyView
  3. onViewCreated
  4. onActivityCreated

ロック画面が縦だからだと思うのだが、landscapeのActivityが破棄され、portlaitのActivityが再生成される。 onLoaderReset()は呼ばれているが、Activity再生成後のonLoaderFinish()は呼ばれていない。そのため、スリープとロックを解除すると、ListFragmentが空になっている。

対処方法

setRetainInstance(true)を呼ばない、というのが一番確実な対処方法だが、onActivityCreatedでinitLoader()ではなくrestartLoader()を呼ぶという方法でも、試した限り動作するようだ。ただしrestartLoader()を使う方法だと、多少無駄にqueryを行うことになる。

2012-11-01

DialogFragmentの落とし穴にはまらないための方法

AndroidにFragmentが導入されて以来、Dialogを直接使うことは非推奨となり、DialogFragmentを使うことが推奨されている。

Dialogはあまり深く考えずに使っても大丈夫だったのだが、DialogFragmentは落とし穴が多数あり、正しく使わないとアプリが落ちてしまう。具体的には、Fragmentの再生成が発生したときに正しく動作しなくなる。

以下では「FragmentからDialogFragmentを開き、結果(OK/Cancel)をFragmentに返す」という場合を例にして、DialogFragmentの落とし穴を回避するための書き方を説明する。

正しいコード

public class OkCancelDialog extends DialogFragment {

    static public interface OkCancelListener {
        void onDialogPositiveClick(long id);
        void onDialogNegativeClick(long id);
    }

    // Fragmentの再生成の時に呼ばれるので、引数なしのpublicなコンストラクタが必要
    public OkCancelDialog() {
    }

    // targetFragmentは、結果を受け取るFragment。OkCancelListenerを実装したFragmentであること。
    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        if (targetFragment != null && !(targetFragment instanceof Fragment)) {
            throw new RuntimeException("targetFragment is not Fragment");
        }
        // Fragmentの再生成後でも使いたい値は、bundleに入れてsetArgumentしておく。
        Bundle bundle = new Bundle();
        bundle.putLong("id", dataId);
        setArguments(bundle);
        // 結果を受け取るFragmentは、直接Fragmentの変数には入れずに、
        // setTargetFragment()/getTargetFragment()を使う
        setTargetFragment((Fragment)targetFragment, 0);
    }

    public long getDataId() {
        return getArguments().getLong("id", -1);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setMessage("Message");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                // getTargetFragment()は、Fragmentが再生成された場合でも、正しいinstanceを返してくれる。
                OkCancelListener listener = (OkCancelListener) getTargetFragment();
                listener.onDialogPositiveClick(getDataId());
                dialog.cancel();
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                OkCancelListener listener = (OkCancelListener) getTargetFragment();
                listener.onDialogNegativeClick(getDataId());
                dialog.cancel();
            }
        });
        return builder.create();
    }
}

間違い1 DialogFragmentをinner classにする

public class MyFragment extends Fragment {

    public class OkCancelDialog extends DialogFragment {
        ...
    }
}
Fragmentの再生成時には、自動的にFragmentのpublicな引数なしのコンストラクタが呼ばれる。inner classにするとFragmentの自動生成ができないため、落ちる。

staticなinner classにするのでも良いが、出来ればファイルを分けるほうが(間違えにくいので)良い。

間違い2 引数なしのコンストラクタを作らない


    public OkCancelDialog() {
    }
を省略、またはprivateにすると、Fragmentの再生成時に落ちる。Fragmentの再生成時には、外から引数なしのコンストラクタが自動で呼ばれるためである。

間違い3 結果を受け取るFragmentへの参照を直接保持する

    private OkCancelListener listener = null;

    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        listener = targetFragment;
        ...
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        ...
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                listener.onDialogPositiveClick(getDataId());
            }
        }
    }
上のようにすると、Fragmentの再生成時にはメンバ変数listenerはnullになってしまう。なぜなら新しく作られたFragmentは以前とは別のオブジェクトで、かつ引数なしのコンストラクタが呼ばれているためである。しかも結果を受け取るFragmentも再生成されて別のオブジェクトになっているので、もはやlistenerの値は意味を持たない。

setTargetFragment()/getTargetFragment()を使えば、Fragmentが再生成された場合でも、新しく生成されたFragmentの参照を取得することができる。

間違い4 setArguments()を使わずに、値をメンバ変数に覚えておく

    private long dataId = -1;

    public OkCancelDialog(long dataId, OkCancelListener targetFragment) {
        this.dataId = dataId;
        ...
    }
間違い3と似ているが、Fragmentが再生成されたときには、メンバ変数dataIdは-1になっている。再生成後でも使いたい値はBundleに入れ、setArguments()しておけばよい。