Search code examples
androidrx-javakotlinrx-binding

Should I unsubscribe when using rxbinding?


There is how I use RxBinding with Kotlin:

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    reset_password_text_view.clicks().subscribe { presenter.showConfirmSecretQuestionBeforeResetPassword() }
    password_edit_text.textChanges().skip(1).subscribe { presenter.onPasswordChanged(it.toString()) }
    password_edit_text.editorActionEvents().subscribe { presenter.done(password_edit_text.text.toString()) }
}

Observable.subscribe(action) returns Subscription. Should I keep it as reference and unsubscribe onPause() or onDestroy()?

Like this:

private lateinit var resetPasswordClicksSubs: Subscription

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    resetPasswordClicksSubs = reset_password_text_view.clicks().subscribe { presenter.showConfirmSecretQuestionBeforeResetPassword() }
}

override fun onDestroy() {
    super.onDestroy()
    resetPasswordClicksSubs.unsubscribe()
}

Solution

  • I've made a small test setup to find it out. It's not an Android app but it simulates the class relationships. Here's what it looks like:

    class Context
    class View(val context: Context) {
        lateinit var listener: () -> Unit
        fun onClick() = listener.invoke()
    }
    
    fun View.clicks() = Observable.fromEmitter<String>({ emitter ->
        listener = { emitter.onNext("Click") }
    }, Emitter.BackpressureMode.DROP)
    
    
    var ref: PhantomReference<Context>? = null
    
    fun main(args: Array<String>) {
        var c: Context? = Context()
        var view: View? = View(c!!)
    
        view!!.clicks().subscribe(::println)
        view.onClick()
        view = null
    
        val queue = ReferenceQueue<Context>()
        ref = PhantomReference(c, queue)
        c = null
    
        val t = thread {
            while (queue.remove(1000) == null) System.gc()
        }
        t.join()
    
        println("Collected")
    }
    

    In this snippet I instantiate a View that holds a reference to a Context. the view has a callback for click events that I wrap in an Observable. I trigger the callback once, then I null out all references to the View and the Context and only keep a PhantomReference. Then, on a separate thread I wait until the Context instance is released. As you can see, I'm never unsubscribing from the Observable.

    If you run the code, it will print

    Click

    Collected

    and then terminate proving that the reference to the Context was indeed released.


    What this means for you

    As you can see, an Observable will not prevent referenced objects from being collected if the only references it has to it are circular. You can read more about circular references in this question.

    However this isn't always the case. Depending on the operators that you use in the observable chain, the reference can get leaked, e.g. by a scheduler or if you merge it with an infinite observable, like interval(). Explictly unsubscribing from an observable is always a good idea and you can reduce the necessary boilerplate by using something like RxLifecycle.