John Doe
John Doe

Reputation: 31

Livedata observer are called forever even with removeObserver

I'm facing an issue which drives me crazy.

I have 4 fragments inside an activity. The logic is: FragA -> FragB -> FragC -> FragD -> FragA -> ...

I'm connected to websockets which post livedata values. To navigate from FragB to FragC, I'm waiting an event. The first time, everything works fine, the websockets is recieved, the event is triggered and I'm going to FragC.

But, the second time (after Frag D -> Frag A), if I go back to fragB, the same event is triggered once again. The user doesn't see FragB, and arrives on FragC.

This is the actual behavior but this is not the one I'm expected.

I have do some research and I think it's because the livedata is trigger twice in is normal behavior. And, it can be only dispatch on main thread, so if my fragment goes in the back stack, it will wait for it to be active again.

I have try to removeObserver in the onDestroyView(), it works and the observer is removed, but once the fragment goes again inside onActivityCreated() and I observe the livedata, the observer is instantanetely triggered... I always use "viewLifecycleOwner" as owner.

Is there any way to cancel a liveData execution if I ever go back on an instanciated fragment?

All my frags extends ScopeFragment :

abstract class ScopedFragment : Fragment(), CoroutineScope {
    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

My liveData:

class MyLiveDatas {
    private val _myLiveData = MutableLiveData<CustomType>()
    val myLiveData: LiveData<CustomType>
        get() = _myLiveData


    fun customTrigger(webSocketMessage: WebSocketMessage) {
        val createdCustomType = CreatedCustomType(webSocketMessage)
        _myLiveData.post(createdCustomType)
    }
}

My Fragment:

class MyFragment: ScopedFragment(), KodeinAware {

    override val kodein by closestKodein()
    private val myLiveData: MyLiveDatas by instance()

    private val myLiveDataObserver = Observer<CustomType> { customType ->
        ... my actions
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        myLiveDatas.myLiveData.observe(viewLifecycleOwner, myLiveDataObserver)
    }


    override fun onDestroyView() {
        super.onDestroyView()

        myLiveDatas.myLiveData.removeObserver(myLiveDataObserver)

        // I've also try removeObservers with viewLifecycleOwner
    }
}

Thanks a lot!

Upvotes: 1

Views: 1911

Answers (4)

EpicPandaForce
EpicPandaForce

Reputation: 81549

LiveData is analogous to a BehaviorRelay, and replays the last value it was told to hold.

LiveData is not LiveEvent, it's not designed for event dispatching.

A regular event bus, a PublishRelay, or something like EventEmitter are better suited for this problem.

Google has devised LiveData<Event<T>> and EventObserver, but if you ever use observe(lifecycleOwner, Observer { instead of observe(lifecycleOwner, EventObserver { it will misbehave, which shows that it's a code smell (LiveData<Event<T>> does not work with Observer, only EventObserver, but its observe method still accepts Observers.)

So personally I'd rather pull in that library EventEmitter I mentioned above, with the LiveEvent helper class.

// ViewModel
    private val eventEmitter = EventEmitter<Events>()
    val controllerEvents: EventSource<Events> = eventEmitter

// Fragment
    viewModel.controllerEvents.observe(viewLifecycleOwner) { event: ControllerEvents ->
        when (event) {
            is ControllerEvents.NewWordAdded -> showToast("Added ${event.word}")
        }.safe()
    }

Upvotes: 1

Mikael
Mikael

Reputation: 304

This article describes two ways to achieve what you want.

Alternative 1: Wrap your live data in a class that makes sure the value is only observed once.

/**
 * Used as a wrapper for data that is exposed via a LiveData that represents an event.
 */
open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Alternative 2: Use a custom live data class (SingleLiveEvent) that only emits the value once.

Upvotes: 0

Parth
Parth

Reputation: 791

Try to observe the LiveData at onCreate() of the Fragment lifecycle with lifecycle owner as Activity and remove the observer at onDestroy() of the Fragment lifecycle.

Or if that doesn't workout use Event class.

open class Event<out T>(private val content: T) {

var hasBeenHandled = false
    private set // Allow external read but not write

/**
 * Returns the content and prevents its use again.
 */
fun getContentIfNotHandled(): T? {
    return if (hasBeenHandled) {
        null
    } else {
        hasBeenHandled = true
        content
    }
}

/**
 * Returns the content, even if it's already been handled.
 */
fun peekContent(): T = content
}

Upvotes: 0

Jafar Sadiq SH
Jafar Sadiq SH

Reputation: 734

You need to use custom live data , in case you want single event

this is my custom mutable live data in one of my project and it is working

class SingleLiveEvent<T> : MediatorLiveData<T>() {

    private val observers = ArraySet<ObserverWrapper<in T>>()

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        val wrapper = ObserverWrapper(observer)
        observers.add(wrapper)
        super.observe(owner, wrapper)
    }

    @MainThread
    override fun removeObserver(observer: Observer<in T>) {
        if (observers.remove(observer)) {
            super.removeObserver(observer)
            return
        }
        val iterator = observers.iterator()
        while (iterator.hasNext()) {
            val wrapper = iterator.next()
            if (wrapper.observer == observer) {
                iterator.remove()
                super.removeObserver(wrapper)
                break
            }
        }
    }

    @MainThread
    override fun setValue(t: T?) {
        observers.forEach { it.newValue() }
        super.setValue(t)
    }

    private class ObserverWrapper<T>(val observer: Observer<T>) : Observer<T> {

        private var pending = false

        override fun onChanged(t: T?) {
            if (pending) {
                pending = false
                observer.onChanged(t)
            }
        }

        fun newValue() {
            pending = true
        }
    }
}

Upvotes: 3

Related Questions