DialogFragmentとFragmentのコールバックな関係

DialogFragmentで設定した情報を、呼び出し元のFragmentに反映する方法のお話です。

例えば、あるFragment上にボタンがあるとします。

そのボタンをタップするとダイアログ(DialogFragment)が表示され、そこで何らかの操作ができるとします。

その操作に応じて呼び出し元のFragment上のビューを変更したいとします。

このような動きは、呼び出し元のFragmentとDialogFragmentをコールバックメソッドで連携させることで実現できます。わりと知られている方法かもしれませんが、次回書く予定のエントリーと対比させたいため一応コードを載せてみます。

// 呼び出し元のFragment
public class MyFragment extends Fragment implements OnOkClickListener{

    // ダイアログを表示するボタン
    private Button mDispDialog;
    // ダイアログで選択したものを反映するTextView
    private TextView mSelectedText;
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.my_fragment, container, false);

        mSelectedText = (TextView) v.findViewById(R.id.textSelected);        
        mDispDialog = (Button) v.findViewById(R.id.btnDispDialog);
        mDispDialog.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                FragmentManager manager = getActivity().getSupportFragmentManager();
                MyDialog dialog = MyDialog.newInstance();
                dialog.setTargetFragment(MyFragment.this, 0); // ★★★
                dialog.show(manager, "MyDialog");
            }
        });
        return v;
    }

    // コールバックされるメソッド
    @Override
    public void onOkClicked(Bundle args) {        
      int selectedId = args.getInt("KEY_MYDIALOG");
      String text = "none";
      
      switch (selectedId) {
          case R.id.radioDog:
              text = "いぬ";
              break;
          case R.id.radioMonkey:
              text = "猿";
              break;
          case R.id.radioPheasant:
              text = "キジ";
              break;
          default:
      }
      
      mSelectedText.setText(text.toString());      
    }    
}
// Fragmentから生成されるダイアログ
public class MyDialog extends DialogFragment {

    // 選択されたラジオボタンのID
    int mCheckedId;

    public static MyDialog newInstance() {
        return new MyDialog();
    }

    private OnOkClickListener mListener;
    public interface OnOkClickListener {
        public void onOkClicked(Bundle args);
    }
        
    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);        
        mListener = (OnOkClickListener) getTargetFragment();
        if (mListener instanceof OnOkClickListener == false) {
            throw new ClassCastException("実装エラー");
        }        
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        LayoutInflater inflater = getActivity().getLayoutInflater();
        View v = inflater.inflate(R.layout.my_dialog, null, false);
        RadioGroup radioGroup = (RadioGroup) v.findViewById(R.id.radioGroupOptions);

        radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                mCheckedId = checkedId;
            }
        });

        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder.setTitle("MY DIALOG");
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int which) {
                // 呼び出し元フラグメントのビューを更新
                Bundle arg = new Bundle();
                arg.putInt("KEY_MYDIALOG", mCheckedId);
                // MyFragmentのonOkClickedをコール
                mListener.onOkClicked(arg);
            }
        });
        builder.setNegativeButton("Cancel", null);
        builder.setView(view);
        return builder.create();
    }
}

今回のサンプルのようなシンプルなアプリであれば上記コードで問題なく動作するのですが、より複雑なレイアウト構造を持ったアプリで同様の実装をしてみると、ダイアログ表示中に画面回転させた時に例外が発生しました。例外の種類及び発生状況は以下のリンク先で述べられているものと似ています。
android - Failure saving state - target not in fragment manager (setTargetFragment) - Stack Overflow

どうやら呼び出し元FragmentのsetTargetFragment(上記コード★★★箇所)で設定したオブジェクトが、画面回転中にロストされたようにも見えます。Androidのシステムではデバイスの向きが変わり画面回転が発生するときは、Activityなどごっそりと再生成されるようで、その過程でsetTargetFragmentで設定したものがロストされたのかもしれません。正確なところは分かりません。

この辺りはつぶさに動作検証したりAndroidのコードを見たりして真の原因を追求すべきものかもしれませんが、実験的に別のロジックでスクリーンショットのものと同じ動作ができるようにしてみました。その内容については次回のエントリーにて書く予定です。