BattleProgrammerShibata

ある日は誰かと戦い、ある日は何かと戦い、そしてある日は自分と戦うのだろう、そういう生き物。

Kotlin x RxJava x FragmentPagerAdapter で Subscription を適切に管理する #Android #Kotlin

結論

  • RxJava で Subscription が帰ってくる処理を書くときは CompositeSubscription に集めて unsubscribe (購読解除)しましょう。
  • unsubscribe(購読解除)した CompositeSubscription を再利用したいときは、新しいインスタンスが必要になります。

今回の問題

RxJava の subscribe(購読)を使っていると null object reference というエラーが出ることに遭遇しました。
よくみてみると、「subscribe() の中で呼んでるオブジェクトが null になっとるで」と言っているようですね。

Android の場合にこのようなパターンに嵌まる一つの原因として「Activity / Fragment が破棄されている = 対象オブジェクトが消滅している」というものがあります。このような場面では Activity / Fragment が破棄されるタイミングで unsubscrible() を実行、subscribe() の中に記述した処理が実行されないようにする必要がありました。

今回、私が嵌まってしまったのは FragmentPagerAdapter 以下 に属する FragmentSubscription を扱うときでした。
そこで、以下のような対処をとってみました。

コード(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 を作り直している理由については以下で紹介した記事をご覧ください。

とくに 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()
}

以上です。