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)
        public void show(FragmentManager manager, String tag) {
            mDismissed = false;
            mShownByMe = true;
            FragmentTransaction ft = manager.beginTransaction();
            ft.add(this, tag);
            ft.commit();
        }
    

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

    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 を表示するようにします。

    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 アプリの開発時、私は以下の様な方法(もしくは、同様のライブラリを作成すること)でデバッグ時にログを出力しないようにしてきました。

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

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

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

以下、Android Studio と Eclipse での具体的な方法です本の中の tips を勝手に公開してしまいました。著者の方々申し訳ありません。
Android Studio に関してなど+αがあるのでご容赦いただきたいです。
皆様、他にも良い tips が載っていますので、是非本を買ってください。なお、著者の方々と私には一切の利害関係はございません。


Android Studio

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

proguard-rules.pro
-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
    buildTypes {
        release {
            runProguard true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }


Eclipse

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

proguard-project.txt
-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
proguard.config=${sdk.dir}/tools/proguard/proguard-android-optimize.txt:proguard-project.txt