LiveData observer is not removed - android

I am trying to get LiveData updates in a ViewModel, and make sure that the observer is not leaking, but it is leaking. The typical problem is that the observer is not stored in a variable, but that is not the case here; the lambda is stored in a variable.
private val observer: (List<MusicFile>) -> Unit =
{ musicFiles: List<MusicFile> ->
_uiState.postValue(FragmentMusicListState(musicFiles))
}
init {
musicRepository.musicFiles.observeForever(observer)
}
#VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public override fun onCleared() {
super.onCleared()
musicRepository.musicFiles.removeObserver(observer)
}
The problem is that after onCleared is called, the observer is still attached. I verified this with the following test.
#Test
fun onCleared_RemovesObserver() {
val musicRepository = MusicRepository.getInstance()
val context = InstrumentationRegistry.getInstrumentation().targetContext
musicRepository.loadMusicFiles(context.contentResolver)
val musicFiles = musicRepository.musicFiles
val viewModel = FragmentMusicListViewModel(SavedStateHandle(), musicRepository)
viewModel.onCleared()
assert(!musicFiles.hasObservers())
}
In addition, I have debugged the test on the last line, and musicFile's attributes show the observer is still attached. Attributes mActiveCount and mObservers show the observer is still attached,
How do I actually remove the observer?

LiveData takes an argument of type Observer<T> in observeForever and removeObserver. Observer is what Kotlin considers a functional interface written in Java in the androidx.lifecycle library.
What you are passing in is of type (List<MusicFile>) -> Unit.
This is a high order function and is not the same type as Observer<List<MusicFile>>. They are functionally similar in that they both have one parameter of type List<MusicFile> and both return Unit, so what Kotlin does for the better or for the worse (the worse in this case) is it will "cast" the type for you.
When Kotlin "casts" from high-order function to functional interface it is creating a new object. This happens every single time in your code when either observeForever or removeObserver are called. That's why removeObserver isn't working, because you're actually not passing in the same object despite how the code looks. I've written about this before.
In short, you can fix your problem by changing the type of observer to Observer:
private val observer: Observer<List<MusicFile>> =
Observer { musicFiles: List<MusicFile> ->
// _uiState.postValue(FragmentMusicListState(musicFiles))
}

Related

How to initialize a field in viewModel with suspend method

How to initialize a field in view model if I need to call the suspend function to get the value?
I a have suspend function that returns value from a database.
suspend fun fetchProduct(): Product
When I create the view model I have to get product in this field
private val selectedProduct: Product
I tried doing it this way but it doesn't work because I'm calling this method outside of the coroutines
private val selectedProduct: Product = repository.fetchProduct()
You can't initialize a field in the way you described. suspend function must be called from a coroutine or another suspend function. To launch a coroutine there are a couple of builders for that: CoroutineScope.launch, CoroutineScope.async, runBlocking. The latter is not recommended to use in production code. There are also a couple of builders - liveData, flow - which can be used to initialize the field. For your case I would recommend to use a LiveData or Flow to observe the field initialization. The sample code, which uses the liveData builder function to call a suspend function:
val selectedProduct: LiveData<Product> = liveData {
val product = repository.fetchProduct()
emit(product)
}
And if you want to do something in UI after this field is initialized you need to observe it. In Activity or Fragment it will look something like the following:
// Create the observer which updates the UI.
val productObserver = Observer<Product> { product ->
// Update the UI, in this case, a TextView.
productNameTextView.text = product.name
}
// Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
viewModel.selectedProduct.observe(this, productObserver)
For liveData, use androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 or higher.
Since fetchProduct() is a suspend function, you have to invoke it inside a coroutine scope.
For you case I would suggest the following options:
Define selectedProduct as nullable and initialize it inside your ViewModel as null:
class AnyViewModel : ViewModel {
private val selectedProduct: Product? = null
init {
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}
}
Define selectedProduct as a lateinit var and do the same as above;
Personally I prefer the first cause I feel I have more control over the fact that the variable is defined or not.
You need to run the function inside a coroutine scope to get the value.
if you're in a ViewModel() class you can safely use the viewModelScope
private lateinit var selectedProduct:Product
fun initialize(){
viewModelScope.launch {
selectedProduct = repository.fetchProduct()
}
}

