結論
-
DialogFragment#show(manager, tag)
をoverrideしてフラグをオンにし、DialogFrament#onResume()
でオフにしろ- ただしダイアログ表示失敗に気をつけろ
問題:DialogFragmentが二重表示されてしまう
Androidにおいてダイアログというのは、周知のとおりDialog
を直接呼ぶのではなく(これは古いやりかた)、DialogFragment
を使って表示させる必要がある。
ところがこれはFragmentManager
を経由するので遅い。遅いというか、UIをロックしない。だから、ダイアログを表示しおわるまでのあいだにUI操作ができてしまう。
static String tag = "TAG"
private String settingValue = "sample";
public void onButtonClick(View v) {
MyDialogFragment f = MyDialogFragment.newInstance(settingValue);
f.show(getSupportFragmentManager, tag);
}
こんなことしていると、ボタン連打でMyDialogFragment
が大量に表示できてしまう。同じタグをちゃんと設定しているにもかかわらずだ。
対応Step1: タグをもとにほかのFragmentを消す
連打されても、同じタグのDialogFragment
は1つしか出したくないだろう。だから表示前にチェックして、同じタグのものは消しておくことにしよう。
public static void removeFragmet(FragmentManager manager, String tag) {
if (manager == null) return;
Fragment prev = manager.findFragmentByTag(tag);
if (prev == null) return;
if (prev instanceOf DialogFragment) {
Dialog dialog = prev.getDialog();
if (dialog != null && dialog.isShowing()) {
dialog.dismiss();
prev.onDismiss(dialog);
}
}
val ft = manager.beginTransaction();
ft.remove(prev);
ft.commitAllowingStateLoss();
manager.executependingTransactions();
}
これはなかなかいいが、問題の解決にはなっていない。生成されたFragment
がFragmentManager
に登録されるまでのあいだ、FragmentManager#findFragmentByTag(tag)
は何も返却しない。そのあいだに連打が入れば、そいつらはスルーされてしまう。
対応Step2:表示準備中フラグを立てる
これでは困るので、いまDialogFragmentを準備中である、というフラグを立てることにしよう。準備が終わったらフラグをオフにする。準備が終わったら、というのは、ダイアログによってUIがロックされ、このフラグに頼らなくても、ダイアログを表示させるボタンが操作できなくなったとき、ということだ。
abstract class GenericDialogFragment: AppCompatDialogFragment {
public static AtomicBoolean isSomeDialogShowing = AtomicBoolean(false);
@Override
public void show(FragmentManager manager, String tag) {
if (isSomeDialogShowing.getAndSet(true)) return;
super.show(manager, tag);
}
@Override
public void onResume() {
super.onResume();
isSomeDialogShowing.set(false);
}
}
あとはすべてのDialogFragment
を、これを継承して実装すればよさそうだ。
なお、バックグラウンドでの通信エラー発生時など、緊急でユーザ操作を求めたい場合は、このGenericDialogFragment#isSomeDialogShowing
フラグを念のため解除してから、当該の緊急ダイアログを表示させてやる必要がある。
対応Step3. 表示失敗時をケアする
……ところで、DialogFragment#show(manager, tag)
は、IllegalStateException
を吐いて失敗することがある。属するActivity
の#onSavedInstanceState()
が呼ばれたあとで呼ばれてしまった場合などだ。
一度でもそれが発生すると、ダイアログの#onResume()
までたどりつけないので、上記のGenericDialogFragment#isSomeDialogShowing
フラグはtrueになったまま、二度と戻ることがない。つまり、ほかのすべてのDialogFragment
は、フラグによって排除されて、表示されなくなる。
そこで、
@Override
public void show(FragmentManager manager, String tag) {
if (isSomeDialogShowing.getAndSet(true)) return;
try {
super.show(manager, tag);
} catch (Exception e) {
isSomeDialogShowing.set(false);
}
}
のように気をつかってやろう。
対応Step4. それでも……
ところが、いろんな画面でダイアログを出しまくっていると、これでもたまにGenericDialogFragment#isSomeDialogShowing
フラグがtrueになったままになることがある。
そこで、Activity#onCreate(savedInstanceState)
やActivity#onActivityResult(requestCode, resultCode, data)
など、ダイアログを出していないことがはっきりしているライフサイクルのタイミングで、GenericDialogFragment.isSomeDialogShowing.set(false)
をセットしてやるほうがいいかもしれない。
そもそもそんなUIはやめたほうがいいかも
しかし、そもそもの問題として、ボタンを押すたびにダイアログを出すというUIをがんばって実現すべきなのか、というのは疑わしい。ダイアログというのはユーザの操作の選択肢を奪って中断を強いるもので、UIとしてそんなにいいものではない。アラートをいっぱい出すWebページはブラクラとして忌み嫌われる。
緊急の対応を求めるならともかく(退会の確認とか、ユーザによる不正アクセスを検知したとか)、ユーザの操作の確認を求める程度のことであれば、確認ダイアログを挟むよりも、処理を受け付けつつSnackbar
で取り消しを可能にするなどのほうがよいかもしれない。DatePicker
みたいな複雑なUIも、ダイアログで処理するのはよろしくない。そもそも複雑なものを頻繁にユーザに入力させるべきではなく、デフォルトを設定しておくべきで、それを修正する場合は画面遷移にして、一手間遠いところに埋め込んでおく、というほうがいい気がする。
Material design guidelineは、どんなときダイアログを使うべきかを示している。その時点のコンテクストを妨げるものであるから、控えめに使え、というのが趣旨だ。ほんとうにダイアログでなければダメなのか、画面をロックせず(たとえば)ネットワーク通信の結果をあとで反映するだけにできないか、というのを考えなおすべきだろう。
参考:Dialogs - Components - Material design guidelines
Use dialogs sparingly because they are interruptive. Their sudden appearance forces users to stop their current task and focus on the dialog content. Not every choice, setting, or detail warrants interruption. Alternatives to dialogs include menus or inline expansion, both of which maintain the current context.