Should I unsubscribe when using rxbinding?
Asked Answered
C

4

13

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()
}
Childhood answered 5/1, 2017 at 10:2 Comment(0)
S
13

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.

Scheller answered 5/1, 2017 at 15:23 Comment(1)
Thanks for such detailed explanation! In my case .editorActionEvents() still emitting events even after disposing View. I will try to use RxLifecycle to unsubscribe with onDestroy.Childhood
U
16

I think that Jake Wharton (the creator of the library) gave the best answer:

Treat a subscribed RxView.clicks() (or any Observable from this library for that matter) like you would the View reference itself. If you pass it (or subscribe to it) somewhere outside the lifetime of the View, you've just leaked your entire activity.

So if you're just subscribing inside your ViewHolder there's no need to unsubscribe just like there'd be no need to unregister a click listener were you doing it manually.

Undesirable answered 3/7, 2017 at 12:39 Comment(0)
S
13

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.

Scheller answered 5/1, 2017 at 15:23 Comment(1)
Thanks for such detailed explanation! In my case .editorActionEvents() still emitting events even after disposing View. I will try to use RxLifecycle to unsubscribe with onDestroy.Childhood
R
5

Yes, you should unsubscribe when using RxBinding.

Here's one way... (in java, could be tweaked for kotlin?)

Collect

Within your Activity or Fragment, add disposables to a CompositeDisposable that you'll dispose at onDestroy().

CompositeDisposable mCompD; // collector

Disposable d = RxView.clicks(mButton).subscribe(new Consumer...);

addToDisposables(mCompD, d); // add to collector

public static void addToDisposables(CompositeDisposable compDisp, Disposable d) {
    if (compDisp == null) {
        compDisp = new CompositeDisposable();
    }

    compDisp.add(d);
}

Dispose

@Override
protected void onDestroy() {
    mCompD.dispose();
    super.onDestroy();
}
Rivy answered 6/5, 2017 at 18:38 Comment(1)
Take great care if CompositeDisposable exists but is disposed, when adding another Disposable it will call disposable.dispose() immediately. That's why we should recreate the CompositeDisposable even if comDisp.isDisposed()Uigur
A
4

Yep, if you look in the doc, it explicitely says:

  • Warning: The created observable keeps a strong reference to view. Unsubscribe to free this reference.
Amaryllidaceous answered 5/1, 2017 at 10:17 Comment(4)
This doesn't answer the question. If the activity is destroyed and nothing is keeping a reference to it, the observable will be collected, too. The question is, does using RxBinding create any references to the activity that wouldn't be created if you just used the listener? My guess is no, unless you use some operators that run on special schedulers.Scheller
If you look at any view constructor, it takes a reference to the context. Therefore any unsubscribed binding will prevent your activity to be garbage collected.Amaryllidaceous
Not if the only reference is a loop. Which is exactly the same with passing an anonymous inner class.Scheller
This observable never completes and hold your reference until you unsubscribe.Amaryllidaceous

© 2022 - 2024 — McMap. All rights reserved.