Coroutine時代のDialogFragment

さて、Android開発者が嫌いなAPIランキングで堂々の一位を獲得したDialogFragment(当社調べ)ですが、皆さん特にコールバックの実装周りでいろいろ苦労されてると思います。

ただダイアログを出したいだけなのに何でこんな大変な思いをしなければならないのか。

ということで今流行りのコルーチンやRx(RxJava2)を使って実装してみます。

## build.gradle(Module: app)の設定

まずはコルーチンとRxを使えるようにします。

```gradle:build.gradle
dependencies {
    // 省略

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.23.4"
    implementation 'io.reactivex.rxjava2:rxjava:2.1.17'
    implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'

    // 省略
}

kotlin {
    experimental {
        coroutines "enable"
    }
}
```

## `DialogFragment`の派生クラスの実装

コルーチンで使うことができるsuspend関数をもったDialogFragment派生クラスを実装します。

```kotlin:AlertDialogFragment.kt
class AlertDialogFragment : DialogFragment() {

    private val subject = SingleSubject.create<Int>()

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = { _: DialogInterface, which: Int ->
            subject.onSuccess(which)
        }

        return AlertDialog.Builder(activity)
                .setTitle("Title")
                .setMessage("Message")
                .setPositiveButton("Ok", listener)
                .setNegativeButton("Cancel", listener)
                .create()
    }

    suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
        show(fm, tag)
        subject.subscribe { it -> cont.resume(it) }
    }
}
```

使う方はこんな感じで

```kotlin:MainActivity.kt
button.setOnClickListener {
    launch(UI) {
        val result = AlertDialogFragment().showAsSuspendable(supportFragmentManager)
        Log.d("AlertDialogFragment", "ボタン($result)が押されました")
    }
}
```

簡単ですね。わざわざActivityにリスナーインターフェイス実装したりとかする必要ないです。

ただし、このコードには問題があって、画面回転などでFragmentが再生成されると`subject`が失われてしまうので正しく動作しません。

`subject`がちゃんと復元されるように`arguments`や`savedInstanceState`に保存する必要があって、それは`Serializable`か`Parcelable`でないといけません。

## `Serializable`な`Subject`の実装

`SingleSubject`(RxJava2)のクラス階層を見てみるとラッキーなことにフィールドにはSerializableな型しか使われてないようです。

そこで`SingleSubject`を参考に、というかコピペして次のクラスを作ります。

```Java:SerializableSingleSubject.java
/**
 *  SingleSubjectに`implements Serializable`と`serialVersionUID`を追加しただけ
 */
public final class SerializableSingleSubject<T> extends Single<T> implements SingleObserver<T>, Serializable {
    private static final long serialVersionUID = 1L;

    final AtomicReference<SerializableSingleSubject.SingleDisposable<T>[]> observers;

    @SuppressWarnings("rawtypes")
    static final SerializableSingleSubject.SingleDisposable[] EMPTY = new SerializableSingleSubject.SingleDisposable[0];

    @SuppressWarnings("rawtypes")
    static final SerializableSingleSubject.SingleDisposable[] TERMINATED = new SerializableSingleSubject.SingleDisposable[0];

    final AtomicBoolean once;
    T value;
    Throwable error;

    // 以下略

```

これを使ってAlertDialogFragmentを次のように書き直します。

```kotlin:AlertDialogFragment.kt
class AlertDialogFragment : DialogFragment() {

    private var subject = SerializableSingleSubject.create<Int>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        savedInstanceState?.let {
            subject = it["subject"] as SerializableSingleSubject<Int>
        }
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = { _: DialogInterface, which: Int ->
            subject.onSuccess(which)
        }

        return AlertDialog.Builder(activity)
                .setTitle("Title")
                .setMessage("Message")
                .setPositiveButton("Ok", listener)
                .setNegativeButton("Cancel", listener)
                .create()
    }

    override fun onSaveInstanceState(outState: Bundle?) {
        super.onSaveInstanceState(outState)
        outState?.putSerializable("subject", subject);
    }

    suspend fun showAsSuspendable(fm: FragmentManager, tag: String? = null) = suspendCoroutine<Int> { cont ->
        show(fm, tag)
        subject.subscribe { it -> cont.resume(it) }
    }
}
```

これで画面回転しても正しく結果が取れるようになりました。

コルーチンを使わないでRxだけだとこんな感じですね。

```kotlin:AlertDialogFragment.kt
fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
    show(fm, tag)
    return subject.hide()
}
```

使う方は

```kotlin:MainActivity.kt
button.setOnClickListener {
    AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
        Log.d("AlertDialogFragment", "ボタン($result)が押されました")
    }
}
```

コルーチンもRxも便利ですね～。面倒くさいDialogFragmentの実装がかなり楽になりました。