Android Room Query returning null

I am trying to get the values from a DB via Room but it always returns null.
It should retrieve the data from DB BalancesCat.
Any help? Thanks!
This is the DAO
#Query("SELECT * FROM BalancesCat")
suspend fun getAllBalances(): List<BalancesCat>
Repository
suspend fun getAllBalancesCat(): List<BalancesCat>? {
var balancesCat: List<BalancesCat>? = null
withContext(Dispatchers.IO){
balancesCat = balancesCatDao.getAllBalances()
}
return balancesCat
}
ViewModel
fun getAllBalancesCat(): List<BalancesCat>? {
var balancesCat: List<BalancesCat>? = null
viewModelScope.launch {
balancesCat = repository.getAllBalancesCat()
}
return balancesCat
}
and the Fragment where I want to retrieve the data
balancesCatViewModel = ViewModelProvider(requireActivity(),
BalancesCatViewModelFactory(requireActivity().application)).
get(BalancesCatViewModel::class.java)
allBalancesCat = balancesCatViewModel.getAllBalancesCat()
var allBalancesCatNew: BalancesCat
val currentDate1 = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
val dateCurrent1 = Date.valueOf(currentDate1)
allBalancesCat?.forEach {
if(it.date != dateCurrent1){
it.date = dateCurrent1
allBalancesCatNew = it
balancesCatViewModel.update(allBalancesCatNew)
}
}
This isn't your problem, but I have to mention, your repository's getAllBalancesCat() function is needlessly complicated and doesn't need to return a nullable. Since balancesCatDao.getAllBalances() is a suspend function, it is pointless to wrap it in withContext(). You never need to specify a context to call a suspend function (unless the suspend function was incorrectly designed and has blocking code in it). It can be simplified to:
suspend fun getAllBalancesCat(): List<BalancesCat> = balancesCatDao.getAllBalances()
Your ViewModel function is incorrect and is guaranteed to always return null. It creates the variable balancesCat with initial value of null, launches a coroutine, and then returns the null balancesCat before the coroutine has even started. Coroutines on the ViewModel scope are added to the main thread looper's queue, but that puts them after the code that is currently running in the main thread, like the rest of this function.
The correct way for this ViewModel function to work is to also be a suspend function that returns a non-nullable List:
suspend fun getAllBalancesCat(): List<BalancesCat> = repository.getAllBalances()
And in your Fragment, launch a coroutine from the lifecycleScope to do all this work that partially involves calling suspend function(s).
I can't comment very much on the fragment code because it's not shown in context, but I see some possible code smells. Properties that should probably just be local vals in the function. The Fragment shouldn't need to get values from the ViewModel and then store them in properties, and if it does, then the Fragment's code gets more complicated because it has to check if the local property holds the up-to-date value or not, instead of just getting it from the source (the ViewModel).

Why is Livedata setValue ignored when called twice?

I have the following ViewModel with MutableLiveData data and another LiveData ones that is derived from data in a way that it updates its value only if the data.number is equal to 1.
class DummyViewModel : ViewModel() {
private val data = MutableLiveData<Dummy>()
val ones = data.mapNotNull { it.takeIf { it.number == 1 } }
init {
data.value = Dummy(1, "Init")
doSomething()
}
fun doSomething() {
data.value = Dummy(2, "Do something")
}
}
data class Dummy(val number: Int, val text: String)
fun <T, Y> LiveData<T>.mapNotNull(mapper: (T) -> Y?): LiveData<Y> {
val mediator = MediatorLiveData<Y>()
mediator.addSource(this) { item ->
val mapped = mapper(item)
if (mapped != null) {
mediator.value = mapped
}
}
return mediator
}
I observe ones in my fragment. However, If I execute doSomething, I don't receive any updates in my fragment. If I don't execute doSomething, the dummy Init is correctly present in ones and I receive an update.
What is happening here? Why is ones empty and how can I overcome this issue?
Maybe I'm missing something, but the behavior seems like expected to me...
Lets' try to reproduce both cases sequentially.
Without doSomething() :
Create Livedata
Add Dummy(1, "Init")
Start listening in the fragment: Because number is 1, it passes your filter and the fragment receives it
With doSomething():
Create Livedata
Add Dummy(1, "Init")
Add Dummy(2, "Do something") (LiveData keeps only the last value, so if nobody observes, the first value is getting lost)
Start listening in the fragment: Because number is 2, the value gets filtered and the fragment receives nothing
A little offtopic: it's always good to write tests for ViewModel cases like this, because you'll be able to isolate the problem and find the real reason quickly.
EDIT: also be aware that your filter is only working on observing, it isn't applied when putting the value into LiveData.

