結論
- RxJava で
Subscription
が帰ってくる処理を書くときはCompositeSubscription
に集めてunsubscribe
(購読解除)しましょう。 unsubscribe
(購読解除)したCompositeSubscription
を再利用したいときは、新しいインスタンスが必要になります。
今回の問題
RxJava の subscribe(購読)を使っていると null object reference
というエラーが出ることに遭遇しました。
よくみてみると、「subscribe()
の中で呼んでるオブジェクトが null
になっとるで」と言っているようですね。
Android の場合にこのようなパターンに嵌まる一つの原因として「Activity
/ Fragment
が破棄されている = 対象オブジェクトが消滅している」というものがあります。このような場面では Activity
/ Fragment
が破棄されるタイミングで unsubscrible()
を実行、subscribe()
の中に記述した処理が実行されないようにする必要がありました。
今回、私が嵌まってしまったのは FragmentPagerAdapter
以下 に属する Fragment
で Subscription
を扱うときでした。
そこで、以下のような対処をとってみました。
コード(Kotlin)
// 説明のため必要最小限のコードのみに止めています。 class SubFragment(): Fragment() { // CompositeSubscription は毎回再生成する必要があるので以下の定義しました。 lateinit private var subscriptions: CompositeSubscription // View が再活性化されたとき常に新しい CompositeSubscription を手に入れます。 override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) this.subscriptions = CompositeSubscription() } private fun initGenericArticlesSrl() { // Subscription 型を返す処理を実行します。 val subscription = something() // subscription を集めておきます。 this.subscriptions.add(subscription) } override fun onDestroyView() { super.onDestroyView() // View が破棄されたので集めた subscription たちをすべて unsubscribe します。 this.subscriptions.unsubscribe() } }
View が再活性化される都度 CompositeSubscription
を作り直している理由については以下で紹介した記事をご覧ください。
unsubscribe しただけで安心するのではなく、次回活性化時における適切な初期化も必要。
— バトルプログラマー柴田智也 (@tomoya_shibata) 2016年8月29日
CompositeSubscriptionは再利用できない #RxJava - Islands in the byte stream https://t.co/XWSElRA3zM
とくに Kotlin でコードを記述するときにはこの点に気をつける必要があります。
対処を取る最中、当初 subscriptions
の初期化は以下のようにしていました。
// 駄目な方法 private val subscriptions: CompositeSubscription = CompositeSubscription()
先の記事で解説されている通り、 unsubscribe
を実行された CompositeSubscription
の変数は、再利用したい場合新しいインスタンスを入れてあげる必要があります。
Kotlin において val
を使った定義は、コンパイル後のクラスでは final
を使った定義となってしまいます。そのため、これでは目的の挙動をしてくれません。
今回は FragmentPagerAdapter
以下の Fragment
で起きていたことなので、他 Fragment
との行き来では Fragment
のインスタンスが新たに作り直されるわけではないからです。
以上のことを踏まえて FragmentPagerAdapter
を利用した際に通過するライフサイクルメソッドを観測した結果、onViewCreated
のタイミングで初期化するのがよいだろうと判断したので、以下のようなものに置き換わりました。
lateinit private var subscriptions: CompositeSubscription // View が再活性化されたとき常に新しい CompositeSubscription を手に入れます。 override fun onViewCreated(view: View?, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) this.subscriptions = CompositeSubscription() }
以上です。