Navigation ComponentでDialogを扱うときの注意

Navigation Componentでは、通常のFragmentと同様にDialogFragmentも扱えるようになっています。しかし、問題点もあるので、その解説をしたいと思います。

以下のような感じで <dialog> を使うだけです。

問題が発生するケース

DialogFragmentを単純に表示するだけでは、特に問題は起きません。

よくあるケースとして、DialogFragmentの結果によって、画面を閉じたり、次の画面に遷移したいケースがあると思います。そのときに問題が起こる可能性があります。

以下は setFragmentResultListener を使った例になります。共有の ViewModel や、単純な findNavController().currentBackStackEntry を使った場合も同様です。

このとき、次の画面に遷移しようとDestinationが見つからずにクラッシュします。そして popBackStack は何も起きません。

知っておいてほしいこと

解説の前に知っておいてほしいことが2つあります。

DialogFragmentを呼び出したときの、呼び出し元のLifecycleについて知っておいてほしいことがあります。

呼び出し元FragmentのLifecycleは、DialogFragmentを表示したとしても、状態は変わりません。変わらずに、 ON_RESUME の状態のままになります。

もう一つ知っておいてほしいことがあり、Navigation ComponentがDialogFragmentを表示するときに使用してる FragmentManger についてです。

Navigation Componentは <dialog> を使ってDialogFragmentを表示する際は、呼び出し元Fragmentの parentFragmentManager を使って呼び出しています。

原因

まず、 setFragmentResultListener ですが、ここが呼ばれるタイミングとしては、 Lifecycleが ON_START 以降になります。

また同様に、単純なfindNavController().currentBackStackEntry についても同様です。

以下は setFragmentResultListener の例で説明しています。

通常のFragmentの遷移の場合は、呼び出し元は一度 ON_DESTROY まで以降するため、遷移先のFragmentで setFragmentResult が実行されても遷移元の setFragmentResultListener はすぐに呼び出されません。遷移元Fragmentへ戻ってきてから呼び出されます。

DialogFragmentの場合ですが、前述しましたが、呼び出し元は ON_RESUME の状態になります。そのため、DialogFragmentで setFragmentResult が実行されると、すぐに呼び出し元の setFragmentResultListener が実行されることになります。
そのため、DialogFragmentがまだ表示されてる状態で、setFragmentResultListener が呼び出されていることになります。

上記の挙動から、DialogFragmentからの戻り値で次の画面への遷移や、画面を閉じようとしたときは、FragmentMangerとしては呼び出したDialogFragmentで行われてることになります。

Navigation Componentでは、DialogFragmentからのDestinationを探して見つからないという状態になります。

対応方法

この場合は公式ドキュメントに書いてあります。

https://developer.android.com/guide/navigation/navigation-programmatic#additional_considerations

詳細についてはドキュメントのほうを確認してください。
対応してる概要としては NavBackStackEntry のLifecycleを監視して対応する感じになります。だいぶ長めコードですが。

setFragmentResultListener や共有 ViewModelを使いたい場合の対応方法は現時点ではありません。(もし、知っていたらコメントもらえると嬉しいです)

ただし、Navigation Componentを使わなければ、 setFragmentResultListener でも問題ありませんが、少し注意があります。

まず、DialogFragmentを表示するときは、 childFragmentManager を使うようにします。ドキュメントにもそう記載されています。

SampleDialogFragment().show(childFragmentManager, TAG)

また、setFragmentResultListenerchildFragmentManager に対して行う必要があります。

これで問題は起きなくなります。

childFragmentManagerAlertDialog を使う場合、少し注意があるので、以前書いた記事を参考にしてみてください。

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO