ページ

2013/05/02

[Android]DialogFragmentの罠を回避するAlertDialogFragment

DialogFragmentの罠

  1. setArgumentsしないと画面回転で値が保持されない
  2. setterなどで追加設定しても画面回転で値が保持されない
  3. ボタンクリックのコールバックリスナーが画面回転で保持されない

setArgumentsしないと画面回転で値が保持されない

public static AlertDialogFragment newInstance(int iconId, int titleId, int textId){
    AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
    Bundle args = new Bundle();
    args.putInt(KEY_DIALOG_ICON, iconId);
    args.putInt(KEY_DIALOG_TITLE, titleId);
    args.putInt(KEY_DIALOG_TEXT, textId);
    args.putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
    alertDialogFragment.setArguments(args);
    return alertDialogFragment;
}
Fragment 関係で newInstance メソッドをよく見る理由は
Fragment が再生成される際にデフォルトコンストラクタが呼び出されるので
newInstance メソッドに引数渡してその中で setArguments の使用を強制させるためらしい。
ということで上記のように記述すれば問題ない。

setterなどで追加設定しても画面回転で値が保持されない

public void setPositiveButtonText(int positiveButtonTextId){
    mPositiveButtonTextId = positiveButtonTextId;
    getArguments().putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
}
通常の setter で追加設定しても画面回転などで DialogFragment が再生成されると
新しいインスタンスを生成するっぽいのでメンバ変数に保持しても意味が無い。
arguments から復元しているっぽいので後から追加しておけば良いんじゃねーのとやってみたら保持された。

ボタンクリックのコールバックリスナーが画面回転で保持されない

public static interface DialogListener {
    /**
     * Positive ボタンが押されたイベントを通知する
     */
    public void onPositiveButtonClick();
}

@Override
public void onAttach(Activity activity){
    super.onAttach(activity);
    if(activity instanceof DialogListener == false){
        throw new ClassCastException("Activity not implements DialogListener.");
    }
    mListener = (DialogListener)activity;
}

@Override
public void onClick(DialogInterface dialog, int which){
    if(mListener != null){
        mListener.onPositiveButtonClick();
    }
}
onAttach でメンバ変数にセットしなおしてやれば問題ない。
呼び出し側の Fragment で DialogListener を implements してやれば良い。

参考

Y.A.M の 雑記帳: Android Fragment で setArguments() してるサンプルが多いのはなぜ?
ネタ帳 A.B.C: DialogFragmentメモ(2)
夜でもアッサム: DialogFragmentの落とし穴にはまらないための方法
コジオニルク - Android - DialogFragment

ソースコード

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;

public class AlertDialogFragment extends DialogFragment {

    public static interface DialogListener {
        /**
         * Positive ボタンが押されたイベントを通知する
         */
        public void onPositiveButtonClick();
    }

    /** 引数受け渡し用のキー */
    private static final String KEY_DIALOG_ICON                 = "dialogIcon";
    private static final String KEY_DIALOG_TITLE                = "dialogTitle";
    private static final String KEY_DIALOG_TEXT                 = "dialogText";
    private static final String KEY_DIALOG_POSITIVE_BUTTON_TEXT = "dialogPositiveButtonText";

    /** Positive ボタンのボタンテキストのリソースID */
    private static int          mPositiveButtonTextId           = android.R.string.ok;

    /** ボタンのクリックを通知するリスナー */
    private DialogListener      mListener                       = null;

    public static AlertDialogFragment newInstance(int iconId, int titleId, int textId){
        AlertDialogFragment alertDialogFragment = new AlertDialogFragment();
        Bundle args = new Bundle();
        args.putInt(KEY_DIALOG_ICON, iconId);
        args.putInt(KEY_DIALOG_TITLE, titleId);
        args.putInt(KEY_DIALOG_TEXT, textId);
        args.putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
        alertDialogFragment.setArguments(args);
        return alertDialogFragment;
    }

    public void setPositiveButtonText(int positiveButtonTextId){
        mPositiveButtonTextId = positiveButtonTextId;
        getArguments().putInt(KEY_DIALOG_POSITIVE_BUTTON_TEXT, mPositiveButtonTextId);
    }

    @Override
    public void onAttach(Activity activity){
        super.onAttach(activity);
        if(activity instanceof DialogListener == false){
            throw new ClassCastException("Activity not implements DialogListener.");
        }
        mListener = (DialogListener)activity;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState){
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setIcon(getArguments().getInt(KEY_DIALOG_ICON));
        builder.setTitle(getArguments().getInt(KEY_DIALOG_TITLE));
        builder.setMessage(getArguments().getInt(KEY_DIALOG_TEXT));
        builder.setPositiveButton(mPositiveButtonTextId, new OnClickListener(){
            @Override
            public void onClick(DialogInterface dialog, int which){
                if(mListener != null){
                    mListener.onPositiveButtonClick();
                }
            }
        });
        return builder.create();
    }
    
}

おまけ: 公式の罠?

DialogFragment | Android Developers で紹介されているサンプルで
prev を remove する FragmentTransaction が commit されていないので 何も起こらない?
連打すると普通に2つくらいダイアログが表示されるが、commit すると1つしか表示されないようになる。
private void showDailog(){
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    Fragment prevDialogFragment = getSupportFragmentManager().findFragmentByTag(TAG_DIALOG_INFO);
    if(prevDialogFragment != null){
        fragmentTransaction.remove(prevDialogFragment);
    }
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();
    AlertDialogFragment dialogFragment = AlertDialogFragment.newInstance(android.R.drawable.ic_dialog_info, R.string.dialogTitleInfo, R.string.dialogTextInfo);
    dialogFragment.setPositiveButtonText(R.string.dialogClose);
    dialogFragment.show(getSupportFragmentManager(), TAG_DIALOG_INFO);
}

※追記(2013/05/02)

AlertDialogFragment をもっと汎用的にしました。
[Android]DialogFragmentの汎用性を高めたAlertDialogFragment | DevAchieve