Hi,
I am observing live data using _observeForever()_ and then removing the observer later. I handle success and failure scenarios and resume the continuation object accordingly.
//failure
removeObserver(observer)
if (coroutine.isActive) coroutine.resume(getErrorState())
//success
removeObserver(observer)
if (coroutine.isActive) coroutine.resume(data)
In most cases it works fine. However, randomly few times I get exception :
Fatal Exception: java.lang.IllegalStateException: Already resumed, but proposed with update android.widget.RemoteViews@ebbd63d
at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError(CancellableContinuationImpl.java:277)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.java:272)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.java:189)
It seems , the _isActive_ check isn't behaving as expected. Please suggest a better check to avoid this crash.
Cheers !
Can you give a little bit more context on your code, please. Do you have a self-contained reproducer by any chance?
Can you give a little bit more context on your code, please. Do you have a self-contained reproducer by any chance?
Sorry, didn't understand exactly. I'll try to elaborate.
I have a runBlocking block, which gets resumed based on LiveData I am observing. This LiveData can return error/success/cachedData. This might be sending multiple data depending on cache data and network availability. In order to prevent "Already Resumed " exception, I added the isActive checks. However, it doesn't seem to work always as I am still observing this exception but hard to get into that scenario.
pseudo code as below,
@Synchronized
fun getData(): UserData = runBlocking {
suspendCancellableCoroutine<UserData> { coroutine ->
observer = Observer { response ->
response.either({
// failure
removeObserver()
if ((response as Either.Left).hasCachedData) {
if (coroutine.isActive) coroutine.resume(data)
} else {
if (coroutine.isActive) coroutine.resume(getErrorState())
}
}, { data ->
// success
removeObserver()
if (coroutine.isActive) coroutine.resume(data)
}
})
}
}
Handler(Looper.getMainLooper()).post {
useCase.data.observeForever(observer)
}
}
}
If you are observing LiveData, then you can use ready-to-use extensions provided by Google to use them with coroutines. No need to write your own. Just write data.asFlow().first() if you want to get the first observed value.
I do need the second value as well, as second one might be the updated value from API. The problem seems to be that the check isActive isn't working as expected. Nowhere in my code, have I resumed the continuation without this check, still somehow it throws this exception.
@gurveers Any chance you can provide some kind of self-contained reproducer for this problem?
@elizarov unfortunately I can't do that, even reproducing the crash is a lot of effort. But the crash rate would increase in production.
Hopefully the crash log helps you.
Fatal Exception: java.lang.IllegalStateException: Already resumed, but proposed with update android.widget.RemoteViews@7564348
at kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError(CancellableContinuationImpl.java:277)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.java:272)
at kotlinx.coroutines.CancellableContinuationImpl.resumeWith(CancellableContinuationImpl.java:189)
at com.TestClass$_testMethod_$1$invokeSuspend$$inlined$suspendCancellableCoroutine$lambda$3$1$2.invoke(_TestClass.java:507_)
at androidx.lifecycle.LiveData.considerNotify(LiveData.java:113)
at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:131)
at androidx.lifecycle.LiveData.setValue(LiveData.java:289)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:33)
at androidx.lifecycle.LiveData$1.run(LiveData.java:91)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7073)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:965)
It just means that you called resume on the same continuation twice. It is hard to figure out how this might happen just by a snippet of your code.
@elizarov I'm having a similar issue in my app. My question is why coroutine doesn't resume immediately after Continuation.resume gets called? That way we'd never get this sort of exception (calling resume on an already resumed Continuation. I'm certain there is some reasons behind. Interested to hear your guidance.
Calling resume, usually, just schedules coroutine for execution in the corresponding coroutine dispatcher.
@elizarov Can you please look into this issue again? We have a simpler use-case where the issue is reproduced, hopefully it will be useful for you:
import android.app.Activity
import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.play.core.review.ReviewManager
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
import javax.inject.Inject
import kotlin.coroutines.resume
/**
* An interface to display a UI requesting user feedback
*/
interface FeedbackRequestUiPresenter {
/**
* Show Feedback Request UI to the user
*
* @return feedback option selected by the user
*/
suspend fun showFeedbackRequest(): UserFeedbackResult
}
/**
* Class that requests a user feedback using a dialog created by [UserFeedbackDialogBuilder].
*/
class FeedbackRequestDialogPresenter @Inject constructor(
private val fragment: Fragment,
private val lifecycleOwner: LifecycleOwner,
private val reviewManager: ReviewManager
) : FeedbackRequestUiPresenter {
override suspend fun showFeedbackRequest(): UserFeedbackResult =
suspendCancellableCoroutine { continuation ->
lifecycleOwner.lifecycleScope.launchWhenResumed {
showDialog(continuation)
}
}
private fun showDialog(continuation: CancellableContinuation<UserFeedbackResult>) {
val activity = fragment.requireActivity()
UserFeedbackDialogBuilder(activity).apply {
setOnDismissListener {
continuation.resume(UserFeedbackResult.DISMISS)
}
setOnSubmitReviewClickListener {
requestRating(activity) {
continuation.resume(UserFeedbackResult.LEAVE_REVIEW)
}
}
setOnContactSupportClickListener {
continuation.resume(UserFeedbackResult.CONTACT_SUPPORT)
}
setOnAskLaterClickListener {
continuation.resume(UserFeedbackResult.ASK_LATER)
}
}.show()
}
private fun requestRating(activity: Activity, onCompleteBlock: () -> Unit) {
reviewManager
.requestReviewFlow()
.addOnCompleteListener { reviewInfo ->
if (reviewInfo.isSuccessful) {
reviewManager
.launchReviewFlow(activity, reviewInfo.result)
.addOnCompleteListener { onCompleteBlock() }
}
}
}
}
class UserFeedbackDialogBuilder @JvmOverloads constructor(context: Context, themeResId: Int = 0) :
MaterialAlertDialogBuilder(context, themeResId) {
private val binding = FeedbackDialogBinding.inflate(context.inflater)
init {
setView(binding.root)
}
var onDismissListener: DialogInterface.OnDismissListener? = null
private set
override fun setOnDismissListener(
onDismissListener: DialogInterface.OnDismissListener?
): MaterialAlertDialogBuilder {
this.onDismissListener = onDismissListener
return super.setOnDismissListener(onDismissListener)
}
fun setOnSubmitReviewClickListener(listener: (View) -> Unit) =
binding.submitReview.setOnClickListener(listener)
fun setOnContactSupportClickListener(listener: (View) -> Unit) =
binding.contactSupport.setOnClickListener(listener)
fun setOnAskLaterClickListener(listener: (View) -> Unit) =
binding.askLater.setOnClickListener(listener)
}
The exception stacktrace:
Fatal Exception: java.lang.IllegalStateException
Already resumed, but proposed with update CONTACT_SUPPORT
kotlinx.coroutines.CancellableContinuationImpl.alreadyResumedError (CancellableContinuationImpl.kt:335)
kotlinx.coroutines.CancellableContinuationImpl.resumeImpl (CancellableContinuationImpl.kt:330)
kotlinx.coroutines.CancellableContinuationImpl.resumeWith (CancellableContinuationImpl.kt:250)
app.sample.feedback.FeedbackRequestDialogPresenter$showDialog$$inlined$apply$lambda$3.invoke (FeedbackRequestUiPresenter.kt:57)
app.sample.feedback.FeedbackRequestDialogPresenter$showDialog$$inlined$apply$lambda$3.invoke (FeedbackRequestUiPresenter.kt:29)
app.sample.feedback.UserFeedbackDialogBuilder$sam$android_view_View_OnClickListener$0.onClick (Unknown Source:2)
android.view.View.performClick (View.java:7192)
com.google.android.material.button.MaterialButton.performClick (MaterialButton.java:992)
It looks like you call resume twice on the same continuation.