Hello I have a suspend function updateUserWithNewPersonalDetails(), I want check if the function has been executed and once executed I want to call success() function.
I am unable to use invokeOnCompletion on the function call
How can I do this please or what is the other way I can call success() function once updateUserWithNewPersonalDetails is executed
suspend function
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
userDao.get().collect { cachedUser ->
val updatedCachedUser = UserDB(cachedUser.id, ...)
userDao.save(updatedCachedUser)
}
}
from viewmodel I want to call the above function and on success or invoke successfull I want to call success function
userRepo.updateUserWithNewPersonalDetails(details). invokeOnCompletion{
success()
}
Thanks
R
Currently your function updateUserWithNewPersonalDetails never completes because the Flow returned by Room never completes (it monitors for DB changes forever).
Assuming you are interested only in the first change you can do:
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
val cachedUSer = userDao.get().first() // this will get the current value from the DB
val updatedCachedUser = UserDB(cachedUser.id, ...)
userDao.save(updatedCachedUser)
}
Here first() gets the current value from the DB (or throws if it doesn't exist) and does NOT observe forever. Alternatively you can use firstOrNull().
Then your other function becomes:
updateUserWithNewPersonalDetails()
success()
Related
I use livedata that collect item data, after it in onClicked() I get this data from livedata. What could be better approach for this?
lifecycleScope.launch {
lifecycleScope.async {
viewModel.fetchItem(args.workId)
}.await()
onClicked()
}
variables in viewModel
val item = _item.immutable()
private val _item = MutableLiveData<ViewState<Item?>>()
[...]
// I wait here for data from first code than I collect it because item is liveData as above
private fun onClicked() {
val item = viewModel.item.value?.dataOrNull
[...]
fetchItem method
fun fetchItem(id: Int) {
viewModelScope.launch {
_item.postAsyncValue { repository.getItem(id) }
}
}
Currently, I think your code is not doing what you think it does. fetchItem is not a suspend function. It launches a coroutine and immediately returns without waiting for the coroutine to finish. So, your async coroutine that calls it doesn't wait for that result either. There's no point in using async at all here since the code returns almost immediately.
I'm guessing that what you're trying to accomplish is wait for postAsyncValue to finish setting that new value. To do this, you need to make fetchItem a suspend function that waits for its work to be done.
I am not familiar with this postAsyncValue, but my best guess is that it is a helper extension function on MutableLiveData that takes a suspend lambda, calls it, and then sets the value to the LiveData on the main thread. If this is the case, you should just do this instead so the function actually waits for the task to be done before returning:
suspend fun fetchItem(id: Int) = withContext(Dispatchers.Main) {
_item.value = repository.getItem(id) // assuming getItem is a suspend function
}
//...
lifecycleScope.launch {
viewModel.fetchItem(args.workId)
onClicked()
}
If this isn't right, please add your source code for postAsyncValue and let me know.
Regarding your literal question, instead of using async followed immediately by await, you can use withContext(Dispatchers.IO) { }, but you would only do this if you are calling blocking code. You only need async when you're working with parallelism, so the compiler warns you that you're doing something silly if you immediately call await() after async { }.
I have a suspend function userRepo.updateUserWithNewPersonalDetails(details) and after executing this function I want to execute success() function which is a call back.
but the issue is success() is not getting executed.
any suggestions on how to get this to work.
this sequence does not work
SUCCESS -> {
progress.postValue(GONE)
userRepo.updateUserWithNewPersonalDetails(details) // EXECUTED
success() // NOT EXECUTED
}
if I change it to call success() first then save to repo, it works fine. but this is not the right way of doing it I think.
could you suggest please
SUCCESS -> {
progress.postValue(GONE)
success() // EXECUTED
userRepo.updateUserWithNewPersonalDetails(details) // EXECUTED
}
Fragment call
viewModel.save(personalDetails) { activity?.onBackPressed() }
ViewModel
fun save(details: PersonalDetails, success: () -> Unit) {
viewModelScope.launch {
userRepo.savePersonalDetails(details).collect {
when (it.status) {
LOADING -> {
progress.postValue(VISIBLE)
}
SUCCESS -> {
progress.postValue(GONE)
userRepo.updateUserWithNewPersonalDetails(details)
success() // THIS IS NOT EXECUTED
}
ERROR -> {
progress.postValue(GONE)
error.postValue(ErrorResult(errorCode = SNACKBAR_ID_USER_DETAILS_SAVE_FAIL))
}
}
}
}
}
userRepository
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
userDao.get().collect { cachedUser ->
val updatedCachedUser = UserDB(cachedUser.id, etc..)
userDao.save(updatedCachedUser)
}
}
Can you please show me the function that you call?, did you already use the breakpoint to make sure the function it self was called?. cause if you dont, i think you might use nullable variable and the value will retrieved after the suspend function (userRepo.blabla()) finished, if yes. maybe you can call .invokeOnCompletion { /your Success Function/ success() }
success() method isn't called because you collect Flow in updateUserWithNewPersonalDetails method:
userDao.get().collect {...}
It suspends a coroutine execution. My guess is that it is an infinite Flow, which doesn't complete until coroutine is completed. That's why userDao.get().collect suspends execution.
I don't quite understand what you are trying to achieve in the updateUserWithNewPersonalDetails method, but it seems it doesn't update the DB. If you want to update user details in the DB, you don't need to collect Flow. You should have something like this:
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
userDao.update(details)
}
where userDao.update(details) is a suspend method, which updates DB:
suspend fun update(details: PersonalDetails)
I have created below function :
suspend fun isBatteryExistsInLocal(batteryId: String): Boolean {
val count = appDatabase.userBatteriesDao().checkBatteryExists(batteryId)
if (count > 0) {
return true
} else {
return false
}
}
which checks the particular record is exists in database or not.
checkBatteryExists is the dao method as below :
#Query("SELECT COUNT(*) FROM " + DatabaseConstant.mUserBatteriesTable + " WHERE isDeleted = 0 and batteryId= :batteryId")
suspend fun checkBatteryExists(batteryId:String): Int
The Method isBatteryExistsInLocal is called from below function in my view model class.
fun isBatteryExistsInLocal(batteryId:String): Boolean {
var isBatteryExistsInLocal = false
scope.launch {
isBatteryExistsInLocal =batteryRepository.isBatteryExistsInLocal(batteryId)
}
return isBatteryExistsInLocal
}
and the above method is calling from my fragment as below :
if (viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId)) {
But here, I am always getting false WHY ?
I have debugged and check that a record is exists in local database and also count variable is returns 1.
Then Why am getting false in above if condition ?
Please guide. What I am doing wrong.
Thanks.
Contrary to what you may have expected the function in ViewModel executes in following manner
fun isBatteryExistsInLocal(batteryId:String): Boolean {
var isBatteryExistsInLocal = false
scope.launch {
// Everything inside will be executed async
// outer function may already have returned by the time this completes
}
return isBatteryExistsInLocal
}
One way to fix this is to put the fragment code (viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId)) inside a coroutine, which can be done as
lifecycleScope.launch{
(viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId))
// other code
}
And change your ViewModel function to be suspending as
suspend fun isBatteryExistsInLocal(batteryId:String): Boolean {
return batteryRepository.isBatteryExistsInLocal(batteryId)
}
When you launch a coroutine, it is launched asynchronously, just like when you call a function that takes a callback. The coroutine is queued to start running, but the current function will possibly complete and return before that happens. Explanations of this issue are in the answers of this question even though they are about API callbacks. It's exactly the same issue when launching a coroutine.
The only way to convert a suspend function into one you can call from outside a coroutine to get a return value synchronously is to do something like use runBlocking or calling join on your launched Job. But this is not an acceptable solution because it will block your main thread and cause stutters or the ANR error.
The correct solution is to use a coroutine higher up in your workflow so you can freely use suspend functions wherever you need to. For instance, if isBatteryExistsInLocal is something you need as part of a response to some button press, you should launch a coroutine in your button's click listener, and then your entire sequence of actions can freely include suspend function calls.
By the way, just a tip, you can simplfy your suspend function. Instead of using if/else to return true or false, you could simply put return count > 0.
I'm trying to deliver realtime updates to my view with Kotlin Flows and Firebase.
This is how I collect my realtime data from my ViewModel:
class MainViewModel(repo: IRepo): ViewModel() {
val fetchVersionCode = liveData(Dispatchers.IO) {
emit(Resource.Loading())
try {
repo.getVersionCode().collect {
emit(it)
}
} catch (e: Exception){
emit(Resource.Failure(e))
Log.e("ERROR:", e.message)
}
}
}
And this is how I emit each flow of data from my repo whenever a value changes in Firebase:
class RepoImpl: IRepo {
override suspend fun getVersionCodeRepo(): Flow<Resource<Int>> = flow {
FirebaseFirestore.getInstance()
.collection("params").document("app").addSnapshotListener { documentSnapshot, firebaseFirestoreException ->
val versionCode = documentSnapshot!!.getLong("version")
emit(Resource.Success(versionCode!!.toInt()))
}
}
The problem is that when I use:
emit(Resource.Success(versionCode!!.toInt()))
Android Studio highlights the emit invocation with:
Suspend function 'emit' should be called only from a coroutine or another suspend function
But I'm calling this code from a CoroutineScope in my ViewModel.
What's the problem here?
thanks
A Firestore snapshot listener is effectively an asynchronous callback that runs on another thread that has nothing to do with the coroutine threads managed by Kotlin. That's why you can't call emit() inside an asynchronous callback - the callback is simply not in a coroutine context, so it can't suspend like a coroutine.
What you're trying to do requires that you put your call to emit back into a coroutine context using whatever method you see fit (e.g. launch), or perhaps start a callbackFlow that lets you offer objects from other threads.
The suspend keyword on getVersionCodeRepo() does not apply to emit(Resource.Success(versionCode!!.toInt())) because it being called from within a lambda. Since you can't change addSnapshotListener you'll need to use a coroutine builder such as launch to invoke a suspend function.
When a lambda is passed to a function, the declaration of its corresponding function parameter governs whether it can call a suspend function without a coroutine builder. For example, here is a function that takes a no-arg function parameter:
fun f(g: () -> Unit)
If this function is called like so:
f {
// do something
}
everything within the curly braces is executed as though it is within a function that is declared as:
fun g() {
// do something
}
Since g is not declared with the suspend keyword, it cannot call a suspend function without using a coroutine builder.
However, if f() is declared thus:
fun f(g: suspend () -> Unit)
and is called like so:
f {
// do something
}
everything within the curly braces is executed as though it is within a function that is declared as:
suspend fun g() {
// do something
}
Since g is declared with the suspend keyword, it can call a suspend function without using a coroutine builder.
In the case of addEventListener the lambda is being called as though it is called within a function that is declared as:
public abstract void onEvent (T value, FirebaseFirestoreException error)
Since this function declaration does not have the suspend keyword (it can't, it is written in Java) then any lambda passed to it must use a coroutine builder to call a function declared with the suspend keyword.
Let me start with example code snippets
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
mediatorLiveData.addSource(response) {
result.value = sortData(it) // sortData is also suspend function which sortData at Dispatcher.Default
}
}
In this example, sortData can't call under lambda function(in this case addSource).And also I already declare executeLive as suspend, that why suspend api request can start at first. But sortData function show compile time error
Suspend function can only be called from a coroutine body
So how do I change my code structure to solve this problems?
Update: Is there any article about this?
A lambda is generally a callback function. Callback functions are so called because we wrap a block of code in a function, and pass it to someone else (or some place else) to be executed. It is a basic inversion of control where the code is not for you to execute, but someone else to do it (example the framework).
For example when you set a onClickListener on a button, we don't know when it will get called, we pass a lambda for the framework which takes care of the user interaction to call the specified action.
In your case similarly the suspend function is not calling the sortdata, it is passing it to the mediatorLiveData object to call it in its own context. It is not necessary the lambda you passed would be called from a coroutine body, as such this is not allowed.
You can solve this by converting the mediatorLiveData.addSource call into a suspending call itself with suspendCoroutine:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
val data = suspendCoroutine<TypeOfData> { cont ->
mediatorLiveData.addSource(response) { cont.resume(it) }
}
result.value = sortData(data)
}
I've used TypeOfData as a placeholder for whatever the type of data emitted by response is. Note that this will only work if the you're intending for a single emission to happen, though.
If you need to track multiple values, you can experiment with callbackFlow:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
callbackFlow<TypeOfData> {
mediatorLiveData.addSource(response) { offer(it) }
awaitClose { mediatorLiveData.removeSource(response) }
}
.collect { result.value = sortData(it) }
}