2014/08/17

onActivityResult 内で DialogFragment.show() を実行すると IllegalStateException が発生する

タイトルの通り、FragmentActivity.onActivityResult() 内で DialogFragment.show() を実行すると、以下の様に IllegalStateException が発生して落ちてしまいます。

Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

onActivityResult() が呼ばれる時には Activity は復活しているはずなので、IllegalStateException はおかしいと思うのですが…

調べてみると、Support Library v4 の既知のバグらしい。

確かに、android.support.v4.app.DialogFragmentandroid.app.DialogFragment に変更すると問題なく動作しました。

2.x 系のシェアは 15% 以下になってきたようなので、新規のアプリでは 2.x 系をサポートしないのが良いかもしれません。
とはいえ、既存のアプリは対応せざるを得ず、まだまだ Support Library を使う必要がありそうですが…

回避方法

回避方法を調べてみたのですが、大きく分けると次の 3通りに分類できそうです。

  1. Support Library を使わない
    新規で作成するのであれば、これが正攻法ですね。
    既存のアプリでも、2.x 系のサポートを終わらせて、移行するのも良いかもしれません。
    ただ、残念ながらそうもいかない場合も多いと思います。
  2. DialogFragment.show() を Override して使う
    DialogFragment.show() の中は以下のようになっています。

    android.support.v4.app.DialogFragment (Support Library rev. 20)
    1
    2
    3
    4
    5
    6
    7
    public void show(FragmentManager manager, String tag) {
        mDismissed = false;
        mShownByMe = true;
        FragmentTransaction ft = manager.beginTransaction();
        ft.add(this, tag);
        ft.commit();
    }

    そこで、DialogFragment の子クラスで以下のように show() を Override します

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public class TestDialog extends DialogFragment {
        // 省略
     
        @Override
        public void show(FragmentManager manager, String tag) {
            // mDismissed = false;
            // mShownByMe = true;
            try {
                Field mDismissedField = DialogFragment.class.getDeclaredField("mDismissed");
                mDismissedField.setAccessible(true);
                mDismissedField.set(this, true);
                Field mShownByMeField = DialogFragment.class.getDeclaredField("mShownByMe");
                mShownByMeField.setAccessible(true);
                mShownByMeField.set(this, true);
            } catch (NoSuchFieldException |
                    IllegalAccessException |
                    IllegalArgumentException e) {
                throw new RuntimeException(e);
            }
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commitAllowingStateLoss();
        }
     
        // 省略
    }

    commit()commitAllowingStateLoss() に書き換えたいだけなのですが、リフレクションを使わないといけないため、とても見にくくなってしまいました。
    mDissmissedmShownByMe を無視して書いてある例もありましたが、あまりお勧めはできません。

    この方法、Dialog の状態を保存しておいて、表示の時に復活させようとすると失敗します。
    また、Support Library を更新するたびに、DialogFragment.show() の内容を確認して、修正があったら小クラスの方にも修正を加えなければなりません。
    結構面倒くさいです。
  3. onActivityResult() 内では Dialog を表示せず、onResumeFragments() 内で表示する
    現時点で最も現実的な解はこれかもしれません。
    onActivityResult() 内で Dialog を表示することを諦め、フラグを設定するだけにします。
    そして、onResumeFragments() 内でフラグをチェックして Dialog を表示するようにします。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class MainActivity extends FragmentActivity {
        private static final String TEST_DIALOG_TAG = "dialog";
        private static final int NO_DIALOG = -1;
        private int mShowDialogNo = NO_DIALOG;
     
        // 省略
     
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data){
            mShowDialogNo = requestCode;
        }
     
        @Override
        protected void onResumeFragments() {
            super.onResumeFragments();
     
            switch (mShowDialogNo) {
            case 1:
                DialogFragment dialog = (DialogFragment) getSupportFragmentManager().findFragmentByTag(TEST_DIALOG_TAG);
                if (dialog == null) {
                    dialog = new TestDialog();
                }
                dialog.show(getSupportFragmentManager(), TEST_DIALOG_TAG);
                break;
            default:
                break;
            }
            mShowDialog = NO_DIALOG;
        }
    }

    記述量は増えてしまいますが、仕方がありません。

補足

一部情報では Support Library rev. 18 で修正されたとあったのですが、rev. 20 で確認したところ、修正されている形跡はありませんでした。
残念ながら、2014年8月16日現在でもこの問題は存在するようです。
2014/08/12

Android の Log をリリース時にだけ表示しないようにする方法

Android に限らず、プログラムを書く上で Log を仕込むのは重要な作業です。
しかし、デバッグ用に出力した Log はリリースビルドでは出力しないようにしたいものです。
Java にはプリプロセッサがありませんので、Android アプリの開発時、私は以下の様な方法(もしくは、同様のライブラリを作成すること)でデバッグ時にログを出力しないようにしてきました。

1
2
3
if (BuildConfig.DEBUG) {
   LOG.d(TAG, "This is a log message");
}

しかし、先日購入した 50 Android Hacks 開発現場ですぐに役立つヒントとコード に、もっとエレガントな方法が載っていました。

それは ProGuard を利用して、後から Log を削除する方法です。
確かに、Android の開発環境ではリリース時にしか ProGuard がかからないようになっていますから、うってつけの方法と言えます1

以下、Android Studio と Eclipse での具体的な方法です2

Android Studio

proguard-rules.pro に以下の記述を追加

proguard-rules.pro
1
2
3
4
5
6
7
8
-assumenosideeffects public class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
    public static *** wtf(...);
}

以下のように ProGuard を有効にします。
proguard-android-optimize.txt を使っているところに注意。デフォルトでは proguard-android.txt なので書き換える必要があります。

app/build.gradle
1
2
3
4
5
6
buildTypes {
    release {
        runProguard true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}


Eclipse

proguard-project.txt に以下の記述を追加。

proguard-project.txt
1
2
3
4
5
6
7
8
-assumenosideeffects public class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
    public static *** wtf(...);
}

以下のように ProGuard を有効にします。
proguard-android-optimize.txt を使っているところに注意。デフォルトでは proguard-android.txt なので書き換える必要があります。

project.properties
1
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt


  1. 何故、今まで気付かなかったのだろうか… 
  2. 本の中の tips を勝手に公開してしまいました。著者の方々申し訳ありません。
    Android Studio に関してなど+αがあるのでご容赦いただきたいです。
    皆様、他にも良い tips が載っていますので、是非本を買ってください。なお、著者の方々と私には一切の利害関係はございません。 
?