Why Transformation.switchMap(anyLiveData) isn't fire when i change the value of "anyLiveData"

I will hope that when i call to "addPlantToGarden()" passing respect "plantId" parameter then fire the "observers" "Transformations.switchMap(plantName)" but that doesn't happen, what is the error?
private val plantName: MutableLiveData<String> = MutableLiveData()
val plant: LiveData<Plant> = Transformations.switchMap(plantName){plantId ->
plantRepository.getPlant(plantId)
}
val isPlanted: LiveData<Boolean> = Transformations.switchMap(plantName){plantId ->
gardenPlantingRepository.isPlanted(plantId)
}
fun addPlantToGarden(plantId: String) {
plantName.value = plantId
}
These are a few things to consider:
1. Check your Repository
Make sure your plantRepository.getPlant(plantId) returns LiveData. Since methods from Repository are executed in background, I prefer encapsulate the function using this:
liveData {
// some async process (e.g. HTTP Request)
emit(/*your value*/)
}
Reference: https://developer.android.com/topic/libraries/architecture/coroutines#livedata
2. Check your Observer
Are you observing on a correct view lifecycle owner? If your ViewModel is inside a Fragment, make sure to do this:
viewModel.plant.observe(viewLifecycleOwner, Observer{
// action
})
instead of:
viewModel.plant.observe(this, Observer{
// action
})
And make sure to observe first before trying to change your plantName value.
3. Start with a simple case
I have no idea how you changed your plantName value. But try from a simple hardcoded/mock value first, for example:
plantName.value = "1"
then trace it through your Repository, then down to your Observer. Hopefully this will help you.

How to observe LiveData changes from a Room Database using Data Binding Library

First time implementing Room and Data Binding.
In my ViewModel:
var numberOfClubs = ObservableInt(0)
private var clubs: LiveData<List<Club>> = clubDao.getAllClubs()
I then try to get clubs.value after inserting into the Room database but am getting a KotlinNullPointerException every time.
fun addClubs() {
Observable.fromCallable({
clubDao.insertClubs(listOf(Club(null, "clubName"))
})
.subscribeOn(schedulerFacade.computation)
.subscribe(
{
numberOfClubs.set(clubs.value!!.size) <--- Kotlin NPE
},
{ error ->
logWrapper.e("MainViewModel", error.message!!)
clubError.set(true)
})
}
In my XML I'm calling: android:text="#{viewModel.numberOfClubs}".
As inserting into the Room database doesn't cause anything to be returned, I can't set numberOfClubs with the result. I assumed that clubs should track the changes?
Yes, as you figured out already LiveData is lifecycle aware. That means that it observe aslong as the livecycle owner is present.
Even if you've posted the solution already, you shouldnt use !! at all.
viewModel.clubs.observe(this, {
it?.let {
viewModel.numberOfClubs.set(it.size)
}
)
is the proper way doing a null check before assigning the size of your recieved list.
By the way, if you want to get the items size in your layout, you should use
text="#{viewModel.clubsList.size() + ``}"
and observe a list, not a list size using
val clubsList = ObservableArrayList<..>()
viewModel.clubs.observe(this, {
it?.let {
viewModel.clubsList.set(it)
}
)
Was a bit of a LiveData misunderstanding from myself. In order to get a LiveData object to begin emitting things is to .observe() it. As LiveData is lifecycle aware, .observe() is best called from within a Fragment or Activity as it has a reference to LifecycleOwner which must be the first parameter passed into .observe().
I'm perhaps not following best practices as I set an ObservableInt in the ViewModel from my Activity but I used:
viewModel.clubs
.observe(this, Observer { clubs ->
viewModel.numberOfClubs.set(clubs!!.size)
})

Categories

Resources