さて、Android開発者が嫌いなAPIランキングで堂々の一位を獲得したDialogFragment(当社調べ)ですが、皆さん特にコールバックの実装周りでいろいろ苦労されてると思います。
ただダイアログを出したいだけなのに何でこんな大変な思いをしなければならないのか。
ということで今流行りのコルーチンやRx(RxJava2)を使って実装してみます。
build.gradle(Module: app)の設定
まずはコルーチンとRxを使えるようにします。
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派生クラスを実装します。
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) }
}
}
使う方はこんな感じで
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
を参考に、というかコピペして次のクラスを作ります。
/**
* 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を次のように書き直します。
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だけだとこんな感じですね。
fun showAsSingle(fm: FragmentManager, tag: String? = null): Single<Int> {
show(fm, tag)
return subject.hide()
}
使う方は
button.setOnClickListener {
AlertDialogFragment().showAsSingle(supportFragmentManager).subscribe { result ->
Log.d("AlertDialogFragment", "ボタン($result)が押されました")
}
}
コルーチンもRxも便利ですね~。面倒くさいDialogFragmentの実装がかなり楽になりました。