ボタン連打でDialogFragmentを二重表示させない

# 結論

* `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();
}
```

[参考ページ](http://eggdev.mobaegg.com/2012/08/blog-post_4.html)

これはなかなかいいが、問題の解決にはなっていない。生成された`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`フラグを念のため解除してから、当該の緊急ダイアログを表示させてやる必要がある。

[参考ページ](http://shirobakama.blogspot.com/2013/08/blog-post.html)

## 対応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](https://material.google.com/components/dialogs.html#dialogs-behavior)
> 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.