Search code examples
androidkotlinandroid-architecture-componentsandroid-livedataandroid-viewmodel

Android ViewModel onChanged called when data isn't changed


I have a Fragment, with a dynamic number of a custom views, consisting in an EditText and a Button. What I do is that every time the user types a price in the EditText and clicks the Button, I make an API request through a ViewModel, and my Fragment observes the LiveData in the ViewModel.

So far so good, when I use the first custom view. The problem comes on the second one (and the third), because the onChanged() method is apparently called even tho the data has not changed, and the second and the third custom views are listening to that data, so they change when they are NOT the ones triggering the data change (they receive the data change from the first one).

When the user clicks on the Button, the way I observe and fetch the price is this:

val observer = Observer<NetworkViewState> { networkViewState ->
            processResponse(networkViewState, moneySpent, coin, date)
        }
        boardingHistoricalPriceViewModel.coinDayAveragePrice.observe(this, observer)
        boardingHistoricalPriceViewModel.getDayAveragePrice(coin.symbol,
                addedCoinDatePriceView.selectedSpinnerItem, dateInMillis)

and what is happening is that the method processResponse gets called when the second custom view triggered the API request, but the result I receive is the one that coinDayAveragePrice has before the API response arrives (this is the value after the first API response from the first custom view has arrived).

This is part of my ViewModel:

val coinDayAveragePrice: MutableLiveData<NetworkViewState> = MutableLiveData()

fun getDayAveragePrice(symbol: String, currency: String, dateInMillis: Long) {
    coinRepository
            .getDayAverage(symbol, currency, "MidHighLow", dateInMillis)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doOnSubscribe { coinDayAveragePrice.postValue(NetworkViewState.Loading()) }
            .subscribeBy(onSuccess = {
                coinDayAveragePrice.postValue(NetworkViewState.Success(it))
            }, onError = { throwable ->
                coinDayAveragePrice.postValue(NetworkViewState.Error(throwable.localizedMessage))
            })
}

NetworkViewState is just a sealed class meant as a wrapper for a response of an API request:

sealed class NetworkViewState {
class Loading : NetworkViewState()
class Success<out T>(val item: T) : NetworkViewState()
class Error(val errorMessage: String?) : NetworkViewState()

}

I have also tried to unsubscribe or to set the coinDayAveragePrice to null, but still I have the same problem.

Thanks a lot in advance!


Solution

  • So, without seeing your ViewModel, it's hard to be sure exactly what the problem is, but I think it's what I indicated in my comment. In that case, one solution is to use a different kind of LiveData. I got this basic idea from a blog post (don't remember the link :-/), but here's the class:

    private const val TAG = "SingleLiveData"
    
    /**
     * A lifecycle-aware observable that sends only new updates after subscription, used for events like
     * navigation and Snackbar messages.
     *
     * This avoids a common problem with events: on configuration change (like rotation) an update
     * can be emitted if the observer is active. This LiveData only calls the observable if there's an
     * explicit call to setValue() or call().
     *
     * Note that only one observer is going to be notified of changes.
     */
    open class SingleLiveData<T> : MutableLiveData<T>() {
    
        private val pending = AtomicBoolean(false)
    
        @MainThread
        override fun observe(owner: LifecycleOwner, observer: Observer<T>) {
            if (hasActiveObservers()) {
                Logger.w(TAG, "Multiple observers registered but only one will be notified of changes.")
            }
    
            // Observe the internal MutableLiveData
            super.observe(owner, wrapObserver(observer))
        }
    
        @MainThread
        override fun observeForever(observer: Observer<T>) {
            if (hasActiveObservers()) {
                Logger.w(TAG, "Multiple observers registered but only one will be notified of changes.")
            }
            super.observeForever(wrapObserver(observer))
        }
    
        private fun wrapObserver(observer: Observer<T>): Observer<T> {
            return Observer {
                if (pending.compareAndSet(true, false)) {
                    observer.onChanged(it)
                }
            }
        }
    
        @MainThread
        override fun setValue(t: T?) {
            pending.set(true)
            super.setValue(t)
        }
    
        /**
         * Used for cases where T is Void, to make calls cleaner.
         */
        @MainThread
        fun call() {
            value = null
        }
    }
    

    Obviously, one problem with this is that it won't permit multiple observers of the same live data. However, if you require that, hopefully this class will give you some ideas.