Android Navigation : DialogFragmentのレスポンスは setFragmentResultListenerで処理しましょうという話

※ 旧題 「Android Navigation : DialogFragmentのレスポンスは ResultReceiverで処理したほうが良くないですか? という話」

## 動機
「コールバックでDialogFragmentの結果を処理するなんてレガシー(笑」 と言われたこと。

とはいえ Navigationによる Dialogの定義と結果の取得は今まで非常に気に入らなかった。

## サンプルコード
GitHubにプロジェクトを置きました

https://github.com/Nunocky/NavigationAndDialogFragment

以下には結論を先に書き、推奨しない方法をそのあとに書く。

* 結論としたい方法 (app3)
* 一般的に紹介されている方法 (app)
* 先日提案したけど良くなかった方法 (app2) コメント参照

## 結論としたい方法 (app3)

setFragmentResult で結果をセットして、 呼び出し側は setFragmentResultListenerで結果を受け取る、という方法。

* [setFragmentResultを使ったFragment間のデータ受け渡し](https://star-zero.medium.com/setfragmentresult%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9Ffragment%E9%96%93%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E5%8F%97%E3%81%91%E6%B8%A1%E3%81%97-9223ddc1ca38)
* [setFragmentResult・setFragmentResultListenerでFragmentに結果を伝える](https://zenn.dev/yass97/articles/391c48b4a56066)

使い方は簡単で、

ダイアログ側で setFragmentResultする

```YesNoDialogFragment.kt
                setFragmentResult("Button1", bundleOf("result" to RESULT_OK))
```

ダイアログを呼び出すフラグメントは setFragmentResultListenerでそれを待ち受ける

```MainFragment.kt
        parentFragmentManager.setFragmentResultListener(
            "Button1",
            viewLifecycleOwner
        ) { requestKey: String, result: Bundle ->
            val retVal = result.getInt("result")
            Toast.makeText(requireActivity(), "Button1 - $retVal", Toast.LENGTH_SHORT).show()
        }
```

setFragmentResult / setFragmentResultListenerの第一引数は requestKeyという文字列を渡す。これが一致していることが必要。

requestKeyを変えれば同じダイアログを同じ画面から呼んで異なる処理をさせることもできるので、再利用性の観点でも問題がない。

![device-2021-11-20-133025.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847/fe35b017-01de-abae-bb48-57678fd32500.gif)

## 一般的に紹介されている方法 (app)

__個人的にお勧めしない方法です。__

ViewModelに LiveDataをもたせて ダイアログの結果を Fragmentと DialogFragmentで共有する方法が一般的。

```DialogResultViewModel.kt
class DialogResultViewModel : ViewModel() {
    val result = MutableLiveData<YesNoDialogFragment.Result>(YesNoDialogFragment.Result.Init)
}
```

DialogFragmentの実装はこんな感じ。 LiveDataに結果をセットする。

```YesNoDialogFragment.kt
class YesNoDialogFragment : DialogFragment() {

    sealed class Result {
        object Init : Result()
        object Yes : Result()
        object No : Result()
    }

    private val resultViewModel: DialogResultViewModel by activityViewModels()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(requireActivity())
            .setTitle("Title")
            .setMessage("Message")
            .setPositiveButton("Yes") { dialog, _ ->
                resultViewModel.result.value = Result.Yes
                dialog.dismiss()
            }
            .setNegativeButton("No") { dialog, _ ->
                resultViewModel.result.value = Result.No
                dialog.dismiss()
            }

        resultViewModel.result.value = Result.Init
        return builder.create()
    }
}
```

呼び出し側の Fragmentはこんな実装をする。 LiveDataを監視し、その変化に応じて処理を実行する。

```MainFragment.kt
        resultViewModel.result.observe(viewLifecycleOwner) {
            when (it) {
                YesNoDialogFragment.Result.Yes -> {
                    Toast.makeText(requireActivity(), "YES", Toast.LENGTH_SHORT).show()
                }
                YesNoDialogFragment.Result.No -> {
                    Toast.makeText(requireActivity(), "NO", Toast.LENGTH_SHORT).show()
                }
                else -> {
                }
            }
        }
```

確かにDialogの結果をFragmentで受けて処理できているが、 この実装方法はいくつかの理由で気に入らない。

+ 一つの画面で異なる目的同じ DialogFragmentを呼べない
* ViewModelの値が他の画面からでも操作できてしまう
* YesNoDialogFragmentに再利用性がない


## ResultReceiverを使ってはどうか? (app2)

__おすすめしない方法です。コメント参照__

従来の DialogFragmentのようにコールバックを呼び出し側から提供する方法がないだろうかと探していたら、  [ResultReceiver](https://developer.android.com/reference/android/os/ResultReceiver) というクラスを見つけた。 ResultReceiverは Parcelableなので Bundleに入れて DialogFragmentに渡すことができる。これにコールバック処理を入れて渡せば良いのでは?

```YesNoDialogFragment.kt
class YesNoDialogFragment : DialogFragment() {
    private val args: YesNoDialogFragmentArgs by navArgs()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        val builder = AlertDialog.Builder(requireActivity())
            .setTitle("Title")
            .setMessage("Message")
            .setPositiveButton("Yes") { dialog, _ ->
                dialog.dismiss()
                args.receiver?.send(0, null)
            }
            .setNegativeButton("No") { dialog, _ ->
                dialog.dismiss()
                args.receiver?.send(1, null)
            }

        return builder.create()
    }
```

呼び出し側はこう。ダイアログのボタン選択後の処理を Fragmentに書くことができる。


```MainFragment.kt
    private fun showDialog() {
        val resultReceiver = object : ResultReceiver(Handler(Looper.getMainLooper())) {
            override fun onReceiveResult(resultCode: Int, resultData: Bundle?) {
                when (resultCode) {
                    0 -> {
                        Toast.makeText(requireActivity(), "YES", Toast.LENGTH_SHORT).show()
                    }
                    1 -> {
                        Toast.makeText(requireActivity(), "NO", Toast.LENGTH_SHORT).show()
                    }
                    else -> throw IllegalArgumentException("unsupported resultCode")
                }
            }
        }

        val action = YesNoDialogFragmentDirections.actionGlobalYesNoDialogFragment(resultReceiver)
        findNavController().navigate(action)
    }
```

ナビゲーショングラフでは、YesNoDialogFragmentはグローバルアクションとして設定する。

```main_navigation.xml
    <fragment
        android:id="@+id/yesNoDialogFragment"
        android:name="org.nunocky.app2.YesNoDialogFragment"
        android:label="YesNoDialogFragment" >
        <argument
            android:name="receiver"
            app:argType="android.os.ResultReceiver" />
    </fragment>
    <action android:id="@+id/action_global_yesNoDialogFragment" app:destination="@id/yesNoDialogFragment"/>

```
![スクリーンショット 2021-11-14 午後9.26.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3847/c40cfba4-a787-6bcc-bf25-1bd03de1db79.png)

これで先程の

+ 一つの画面で異なる目的同じ DialogFragmentを呼べない
* ViewModelの値が他の画面からでも操作できてしまう
* YesNoDialogFragmentに再利用性がない

という問題は解決できているはず