再びDialogFragmentとFragmentのコールバックな関係
前回、DialogFragmentとFragmentをコールバックで連携させるコードを書きました。
また、ダイアログ表示中に画面回転させると例外が発生する場合があることを述べました。
そこで今回は、同じような動作で且つ画面回転しても例外が発生しないコードを書いてみます。もちろん、他にもやり方はあると思いますが、アイデアの一つとして捉えてもらえればと思います。
方針としては、DialogFragmentとFragmentを直接的にコールバックで連携させるのではなく、それらの間にコールバックメソッドを貯めるクラスを用意します。仮にそのクラスをCallbackPoolと名づけます。呼び出し元のFragmentからこのCalbackPoolに対してコールバックメソッドを登録。そして、呼び出されたDaialogFragmentからはCallbackPoolにアクセスして登録済みのコールバックメソッドを引き取って実行します。その際は、直接メソッド名でやりとりするのではなく、メソッドとともに登録したコマンドでやりとりします。
つまり、「Fragment→【CallbackPool】←DialogFragment」という形でFragmentとDialogFragmentがなるべく疎に連携するようにします。画面回転してもCallbackPoolはロストされないためアプリケーションが落ちることはありません。
具体的には以下のコードになります。なお、厳密にするにはエラー処理等も書くべきですが、説明として簡素化したいため最小限のコードにしました。
// 呼び出し元のFragment public class MyFragment2 extends Fragment { // コールバックメソッドと同時に登録する16進数コマンド private final int ADD_TEXT = 0x01; private final int CLEAR_TEXT = 0x02; private Button mDispDialog; private TextView mSelectedText; // コールバックメソッドを登録するための領域 CallbackPool pool = CallbackPool.getInstance(); @Override public void onAttach(Activity activity) { super.onAttach(activity); // コールバックメソッドを登録 // (呼び出してもらいたいときのコマンドと、登録したいメソッド名と引数の型を登録) pool.addMethod(ADD_TEXT, set(this, "addText", Integer.class)); pool.addMethod(CLEAR_TEXT, set(this, "clearText")); } @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(); MyDialog2 dialog = MyDialog2.newInstance(); dialog.setTargetFragment(MyFragment2.this, 0); dialog.show(manager, "MyDialog"); } }); return v; } private CallbackMethod set(Object instance, String methodName, Class...params) { CallbackMethod cm = new CallbackMethod(); cm.set(instance, methodName, params); return cm; } // 登録対象のメソッド1つめ public void addText(Integer selectedId) { 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()); } // 登録対象のメソッド2つめ public void clearText() { mSelectedText.getEditableText().clear(); } }
// Fragmentから生成されるダイアログ public class MyDialog2 extends DialogFragment { private final int ADD_TEXT = 0x01; private final int CLEAR_TEXT = 0x02; // 選択されたラジオボタンのID int mCheckedId; // 呼び出し元のテキストエリアをクリアするボタン Button mClear; public static MyDialog2 newInstance() { return new MyDialog2(); } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { LayoutInflater inflater = getActivity().getLayoutInflater(); View view = inflater.inflate(R.layout.my_dialog, null, false); RadioGroup radioGroup = (RadioGroup) view.findViewById(R.id.radioGroupOptions); radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { public void onCheckedChanged(RadioGroup group, int checkedId) { mCheckedId = checkedId; } }); mClear = (Button) view.findViewById(R.id.btnClear); mClear.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 呼び出し元のテキストエリアを更新 // → CLEAR_TEXT(0x02)で登録されているコールバックメソッドを実行 CallbackPool pool = CallbackPool.getInstance(); pool.callMethod(CLEAR_TEXT); } }); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setTitle("MY DIALOG"); builder.setPositiveButton("OK", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // 呼び出し元のテキストエリアを更新 // → ADD_TEXT(0x01)で登録されているコールバックメソッドを実行 CallbackPool pool = CallbackPool.getInstance(); pool.callMethod(ADD_TEXT, mCheckedId); } }); builder.setNegativeButton("Cancel", null); builder.setView(view); return builder.create(); }
// コールバックメソッド単体を格納するクラス public class CallbackMethod { private Method method; private Object instance; public Class[] params; // メソッド登録 public void set(Object instance, String methodName, Class... params) { this.instance = instance; this.params = params; try { this.method = instance.getClass().getMethod(methodName, params); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } } public Object invoke(Integer... params){ if (instance == null || method == null) return null; // --- TODO --- // 厳密にはここでparamがmethodに対応した正しいパラメータになっているか、 // チェックする try { //メソッドを呼び出して実行 return method.invoke(instance, params); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return null; } }
// コールバックメソッドをいくつも貯めるクラス public class CallbackPool { private static HashMap<Integer, CallbackMethod> map = new HashMap<Integer, CallbackMethod>(); private static CallbackPool instance = new CallbackPool(); private CallbackPool() { } public static CallbackPool getInstance() { return instance; } // コールバックメソッド登録 public void addMethod(Integer cmd, CallbackMethod method) { map.put(cmd, method); } // コールバックメソッド呼び出し public void callMethod(Integer cmd, Integer...params) { CallbackMethod method = map.get(cmd); if(method.params.length == 0) { method.invoke(); } else { method.invoke(params); } } }
ざっくりと以上です。
コメント行にも書いてますが厳密にはエラー処理を随所に入れた方がよく、またAndroidのシステムテム的に作法として良いのかどうかも確信は持てません。一つのアイデアとして、DialogFragment表示に画面回転しても落ちない書き方を検討してみた次第です。