なんか同じようなこと度々やっては忘れている気がするので記録しておく。
DialogFragment
を開いて、その操作結果でさらに別のダイアログを開きたくなったときなどのやり方。
問題
AlertDialog
(とか)を直接開いただけでは画面回転で閉じてしまう。そこでDialogFragment
を作成して、その中でAlertDialog
を作る。
こうすると画面回転後もDialogFragment
が再生成されて操作を続行できるわけだが、ダイアログの操作結果を受けてダイアログの呼び出し元側で色々するようにしているときにfragmentManager
やActivity
など再生成されるもの達の参照に問題が発生する。
分かりづらいので具体例。
問題ある例
呼び出し元
たとえば何らかのFragment
とかで、以下のようにしてダイアログを開くことにする。
fun openFooDialog(fragmentManager: FragmentManager) {
val dialog = FooDialogFragment.createInstance()
dialog.setPositiveAction {
// ダイアログのポジティブボタン押下で別のダイアログ`BarDialogFragment`を開く
// `FooDialogFragment`表示中に画面回転すると失敗する
openBarDialog(fragmentManager)
}
dialog.show(fragmentManager, DIALOG_TAG_FOO)
}
ダイアログ
ダイアログ自体は次のような感じ。BarDialogFragment
も同じ感じとする。
class FooDialogFragment : DialogFragment() {
companion object {
// 引数がある場合に`createInstance()`を経由して`setArguments()`したりしている
// 統一のため引数有無に関わらずすべての`Fragment`にこれを用意することにしている
fun createInstance() = FooDialogFragment()
}
// ------ //
// `lazyProvideViewModel`は`ViewModelFactory`をよしなにやるやつ
// 良い感じに`ViewModel`を生成しているだけなので別にどうでもいい
private val viewModel by lazyProvideViewModel {
DialogViewModel()
}
// ------ //
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_foo)
.setNegativeButton(R.string.dialog_cancel, null)
.setPositiveButton(R.string.dialog_ok) { _, _ ->
viewModel.positiveAction?.invoke()
}
.create()
}
// ------ //
/** ViewModelを扱っていいライフサイクルに達したらリスナを設定する */
fun setPositiveAction(l : (()->Unit)?) = lifecycleScope.launchWhenCreated {
viewModel.positiveActioin = l
}
// ------ //
class DialogViewModel : ViewModel() {
var positiveAction : (()->Unit)? = null
}
}
このような実装をしている場合、
dialog.setPositiveAction {
// ダイアログのポジティブボタン押下で別のダイアログ`BarDialogFragment`を開く
// `FooDialogFragment`表示中に画面回転すると失敗する
openBarDialog(fragmentManager)
}
の部分で参照しているfragmentManager
がopenFooDialog()
呼び出し時点のものであるため、画面回転などで破棄されて使用不可能な状態になってしまう。
同じ理由でActivity
やFragment
への参照もキャプチャするべきではない。
修正例
「操作完了時点の」 DialogFragment
を必ず結果とあわせて返すようにする。
その時点で生きている(アタッチしている)Activity
も呼び出し元Fragment
もこのインスタンスを通して取得することができる。
呼び出し元(修正後)
fun openFooDialog(fragmentManager: FragmentManager) {
val dialog = FooDialogFragment.createInstance()
dialog.setPositiveAction { f ->
// ダイアログのポジティブボタン押下で別のダイアログ`BarDialogFragment`を開く
// `FooDialogFragment`の`parentFragmentManager`なので、
// `openFooDialog()`に渡された`fragmentManager`に相当するものである(再生成されていたらインスタンスは別である)
openBarDialog(f.parentFragmentManager)
}
dialog.show(fragmentManager, DIALOG_TAG_FOO)
}
ダイアログ(修正後)
class FooDialogFragment : DialogFragment() {
companion object {
// 引数がある場合に`createInstance()`を経由して`setArguments()`したりしている
// 統一のため引数有無に関わらずすべての`Fragment`にこれを用意することにしている
fun createInstance() = FooDialogFragment()
}
// ------ //
// `lazyProvideViewModel`は`ViewModelFactory`をよしなにやるやつ
// 良い感じに`ViewModel`を生成しているだけなので別にどうでもいい
private val viewModel by lazyProvideViewModel {
DialogViewModel()
}
// ------ //
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_foo)
.setNegativeButton(R.string.dialog_cancel, null)
.setPositiveButton(R.string.dialog_ok) { _, _ ->
viewModel.positiveAction?.invoke(this)
}
.create()
}
// ------ //
/** ViewModelを扱っていいライフサイクルに達したらリスナを設定する */
fun setPositiveAction(l : ((FooDialogFragment)->Unit)?) = lifecycleScope.launchWhenCreated {
viewModel.positiveActioin = l
}
// ------ //
class DialogViewModel : ViewModel() {
var positiveAction : ((FooDialogFragment)->Unit)? = null
}
}