LoginSignup
14
13

More than 5 years have passed since last update.

Coroutine時代のDialogFragment

Posted at

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

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

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

build.gradle(Module: app)の設定

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

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派生クラスを実装します。

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) }
    }
}

使う方はこんな感じで

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

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

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

subjectがちゃんと復元されるようにargumentssavedInstanceStateに保存する必要があって、それはSerializableParcelableでないといけません。

SerializableSubjectの実装

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

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

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を次のように書き直します。

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だけだとこんな感じですね。

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

使う方は

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

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

14
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
13