なんか同じようなこと度々やっては忘れている気がするので記録しておく。
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
}
}