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()
}
}
Related
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))
}
I'm trying to get the last id added from entity A to entity B to add to entity B by it , I fetched the id of the last element added to entity A like this :
in Dao :
#Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(addSpendEntity: AddSpendEntity) : Long
and in fun insert in repo i used mutableLiveData to save the last id inserted and get it to viewmodel then to observing it in fragment
in repo :
class AddSpendRepository(private var database: PersonalAccountingDateBase) {
private var id : Long = 0
private var mutableLiveData = MutableLiveData<Long>()
suspend fun insert(addSpendEntity: AddSpendEntity){
id = database.getAddSpendDao().insert(addSpendEntity)
Log.e("addspendrepository",id.toString())
mutableLiveData.postvalue(id)
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
...
}}
fun getMutableLiveData() : MutableLiveData<Long> = mutableLiveData
and in VM :
fun insertSpend(addSpendEntity: AddSpendEntity) = viewModelScope.launch(Dispatchers.IO) {
addSpendRepository.insert(addSpendEntity)
}
fun getMutableLiveData() : MutableLiveData<Long> = addSpendRepository.getMutableLiveData()
the observer in fragment i try to add to entity B When mutableLiveData is change :
private fun insert()
{
val totalMoney = binding.edtAddSpendSpendMoney.text.toString().toInt()
val notice = binding.edtAddSpendNotice.text.toString()
val date = binding.txtAddSpendDateText.text.toString()
val addSpendEntity = AddSpendEntity(totalMoney,notice,date)
addSpendViewModel.insertSpend(addSpendEntity)
addSpendViewModel.getMutableLiveData().observe(viewLifecycleOwner,
Observer {
Log.e("addspendfragment",it.toString())
if(it.toInt() != 0)
{
val dailyMovementEntity = DailyMovementEntity("make",totalMoney,notice,5,it.toInt())
addSpendViewModel.insertDailyMovement(dailyMovementEntity)
}
})
so the problem i faced is when to insert in the first time the value of mutable get null and the observer does'nt notice any thing then in the second time the observer notice the previos state of id and this condition continues as long as the application is running , when i close the app and do the same in the same way : The same problem is repeated as shown
enter image description here
You didn't show it in your code, so I'm just guessing, but here's a possible cause of your issue.
I'm guessing your ViewModel's insertSpend function is doing something like this:
fun insertSpend(addSpendEntity: AddSpendEntity) {
viewModelScope.launch(Dispatchers.IO) {
repository.insert(addSpendEntity)
}
}
The problem is, if you call MutableLiveData.value on a thread other than the main thread, then the change is not viewable until another loop of the main thread has occurred. You're not supposed to call .value on any thread besides the main thread. Then you get the proper value in your observer because observers are called on the next loop of the main thread.
Also, a suspend function should never require being called from a specific dispatcher, so you should not need to specify Dispatchers.IO when you launch your coroutine. More properly, your repository function should look like this, so it is safe to call it from anywhere. Any time a suspend function calls a function that requires a specific dispatcher, it is best to specify that dispatcher internally (I think of this as an extension of the single responsibility principle--outside functions shouldn't have to know what state to specify when calling another function if it can be avoided).
I would define it like this:
suspend fun insert(addSpendEntity: AddSpendEntity) = withContext(Dispatchers.Main) {
id = database.getAddSpendDao().insert(addSpendEntity) // it's safe to call this on main because it's a suspend function which by convention must not block
Log.e("addspendrepository",id.toString())
mutableLiveData.value = id
Log.e("addspendrepositoryid",mutableLiveData.value.toString())
// ...
}
Just my opinion:
On the ViewModel side, in general, you should rarely ever be launching a coroutine on the ViewModel scope with a specific dispatcher. Android has a general convention of treating the main thread as the default, and it is full of functions that must be called on main for proper behavior. So it is clean to always leave that as your default and only use withContext(Dispatchers.IO) (or .Default) for the bits of your coroutine that need it. And you should never need those just to call suspend functions, because of the coroutine convention that suspend functions must never block. So you only need them when calling blocking code.
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).
I saw all of the following scenarios in different example projects from Google's Codelabs and other sources and do not fully understand where the values from the LiveData object are retrieved from.
Scenario 1 - Current Understanding:
According to https://developer.android.com/.../viewmodel one reason to use a ViewModel is to store/cache UI related data that I want to re-use after the corresponding UI has been rebuild after a configuration change.
Given the following simplified ViewModel and Repository: After updateName() is called the first time, the LiveData object of _currentName contains a String. If the UI is then rebuild after a screen rotation, the view that needs to display the current name requests it by observing currentName which in turn returns the value of the LiveData object that is contained in the field of the _currentName property. Am I correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this._currentName.value = this.repository.updateName()
}
}
Repository
class NamesRepository() {
fun updateName(): String {
val nextName: String
...
return nextName
}
}
Scenario 2:
What happens if the UI is rebuild after a screen rotation in the following case? _currentName in the ViewModel 'observes' currentName in the repository, but it still is a property and therefore stores its own LiveData object in its field. When the view then requests currentName from the ViewModel, the value is retrieved from the LiveData object that is contained in the field of the _currentName property in the ViewModel. Is this correct?
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
private val _currentName: LiveData<String?> = this.repository.currentName
val currentName: LiveData<String?> get() = this._currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
Scenario 3:
In the following scenario, if the UI is rebuild and a view requests currentNam from the ViewModel, where is the requested value stored? My current understanding is, that currentName falls back to the field of the property _currentName in the repository. Isn't that against the idea of the ViewModel to store relevant UI data to be re-used after a configuration change? In the case below, it might be no problem to retrieve the value from the repository instead of the viewModel, but what if the repository itself retrieves the value directly from a LiveData object that comes from a Room database? Wouldn't a database access take place every time a view requests _currentName from the viewModel?
I hope somebody can clarify the situation more, in order to understand how to cache UI related data in the viewModel the correct way (or at least to understand what are the incorrect ways).
ViewModel
class NamesViewModel(): ViewModel() {
private val respository = NamesRepository()
val currentName: LiveData<String?> get() = this.repository.currentName
...
// Called as UI event listener.
fun updateName() {
this.repository.updateName()
}
}
Repository
class NamesRepository() {
private val _currentName: MutableLivedata<String?> = MutableLiveData(null)
val currentName: LiveData<String?> get() = this._currentName
fun updateName() {
val nextName: String
...
this._currentName.value = nextName
}
}
To answer your question scenario#1 is correct usage of LiveData.
Firstly, LiveData is not responsible for caching, it is just LifeCycleAware Observable, given that caching is done at ViewModel, when your activity recreates due to any configuration changes, android will try to retrieve the existing instance of ViewModel, if found then it's state and data are retained as is else it will create a new instance of ViewModel.
Second, using LiveData in repository is a bad idea at many levels, repository instances are held by ViewModel and LiveData are part of Android Framework which makes repositories rely on Android Framework thus creating problems in Unit Testing. Always use LiveData only in ViewModels.
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.