BattleProgrammerShibata

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

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

結論

  • RxJava で Subscription が帰ってくる処理を書くときはそれを集めてちゃんと unsubscribe しましょう。
  • Subscription を集めた CompositeSubscription が再度必要になる場合には新しく作り直す必要があります。

今回の問題

RxJava を利用した処理を雑に書く不届き行為をしていると、null object reference というエラーが出ることがあります。 「値をセットする対象オブジェクト呼んだけどその子 null 参照してるで」と言っているようですね。

この原因の一つとして、値をセットしようとしたオブジェクトがすでに消滅している(Activity の破棄など)にも関わらず、RxJava の処理結果の格納が試みられてしまったというのがあります。そのような場面では Activity や Fragment が破棄されるタイミングで unsubscrible を実行、実行中の処理を停止して続く処理にも行かないようにする必要がありました。

今回、私が嵌まってしまった FragmentPagerAdapter 以下 に属する Fragment たちに対しては以下のような対処をとってみました。

コード(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 を使った定義となってしまいます。次回 CompositeSubscription が必要となるときにメンバ変数の再定義が行われるような呼び出しが行われるならともかく、今回のケースではそうではありません。

今回は FragmentPagerAdapter 以下の 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()
}

以上です。