ボタン連打でDialogFragmentを多重に表示させない為のナニか

# ボタン連打でDialogFragmentを多重に表示させない
- AndroidのUIで、ボタン押下によりダイアログを表示したいと思います。
- 以下のように実装したところ、ボタン連打をすることでDialogが複数表示されてしまいました(ToT)

```MainActivity.java
  int id = 0;
  @OnClick(R.id.button)
  public void onClick(Button button) {
     MyDialogFragment dialog = MyDialogFragment.newInstance(id++);
     dialog.show(getSupportFragmentManager() , "dialog");
  }
```


- 原因としては、 
    - ダイアログ表示が遅い(onCreateDialogに500msec程度かかる)
    - ダイアログ表示中かどうかをそもそもチェックしていない。
でした。

## 絶対コレ悩んでるのは自分だけではないはず

- 参考とさせて頂いた記事
    - @hackugyoさんの[ボタン連打でDialogFragmentを二重表示させない](https://qiita.com/hackugyo/items/217207dd7b6de0ad3779)
        - そのものズバリな記事。
    - @kojionilkさんの[DialogFragment 実装パターン](https://qiita.com/kojionilk/items/9584c012679f61569995)
        - DialogFragmentからFragmentへのコールバック方法を参考にさせていただきました。

### BaseDialogFragmentの実装 (extendsすれば重複が防げるモノ)
- やりたいこと
    - `表示元のFragment`から`DialogFragment`に対して引数を渡せること。
    - `DialogFragment`が正常終了(AlertDialogで各ボタンが押下)またはキャンセル(バック、ダイアログ外タップ)された場合、表示元の`Fragment`に対して`onActivityResult()`を利用してコールバックできること。
    - コールバック用の`Listener`を`Fragment`に持たせないこと！
    - `DialogFragment`を`show` or `showNow`した際に、既に同一のTAGの`DialogFragment`が存在する場合は先発優先とすること(後発は無視)

``` BaseDialogFragment.java
@DebugLog
public abstract class BaseDialogFragment extends DialogFragment {

    private static final String TAG = BaseDialogFragment.class.getSimpleName();

    static BaseDialogFragment newInstance(Class<? extends BaseDialogFragment> clazz, Bundle args) {
        BaseDialogFragment dialog = null;
        try {
            dialog = clazz.getDeclaredConstructor().newInstance();
            dialog.setArguments(args);
        } catch (Exception e) {
            Log.d(TAG, e.getMessage());
        }
        return dialog;
    }

    @Override
    public void show(@NonNull FragmentManager manager, String tag) {
        showNow(manager, tag); //showではなく、showNowを呼ぶ
    }

    @Override
    public void showNow(@NonNull FragmentManager manager, String tag) throws RuntimeException {
        if (isSameTagDialogShowing(manager,tag)) return;
        super.showNow(manager, tag);
    }

    /**
     * 同一TAGのダイアログが表示されているかどうかを判定する。
     * @return 同一TAGのダイアログが表示されている場合は true を返す。
     */
    private boolean isSameTagDialogShowing(@NonNull FragmentManager manager, String tag) {
        Fragment previousFragment = manager.findFragmentByTag(tag);
        if (previousFragment instanceof DialogFragment) {
            Dialog dialog = ((DialogFragment)previousFragment).getDialog();
            if (dialog != null && dialog.isShowing()) {
                return true;
            }
        }
        return false;
    }

    /**
     * AlertDialog用の汎用リスナー
     * @return 汎用リスナー。 呼び出し元のFragmentのonActivityResultを返す。
     */
    DialogInterface.OnClickListener getDefaultClickListener() {
        return (dialog, which) -> {
            dialog.dismiss();
            Intent intent = new Intent();
            if (getArguments() != null) {
                intent.putExtras(getArguments());  // intent に Dialog 実行時の引数を詰める
            }
            if (getTargetFragment() != null) {
                getTargetFragment().onActivityResult(getTargetRequestCode(), which, intent);
            }
        };
    }
}

```

- 実際の使用例(`BaseDialogFragment`を拡張した`Dialog`)はこちら
    - 基本的には普通の`DialogFragment`と同じ。
    - 制約事項: `show()`および`showNow()`はオーバーライドしてはならない。

``` MyDialogFragment.java
@DebugLog
public class MyDialogFragment extends BaseDialogFragment {

    private static final String ARGS_TITLE = "title";

    static MyDialogFragment newInstance(String title) {
        Bundle bundle = new Bundle();
        bundle.putString(ARGS_TITLE, title);
        return (MyDialogFragment)BaseDialogFragment.newInstance(MyDialogFragment.class, bundle); //要キャスト
    }

    @Override
    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), getTheme());
        builder.setPositiveButton("OK", getDefaultClickListener())
               .setTitle(getArguments().getString(ARGS_TITLE));
        return builder.create();
    }

}

```
- 実際の使用例(`Dialog`を呼び出す`Fragment`)はこちら
    - `DialogFragment`は引数を`setArguments()`および`getArguments()`でやり取りする必要がある。
    - `setTargetFragment()`で指定した`Fragment`に対して`onActivityResult()`をコールバックする。

``` MainFragment.java

    @OnClick(R.id.button)
    public void OnButtonClicked(Button b) {
        MyDialogFragment dialog = MyDialogFragment.newInstance("MyTitle");
        dialog.setTargetFragment(this, 100);
        dialog.show(getFragmentManager(), "dialog");
    }

    @Override
    public void onActivityResult(int targetRequestCode, int which, Intent intent) {
        //TODO: FragmationDialogが終了した際の処理を記載
    }
```

## デバッグ時に超助かったライブラリ [Hugo](https://github.com/JakeWharton/hugo)
- デバッグ時にメソッドがどの順序で呼ばれたり呼ばれなかったりしたのか、Logcatに出力してくれるライブラリ。
- クラスやメソッドに `@DebugLog`と書いておくだけ。

- １点だけ愚痴。HugoのREADME.mdに *"Disable logging temporarily by adding the following:"* との記載あり。

``` Build.gradle
hugo { enabled false } 
```

- ふむふむ。。。と思って記載したらビルドエラー。
- しばらく悩んだ後にGoogle先生に問い合わせたら、[でるわ](https://github.com/JakeWharton/hugo/issues/104)[でるわ](https://github.com/JakeWharton/hugo/issues/125)。。。READMEには記載したけど未実装とのこと。
- **未実装の機能を READMEに書くな！**(せめて注釈つけてよ。。。)

# まとめ
- 「多重起動時に後発Dialogの表示を抑制」「呼び出し元Fragmentにコールバック可」機能を持った`BaseDialogFragment`を実装しました。
- 幾度となく行われた車輪の再発明ですが、実際に実装してみると勉強になります。