DialogFragment のコールバックを（AAC の）ViewModelで行う  

# 前置き

カスタムしたDialogを実装することになったので、DialogFragment のボタンのリスナーをどうするか考えたり、調べたりしていました。  
ちょっと前は Activity にコールバックのインターフェースを実装して、Fragment 側で getActivity()  してキャスト！ みたいなことしてた気がするんですが（そんなことはない？？）、今はどうすべきなんだろうかと考えたわけです。

するとこんな記事を発見（1年前の記事ですが）  

[Android Architecture ComponentsのViewModelとHolderFragmentとActivity-Fragment間通信と。](http://sys1yagi.hatenablog.com/entry/2017/08/22/221721)

AACのViewModelはActivityをKeyにしてStaticに取得できるため使い勝手がよさそうです。  

# こんな感じで実装
Activityではコールバックのインターフェースを実装することなくきれいにかけます。


``` MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        TemptationDialogFragment.Builder()
                .title("仕事を始めてそろそろ2時間がたちます")
                .message("まだお仕事を続けますか？\nそろそろ家に帰りたくないですか？")
                .cancelable(false)
                .goHomeButton("帰ります", OnClickListener { _, _ ->
                    Toast.makeText(this, "よい夏休みを！！", Toast.LENGTH_LONG).show()
                })
                .continueButton("まだ続けます", OnClickListener { _, _ ->
                    Toast.makeText(this, "がんばってください！", Toast.LENGTH_LONG).show()
                })
                .create(this)
                .show(supportFragmentManager, "ゆうわく")
    }
}
```

``` TemptationDialogFragment.kt 

class TemptationDialogFragment: AppCompatDialogFragment() {

    companion object {
        private const val KEY_ARG_NAME = "ArgumentsKeyName"
        private const val KEY_ARG_TITLE = "ArgumentsKeyTitle"
        private const val KEY_ARG_MESSAGE = "ArgumentKeyMessage"
        private const val KEY_ARG_CANCELABLE = "ArgumentKeyCancelable"
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val act: FragmentActivity = activity ?: return super.onCreateDialog(savedInstanceState)
        val arg: Bundle = arguments ?: return super.onCreateDialog(savedInstanceState)
        val param: TemptationDialogFragmentViewModel.Param = ViewModelProviders.of(act).get(TemptationDialogFragmentViewModel::class.java)
                .params[arg.getString(KEY_ARG_NAME)]!!

        return AlertDialog.Builder(context!!).also { builder ->
            arg.run {
                getString(KEY_ARG_TITLE)?.let { builder.setTitle(it) }
                getString(KEY_ARG_MESSAGE)?.let { builder.setMessage(it) }
                this@TemptationDialogFragment.isCancelable = getBoolean(KEY_ARG_CANCELABLE)
            }
            param.goHomeButton?.run {
                builder.setPositiveButton(first) { dialog, which ->
                    second.onClick(dialog, which)
                    dismiss()
                }
            }
            param.continueButton?.run {
                builder.setNegativeButton(first) { dialog, which ->
                    second.onClick(dialog, which)
                    dismiss()
                }
            }
        }.create()
    }

     /**
     * TemptationDialogFragment のインスタンス生成をするBuilder
     */
    class Builder(private val dialogName: String = TemptationDialogFragment::class.java.simpleName) {

        private var _title: String? = null
        private var _message: String? = null
        private var _cancelable: Boolean = false
        private var _goHomeButton: Pair<CharSequence, DialogInterface.OnClickListener>? = null
        private var _continueButton: Pair<CharSequence, DialogInterface.OnClickListener>? = null

        fun title(title: String): Builder = apply{ _title = title }
        fun message(message: String): Builder = apply{ _message = message }
        fun cancelable(cancelable: Boolean): Builder = apply{ _cancelable = cancelable }
        fun goHomeButton(tag: String, listener: OnClickListener): Builder = apply{ _goHomeButton = Pair(tag, listener) }
        fun continueButton(tag: String, listener: OnClickListener): Builder = apply{ _continueButton = Pair(tag, listener) }

        fun create(activity: FragmentActivity): TemptationDialogFragment {

            // FragmentのArgumentsにパラメータを詰める
            val dialog = TemptationDialogFragment().apply {
                arguments = Bundle().apply {
                    putString(KEY_ARG_NAME, dialogName)
                    _title?.let { putString(KEY_ARG_TITLE, it) }
                    _message?.let { putString(KEY_ARG_MESSAGE, it)}
                    putBoolean(KEY_ARG_CANCELABLE, _cancelable)
                }
            }

            // ボタンに関するパラメータは ViewModel につめる
            ViewModelProviders.of(activity).get(TemptationDialogFragmentViewModel::class.java).also { viewModel ->
                viewModel.params[dialogName] = TemptationDialogFragmentViewModel.Param().also{ param ->
                    _goHomeButton?.let { param.goHomeButton = it }
                    _continueButton?.let { param.continueButton = it }
                }
            }

            return dialog
        }
    }

    /**
     * ボタン関係のパラメータを詰めるViewModel
     */
    class TemptationDialogFragmentViewModel: ViewModel() {
        class Param{
            var goHomeButton: Pair<CharSequence, DialogInterface.OnClickListener>? = null
            var continueButton: Pair<CharSequence, DialogInterface.OnClickListener>? = null
        }
        val params: MutableMap<String, Param> = mutableMapOf()
    }
}
```   

`TemptationDialogFragmentViewModel` に持たせる値を Map にしているのは、同じ `Activity` に複数のダイアログを関連付けできるようにするためです。
`TemptationDialogFragment.Builder` のコンストラクタで設定する `dialogName` を Key にしています。

個人的な悩みどころとしては、  

-  この `TemptationDialogFragment` は必ず `Builder` を使って生成してほしいけど、 `DialogFragment()` を継承している関係上 デフォルトコンストラクタ を private にできないので、外から `TemptationDialogFragment()` とできてしまう
- この際ボタン周りの処理だけでなく、 `arguments` に詰めているものも `ViewModel` で扱ってしまったほうがよいのか  

らへんです :thinking: 
どなたかアドバイスいただけるとうれしいです :bow:  

Activity の再生成等にも耐えられて、いい感じに動いているように見えます :raised_hands:

# 最後に

これ結構いいんじゃね？ と思っていたら、こんな記事を発見してやるせない気持ちになりましたとさ。（上の記事の 2日後）  
[Android Architecture ComponentsのViewModelとDialogFragment](http://sys1yagi.hatenablog.com/entry/2017/08/24/125416)  

まぁでも、すごい人もこういっていることだし、間違ってないんじゃね？ 的な自信が得られたので、良しとしましょう :thumbsup: 

ちなみに、先日ソースコードが公開された Google I/O 2018 の公式アプリ [iosched](https://github.com/google/iosched) でも ViewModel を使った実装が行われてました :sunglasses:

たとえば [こんな感じ](https://github.com/google/iosched/blob/2696fc7e06826ba2db72de243f0d63f83f4a29b5/mobile/src/main/java/com/google/samples/apps/iosched/ui/signin/NotificationsPreferenceDialogFragment.kt#L69)  

なにか間違っていることや、いやいやその実装やばいでしょ！ 的なことがあれば教えてください :pray: