前置き
カスタムしたDialogを実装することになったので、DialogFragment のボタンのリスナーをどうするか考えたり、調べたりしていました。
ちょっと前は Activity にコールバックのインターフェースを実装して、Fragment 側で getActivity() してキャスト! みたいなことしてた気がするんですが(そんなことはない??)、今はどうすべきなんだろうかと考えたわけです。
するとこんな記事を発見(1年前の記事ですが)
Android Architecture ComponentsのViewModelとHolderFragmentとActivity-Fragment間通信と。
AACのViewModelはActivityをKeyにしてStaticに取得できるため使い勝手がよさそうです。
こんな感じで実装
Activityではコールバックのインターフェースを実装することなくきれいにかけます。
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, "ゆうわく")
}
}
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
で扱ってしまったほうがよいのか
らへんです
どなたかアドバイスいただけるとうれしいです
Activity の再生成等にも耐えられて、いい感じに動いているように見えます
最後に
これ結構いいんじゃね? と思っていたら、こんな記事を発見してやるせない気持ちになりましたとさ。(上の記事の 2日後)
Android Architecture ComponentsのViewModelとDialogFragment
まぁでも、すごい人もこういっていることだし、間違ってないんじゃね? 的な自信が得られたので、良しとしましょう
ちなみに、先日ソースコードが公開された Google I/O 2018 の公式アプリ iosched でも ViewModel を使った実装が行われてました
たとえば こんな感じ
なにか間違っていることや、いやいやその実装やばいでしょ! 的なことがあれば教えてください