4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DialogFragment のコールバックを(AAC の)ViewModelで行う

Posted at

前置き

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

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

Android Architecture ComponentsのViewModelとHolderFragmentとActivity-Fragment間通信と。

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

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

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

たとえば こんな感じ

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

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?