Gregory
Gregory

Reputation: 977

Using "Delegates.observable" with list of Observers

This article shows one example code using multiple handlers in one observer. I give a code example from this article below. But there is a memory leak error in this code - the handler is added to the mutablelist, but it is not deleted from the list when, for example, an object using one of the handlers is deleted from memory.

class WeatherStation {
    val temperatureChanged = mutableListOf<(Int) -> Unit>()

    var temperature: Int by Delegates.observable(0) { _, _, newValue ->
        temperatureChanged.forEach{it(newValue)}
    }
}

// ...

val weatherStation = WeatherStation()    
// Adding observer to the list, but where is its removal???
weatherStation.temperatureChanged.add { temperature ->
    println("Temperature changed: $temperature")
}

How to fix it, or are there alternative solutions? I need - so that when changing one property, several observers are invoked. Trying to use LiveData causes a lot of difficulties.

Upvotes: 1

Views: 1217

Answers (2)

Tenfour04
Tenfour04

Reputation: 93609

Traditionally, when something subscribes to something else, it is responsible for unsubscribing itself. You could do this by using an IdentityHashMap:

class WeatherStation {
    val temperatureChangedObservers = IdentityHashMap<Any, (Int) -> Unit>()

    var temperature: Int by Delegates.observable(0) { _, _, newValue ->
        temperatureChangedObservers.values.forEach { it(newValue) }
    }
}

// ...

val weatherStation = WeatherStation()    

weatherStation.temperatureChangedObservers.add(this) { temperature ->
    println("Temperature changed: $temperature")
}

// remove self as observer when going out of scope:
weatherStation.temperatureChangedObservers.remove(this)

I used IdentityHashMap rather than a MutableMap or HashMap so we won't have to worry about the possibility of two different observers possibly having object-equality.

If you want to automate unsubsribing, so you don't have to worry about it when your Fragment or Activity goes out of scope, you can require observers to be LifecycleOwners so you can observe their lifecycles. I didn't test this:

class WeatherStation: LifecycleObserver {
    private val temperatureChangedObservers = IdentityHashMap<LifecycleOwner, (Int) -> Unit>()

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onObserverDestroyed(source: LifecycleOwner) {
        temperatureChangedObservers.remove(source)
    }

    fun observeTemperature(observer: LifecycleOwner, action: (Int) -> Unit) {
        temperatureChangedObservers[observer] = action
        observer.lifecycle.addObserver(this)
    }

    var temperature: Int by Delegates.observable(0) { _, _, newValue ->
        temperatureChangedObservers.values.forEach { it(newValue) }
    }
}

// ...

val weatherStation = WeatherStation()    

weatherStation.observeTemperature(this) { temperature ->
    println("Temperature changed: $temperature")
}

Upvotes: 2

Gregory
Gregory

Reputation: 977

Thanks for the answer to the Tenfour04! I took his answer as a basis and made a simple universal class that maintains a list of observers. Class supports auto unsubsribing if the LifecycleOwner is used as a key. This is a simple alternative to LiveData.

class Visor<T>(initialValue: T): LifecycleObserver {
    private var value = initialValue
    private val observers = WeakHashMap<Any, (T) -> Unit>()

    fun subscribe(owner: Any, observer: (T) -> Unit) {
        if (owner is LifecycleOwner)
            owner.lifecycle.addObserver(this)
        observers[owner] = observer
    }

    fun subscribeAndInvoke(owner: Any, observer: (T) -> Unit) {
        add(owner, observer)
        observer(value) // invoke
    }

    fun remove(key: Any) {
        observers.remove(key)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onObserverDestroyed(owner: LifecycleOwner) {
        remove(owner)
    }

    operator fun getValue(thisRef: Any?, prop: KProperty<*>): T = value

    operator fun setValue(thisRef: Any?, prop: KProperty<*>, value: T) {
        this.value = value
        observers.values.forEach{it(value)} // invoke all observers
    }
}

// example of using

class WeatherStation() {
    var temperatureVisor = Visor<Int>(0)
    var temperature: Int by temperatureVisor
    // ...
}

// addition of the first observer
val weatherStation = WeatherStation()
weatherStation.temperatureVisor.subscribe(this) {
    Log.d("Visor", "New temperature: $it")
}

Upvotes: 0

Related Questions