CoroutineでDialogFragment

coroutineとViewModelを使ってDialogFragmentを実装する
https://qiita.com/FKbelm/items/43decadf95a4360b1d64

本記事の手法をつかうと、メモリリークなどが発生しやすいので上記の記事の手法を推奨します。

<details><summary>もとの記事は折りたたみます</summary><div>

DialogFragmentはコールバックで動いているのでコルーチンで書き直したくなりました。

すでに[DialogFragmentをコルーチンを使って実装する記事](https://qiita.com/idaisuke/items/b4f3c2e0a872544b97d0
)はありましたが、Rxを使用されていたのでkotlin coroutines標準機能のみで実現したいと思います。

## Callback to suspend

コールバックで結果を受け取っているのをsuspendで実現します。

C#-Prismの例ですが、[同様の処理を実現されているもの](https://blog.okazuki.jp/entry/2014/05/09/083315)がありましたのでこれをkotlinに移植します。

以下が実装例です。

```kotlin
abstract class BaseDialogFragment<T> : DialogFragment() {

    private val channel = Channel<T>()

    protected fun sendResult(result: T) = channel.offer(result)

    suspend fun showAndReceive(fm: FragmentManager, tag: String? = null): T {
        show(fm, tag)
        return channel.receive()
    }

    suspend fun receive(): T = channel.receive()

}
```

`BaseDialogFragment<T>`を継承したクラスで適切に`sendResult`することで、呼び出し側で`receive()`すれば結果を取り出せます。

`showAndReceive`はなくても大丈夫ですが、便利そうなので追加しました。

### C#との対応

|C#|kotlin|
|:---|:---|
| `await task`|`suspendFunction()`|
|`TaskCompletionSource<T>`|`Channel<T>`|
|`TaskCompletionSource<T>.SetResult`|`Channel<T>.send`<br>`Channel<T>.offer`|
|`await TaskCompletionSource<T>.Task`|`Channel<T>.receive`|

## 使用例

### DefaultAlertDialogFragment

Viewをカスタマイズせずに標準のAlertAlertDialogを使用するパターンです。

```kotlin
class DefaultAlertDialogFragment : BaseDialogFragment<Boolean?>() {
    // キャンセル時に結果を返さないとreceiveが終わらない
    override fun onCancel(dialog: DialogInterface?) {
        super.onCancel(dialog)
        sendResult(null)
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = AlertDialog.Builder(context!!).apply {
        arguments!!.also { args ->
            setTitle(args.getString(ARGS_TITLE))
            setMessage(args.getString(ARGS_MESSAGE))

            args.getString(ARGS_POSITIVE)?.let {
                setPositiveButton(it) { _, _ ->
                    sendResult(true)
                }
            }
            args.getString(ARGS_NEGATIVE)?.let {
                setNegativeButton(it) { _, _ ->
                    sendResult(false)
                }
            }
            args.getString(ARGS_NEUTRAL)?.let {
                setNeutralButton(it) { _, _ ->
                    sendResult(null)
                }
            }
        }
    }.create()


    companion object {
        const val ARGS_TITLE = "title"
        const val ARGS_MESSAGE = "message"
        const val ARGS_POSITIVE = "positive"
        const val ARGS_NEGATIVE = "negative"
        const val ARGS_NEUTRAL = "neutral"

        @JvmStatic
        fun newInstance(
            title: String? = null,
            message: String? = null,
            positiveButtonText: String? = null,
            negativeButtonText: String? = null,
            neutralButtonText: String? = null
        ) = DefaultAlertDialogFragment().apply {
            arguments = Bundle().apply {
                putString(ARGS_TITLE, title)
                putString(ARGS_MESSAGE, message)
                putString(ARGS_POSITIVE, positiveButtonText)
                putString(ARGS_NEGATIVE, negativeButtonText)
                putString(ARGS_NEUTRAL, neutralButtonText)
            }
        }
    }
}
```

```kotlin:呼び出し側
val alertDialogResult = DefaultAlertDialogFragment.newInstance(
    message = getString(R.string.message),
    positiveButtonText = getString(R.string.btn_yes),
    negativeButtonText = getString(R.string.btn_no)
).showAndReceive(supportFragmentManager)
if (finishDialogResult == true) {
    return "なんかやる"
}
```

## 参考

Coroutine時代のDialogFragment
https://qiita.com/idaisuke/items/b4f3c2e0a872544b97d0

Prism 5とReactiveProperty
https://blog.okazuki.jp/entry/2014/05/09/083315

</div></details>