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.
Related
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()
}
}
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 have a function 'A' in a ViewModel that retrieves data from firebase and I assign the value to a MutableLiveData<Int> (all of this is wrapped in onSuccessListener)and return it. This function is called from another function 'B' in the same ViewModel. But when I try to return MutableLiveData<Int> from 'A', it is returned as 0 (the default value). But if I assign value for the MutableLiveData<Int> outside the onSuccessListener, then the value is being returned.
Code:
val num = MutableLiveData<Int>().default(0)
private fun A():Int {
FirebaseOperation
.addOnSuccessListener{ //it:DocumentSnapshot!
num.value = it.num
}
return num.value.toInt() // outside onsuccesslistener, default value 0 is being returned
}
private fun B() {
val num2 = A()
}
Update:
After learning about the firebase callback hell, I've switched to kotlin coroutine for firebase (implementing org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1 as a dependancy)
Updated Code:
private suspend fun A():DocumentSnapshot? {
return Firebase.firestore.collection("collection").documet("document").get().await()
}
private suspend fun B(): Int{
val data = A()
val user = data.toObject<User>()
val num = user.num
return num
}
But the main thread freezes and the app crashes with Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 9.)
If you use kotlinx.coroutines, you can use suspendCoroutine.
private suspend fun A(): Int = suspendCoroutine { cont ->
FirebaseOperation.addOnSuccessListener{ // it: DocumentSnapshot!
cont.resume(it.num)
}
}
private suspend fun B() {
val num2 = A()
}
The retrieval of data from firebase happens asynchronously or in other words the addOnSuccessListener callback is only invoked when the data is retrieved from the firebase. So the livedata is only updated when the callback is invoked.
when you call A() from B(), it returns 0 because it does not wait for the livedata to be updated (callback to be invoked) and just returns the default value.
When you simply update the livedata value outside the addOnSuccessListener, it is updated synchronously and hence you get the updated result
The main purpose of LiveData is to be updated after asynchronous work and to be observed. In function A() I see that you only add SuccessListener but no operation is executed, so SuccessListener is not invoked. In function B() you just retrieve a reference to MutableLiveData but don't observe it, so it always contains default value. Usually LiveData objects are observed in Activity or Fragment:
num.observe(lifecycleOwner, androidx.lifecycle.Observer {
// here you retrieve changes of your "num" variable as parameter "it"
})
where lifecycleOwner - usually activity or fragment.
A way to get the value in function B without using coroutines and without observing the livedata by a lifecycle (which btw you can't do as you don't have a lifecycle owner in viewmodel and you should refrain from passing any to it), is to use a MediatorLiveData.
The main purpose of mediator live data is that depends on other livedata for it's value and it can observe other livedata without a lifecycle.
So you can create a MediatorLiveData inside the function B, then return the livedata from function A and then observe it using the MediatorLiveData in function B. That way, when the value will get updated in A, you will be notified in B.
You can read more about MediatorLiveData here: https://developer.android.com/reference/android/arch/lifecycle/MediatorLiveData
You could have flow or you can directly feed your VM.
class YourViewModel:ViewModel(){
val num = MutableLiveData<Int>().default(0)
private suspend fun A():DocumentSnapshot? {
return Firebase.firestore.collection("collection").documet("document").get().await()
}
fun onSomeButtonClick(){
viewModelScope.launch(Main){
val num = A()?.toObject<User>().num ?: return
num.value = num
}
}
}
or flow with asLiveData extension.
val num:LiveData<Int> = flow{
val num = A()?.toObject<User>()?.num ?: 0
emit(num)
}.asLiveData()
In your UI, you can observe and get id.
I somehow managed to merge both the functions and I've nested all the DB callbacks now. I don't think that's good for code readability though. I'll be trying out other alternatives meanwhile.
I have a ViewModel which has a property of type LiveData<UserData>, being read from a Room database.
Its code is as follows:
class UserDataViewModel(application: Application) : AndroidViewModel(application) {
private val userDataDao: UserDataDao = AppDatabase.getInstance(application).dao()
val userData: LiveData<UserData?> = userDataDao.getUserData()
}
In the associated activity, I get a reference to the view model:
private val viewModel: UserDataViewModel by viewModels()
In that activity, I need to get the value of the UserData on a button click:
private fun handleClick(view: View) {
viewModel.userData.value?.let {
// do stuff if the userData is present
}
}
Now in theory, unless the user presses the button before the data has been loaded, this should never be null.
However, as the code stands, the call to viewModel.userData.value is always null and the let block never executes.
But, if I add this statement in onCreate, the let block in the click handler executes as desired:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.userData.observe(this, Observer {
// do nothing
})
}
My question is: why do I need to call the observe function, even if I'm doing nothing with a change event, to get valid responses from LiveData::getValue?
My question is: why do I need to call the observe function, even if I'm doing nothing with a change event, to get valid responses from LiveData::getValue?
Because the ComputableLiveData returned from the Room DAO only executes the query if the LiveData has at least one active observer (inside LiveData.onActive()). Then it runs asynchronously on a different thread, and at some point in the future it will be posted into the LiveData.
You do not need to call observe() in order to get a LiveData to give up a value other than null. LiveData always contains and yields null initially until something sets its value. If you don't want this initial null value, then you should immediately set it to something else instead, before making the LiveData available to any other components. If you want to know when it first contains a non-null value, you will need to use an observer.
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.