getting error of suspend function while using coroutine - android

also, I'm calling my suspend function inside a coroutine scope, I'm getting an error that Suspends functions can only be called within a coroutine body. how's that happening?
lifecycleScope.launchWhenCreated{
viewModel.tickets.observe(viewLifecycleOwner, {
it?.let { data ->
adapter.submitData(data)
}
})
}

Hi you are calling suspend function inside observe lambda.
Change the order like this:
viewModel.tickets.observe(viewLifecycleOwner, { data ->
lifecycleScope.launchWhenCreated{
data?.let { list ->
adapter.submitData(list)
}
})
}

Related

Non suspend function is not executed after suspend function

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)

SuspendCoroutine code not reachable when using with Firebase auth

I have a suspendCoroutine in my repository with which I want to send data back to my ViewModel -
suspend fun sendPasswordResetMail(emailId: String): Boolean {
return withContext(Dispatchers.IO) {
suspendCoroutine { cont ->
firebaseAuth?.sendPasswordResetEmail(emailId)
?.addOnCompleteListener {
cont.resume(it.isSuccessful)
}
?.addOnFailureListener {
cont.resumeWithException(it)
}
}
}
}
However, neither of the listeners are called. Debugger says no executable code found at line where 'cont.resume(it.isSuccessful)' or 'cont.resumeWithException(it)' are.
I tried 'Dispatchers.IO', 'Dispatchers.Main' and 'Dispatchers.Default' but none of them seem to work. What could I be doing wrong?
My ViewModel code -
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
and
fragment -
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
I believe you are calling
isEmailSent : LiveData<Boolean> = liveData {
emit(firebaseAuthRepo.sendPasswordResetMail(emailId))
}
this piece of code everytime for sending email
and
viewModel.isEmailSent.observe(viewLifecycleOwner, { flag ->
onResetMailSent(flag)
})
this piece only once.
Assuming that's true what you are essentially observing is the initial live data that was created with the model while it is being replaced everytime when resent is called. Instead call
isEmailSent.postValue(firebaseAuthRepo.sendPasswordResetMail(emailId))
from inside of a coroutine.
Also for the debugger not showing anything try adding a log above the cont.resume call and cont.resumeWithException call since it has worked for me in the past.
I think the easier way to achieve this is by using firebase-ktx and the await() function (which does what you are trying under the hood):
suspend fun sendPasswordResetMail(emailId: String): Boolean {
try {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
return true
} catch(e: Exception) {
return false
}
}
Another way would be to use flow:
suspend fun sendPasswordResetMail(emailId: String): Boolean = flow<Boolean {
firebaseAuth?.sendPasswordResetEmail(emailId).await()
emit(true)
}.catch { e: Exception -> handleException(e) }
You could then observe this in your fragment by putting the code inside your viewmodel and calling .asLiveData()

Suspend execution until object value is set through LiveData

I'm working with a third party SDK which has a LiveData object I need to observe in order to set the value of a custom object of mine:
fun setValueAndDoSomething(activity: FragmentActivity) {
var myCustomObject: MyCustomObject? = null
sdkComponent.observe(activity, Observer { sdkResult ->
myCustomObject = MyCustomObject(sdkResult)
})
// Wait until myCustomObject is not null
myCustomObject.doSomething()
}
I want to wait until the SDK LiveData's callback is triggered, so that myCustomObject is not null, to proceed with the rest of the execution. How can I achieve that using Kotlin Coroutines?
If you want to await a result, your function must be suspendable. Use suspendCoroutine to get a continuation and resume it from the callback:
suspend fun setValueAndDoSomething(activity: FragmentActivity) {
val myCustomObject = suspendCoroutine<MyCustomObject> { cont ->
sdkComponent.observe(activity, Observer { sdkResult ->
cont.resume(MyCustomObject(sdkResult))
})
}
myCustomObject.doSomething()
}
To be able to call this, you'll have to launch a coroutine.

Suspending function can only be called within coroutine body

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.

onCompletion not called while using Koltin Flow with LiveData

So here is what I was trying to do with Flow, I am showing a ProgressBar in onStart and trying to hide the ProgressBar in onCompletion.
In ViewModel class appDatabase.eventDao().getAllEvents() returns Flow<List<EntityEvents>
#ExperimentalCoroutinesApi
val allEvents: LiveData<Outcome<List<Event>>> = _fetchEvents.switchMap { _ ->
appDatabase.eventDao().getAllEvents()
.map { eventListMapper.map(it) }
.map { sortEventsBasedOnPreference(it) }
.flowOn(Dispatchers.IO)
.map { Outcome.success(it) }
.onStart { emitLoading(true) }
.onCompletion { emitLoading(false) }
.catch { emitFailure(it, R.string.err_something_wrong) }
.asLiveData(context = viewModelScope.coroutineContext)
}
All working fine, what I am not able to figure out why is onCompletion not called when the task is completed?
if appDatabase.eventDao().getAllEvents() is based Room on Flow, never called onCompletion().
Why?
Because getAllXXX() Query is 'Hot'.
Actually, query is not completed. Only data is emited.
When the data changes, the query will emit data again.

Categories

Resources