I m using coroutines in my android app, and i have this function that need to communicate with UI and Main thread.
private suspend fun init() : RequestProcessor<LocalData, ApiResult, ApiError>
{
#Suppress("LeakingThis")
_localData = listener.databaseCall()
withContext(Dispatchers.IO) {
if (_localData == null)
{
checkIfShouldFetch(null, null)
}
else
{
withContext(Dispatchers.Main) {
mediatorLiveData.addSource(_localData!!) { newLocalData ->
mediatorLiveData.removeSource(_localData!!)
// I want to call this from the IO thread.
checkIfShouldFetch(newLocalData, _localData)
}
}
}
}
return this
}
My question is, how to come back to the root context (IO) from the nested context (Main)?
when i call again withContext(Dispatchers.IO) this error is displayed : Suspension functions can be called only within coroutine body
I need to call the function checkIfShouldFetch(newLocalData, _localData) from the IO context and i didn't find how to do it.
You would need to launch a coroutine to call withContext in that place. What you can try to do without launching a coroutine is to use suspendCoroutine or suspendCancellableCoroutine to suspend execution until callback is fired:
withContext(Dispatchers.Main) {
val newLocalData = addSource()
checkIfShouldFetch(newLocalData, _localData)
}
suspend fun addSource(): LiveData<...> = suspendCoroutine { continuation ->
mediatorLiveData.addSource(_localData) { newLocalData ->
mediatorLiveData.removeSource(_localData)
continuation.resumeWith(newLocalData)
}
}
suspend fun checkIfShouldFetch(newLocalData: ..., _localData: ...) = withContext(Dispatchers.IO) {
// ...
}
Related
I wish if you can elaborate about the difference when calling those 3 functions:
lifecycleScope.launch(Dispatchers.IO) {
var res = addTwoNumbers1(2,3)
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
var res = addTwoNumbers1(2, 3)
}
}
lifecycleScope.launch {
var res = addTwoNumbers2(2,3)
}
Functions:
suspend fun addTwoNumbers1(num1: Int, num2: Int): Int = suspendCoroutine { cont ->
val res = num1+num2
cont.resume(res)
}
suspend fun addTwoNumbers2(num1: Int, num2: Int) = withContext(Dispatchers.IO) {
val res = num1+num2
return#withContext res
}
First version launches a coroutine using Dispatcher.IO, meaning any code inside will execute on background thread, unless you change it
lifecycleScope.launch(Dispatchers.IO) {
var res = addTwoNumbers1(2,3) // This call executes on background thread (IO pool)
}
Second version launches a coroutine using Dispatchers.Main.immediate (UI thread, this is implicit for lifecycleScope)
lifecycleScope.launch { // Starts on UI thread
withContext(Dispatchers.IO) { // Here thread is changed to background (IO pool)
var res = addTwoNumbers1(2, 3)
}
}
Third one starts a new coroutine on UI thread and then calls a suspending function(doesn't actually suspend) which changes the Dispatcher to IO
lifecycleScope.launch { // Starts on UI thread
var res = addTwoNumbers2(2,3) // This function changes the dispatcher to IO
}
as for your suspending functions, addTwoNumbers1 is the only one that have the capability to suspend because it calls suspendCoroutine.
addTwoNumbers2 is not really a suspending function
This function is executed from my activity to check if a specific feature flag is enable, dispatcher.io() is injected so, for test mode the dispatcher is Main
fun isFeatureFlagEnabled(nameKey: String, completion: (enabled: Boolean) -> Unit) {
CoroutineScope(context = dispatcher.io()).launch {
val featureFlag = featureFlagsRepository.featureFlagsDao.getFeatureFlag(nameKey)
if (featureFlag != null) {
completion(featureFlag.isEnabled)
}
else {
completion(false)
}
}
}
This is the invocation if the function in the activity
private fun launchClearentCardOnFileBaseOnFeatureFlag(cardNavHelper: CardNavHelper){
featureFlagsHelper?.isFeatureFlagEnabled(CLEARENT_CARD_ON_FILE){enable ->
if(enable){
putFragment(
ClearentNativeFormFragment.newInstance(cardNavHelper))
}else{
putFragment(
ClearentCardOnFileFormFragment.newInstance(cardNavHelper))
}
}
}
And this is my test that fails because Espresso doesn't wait for the lambda function that i pass as parameter to return a response after check the database (it is an inMemoryDatabase)
#Test
fun testFFClearentNativeFormEnable(){
mockWebServer.setDispatcher(BusinessWithAddonsAndPaymentProcessorClearentDispatcher())
val application = ApplicationProvider.getApplicationContext<SchedulicityApplication>()
runBlocking(Dispatchers.Main) {
application.featureFlagsHelper.featureFlagsRepository.featureFlagsDao.insertFeatureFlags(
featureFlagsEnable
)
}
intent.putExtra(Constants.INTENT_KEY_CARD_ON_FILE_EXTRAS, cardNavHelperFromClientRecord)
activityTestRule.launchActivity(intent)
runBlocking { delay(10000) }
populateClearentForm(validCardNumber = false, validExpDate = true)
}
I have to put this delay runBlocking { delay(10000) } other ways fails.
So my question is. Do you know how i can wait for the coroutine response in the UI thread so my test could pass ?
Have a look at the espresso idling resources.
I have wrapped a callback in suspendCancellableCoroutine to convert it to a suspend function:
suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
val uniqueUtteranceId = getUniqueUtteranceId(text)
speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
Timber.d("word is read, resuming with the next word")
continuation.resume(true)
}
}
})
}
}
I'm calling this function with the lifecycleScope coroutine scope of the fragment and I was assuming that it was cancelled when fragment is destroyed. However, LeakCanary reported that my fragment was leaking because of this listener and I verified with logs that the callback was called even after the coroutine is cancelled.
So it seems that wrapping with suspendCancellableCoroutine instead of suspendCoroutine does not suffice to cancel the callback. I guess I should actively check whether the job is active, but how? I tried coroutineContext.ensureActive() and checking coroutineContext.isActive inside the callback but IDE gives an error saying that "suspension functions can be called only within coroutine body" What else can I do to ensure that it doesn't resume if the job is cancelled?
LeakCanary reported that my fragment was leaking because of this listener and I verified with logs that the callback was called even after the coroutine is cancelled.
Yes, the underlying async API is unaware of Kotlin coroutines and you have to work with it to explicitly propagate cancellation. Kotlin provides the invokeOnCancellation callback specifically for this purpose:
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
/* continuation.resume() */
})
continuation.invokeOnCancellation {
this.setOnUtteranceProgressListener(null)
}
}
If you want to remove your JeLisUtteranceProgressListener regardless of result (success, cancellation or other errors) you can instead use a classic try/finally block:
suspend fun TextToSpeech.speakAndWait(text: String) : Boolean {
val uniqueUtteranceId = getUniqueUtteranceId(text)
speak(text, TextToSpeech.QUEUE_FLUSH, null, uniqueUtteranceId)
return try {
suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener() {
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
Timber.d("word is read, resuming with the next word")
continuation.resume(true)
}
}
})
} finally {
this.setOnUtteranceProgressListener(null)
}
}
In addition to the accepted answer, I recognized that continuation object has an isActive property as well. So alternatively we can check whether coroutine is still active inside the callback before resuming:
return suspendCancellableCoroutine { continuation ->
this.setOnUtteranceProgressListener(object : JeLisUtteranceProgressListener()
{
override fun onDone(utteranceId: String?) {
if(utteranceId == uniqueUtteranceId) {
if (continuation.isActive) {
continuation.resume(true)
}
}
}
})
continuation.invokeOnCancellation {
this.setOnUtteranceProgressListener(null)
}
}
I've been mulling this over for some time now and I just can't get it to work.
So in brief, I have a Splash Activity from where I call another activity that contains my ViewModel. The ViewModel in simple terms just needs to sequentially run function A(which is getfbdata below; it is a network call.). And only after this function completes, it should run function B (which is dosavefbdata below; save info to DB.). Again, it should wait for function B to complete before running the main thread function, function C(which is confirm first below; it checks whether function B has completed by getting the result from function B (dosavefbdata below). If function C is positive, it closes the Splash activity.
Suffice to say, none of the above works. Println results show all functions were run sequentially without waiting for each to complete. Lastly, SplashActivity().killActivity() call on function C did not work.
Note: withContext does not require to await() on the suspended functions right? I also tried using viewModelScope.async instead of viewModelScope.launch.
I would really appreciate your help here. Thanks in advance.
*Under SplashActivity:
fun killActivity(){
finish()
}
*Under onCreate(SplashActivity):
CoroutingClassViewModel(myc).initialize()
**
class CoroutingClassViewModel(val myc: Context): ViewModel() {
fun initialize() {
viewModelScope.launch(Dispatchers.Main) {
try {
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
val test1 = withContext(Dispatchers.IO) { test1(fbdata) }
val savedfbdata = withContext(Dispatchers.IO) { dosavefbdata(fbdata,myc) }
val confirmfirst = { confirmfunc(savedfbdata,myc) }
println("ran savedfbdata.")
} catch (exception: Exception) {
Log.d(TAG, "$exception handled !")
}
}
}
fun confirmfunc(savedfbdata: Boolean, myc: Context){
if (savedfbdata==true){
SplashActivity().killActivity()
}
}
suspend fun getfbdata(): MutableList<FirebaseClass> {
return withContext(Dispatchers.IO) {
//perform network call
return#withContext fbdata
}
}
suspend fun dosavefbdata(fbdata: MutableList<FirebaseClass>,myc: Context): Boolean{
return withContext(Dispatchers.IO) {
//save to database
return#withContext true
}
}
suspend fun test1(fbdata: MutableList<FirebaseClass>){
return withContext(Dispatchers.IO) {
println("test1: fbdata is: $fbdata")
}
}
}
Use AndroidViewModel if you want to have Context in it:
class CoroutingClassViewModel(myc: Application) : AndroidViewModel(myc) { ... }
In onCreate method of SplashActivity activity instantiate the view model like this:
val vm = ViewModelProvider(this)[CoroutingClassViewModel::class.java]
vm.initialize()
In CoroutingClassViewModel class create LiveData object to notify activity about operations completion:
val completion = MutableLiveData<Boolean>()
fun confirmfunc(savedfbdata: Boolean, myc: Context) {
if (savedfbdata) {
completion.postValue(true)
}
}
In your SplashActivity use this code to observe completion:
vm.completion.observe(this, Observer {
if (it) killActivity()
})
You use withContext(Dispatchers.IO) function two times for the same operation. Don't do that. For example in this code:
val fbdata = withContext(Dispatchers.IO) { getfbdata() }
if we look at getfbdata function we see that function withContext(Dispatchers.IO) is already called there. So get rid of repeated calls:
val fbdata = getfbdata()
I had same issue with withContext(Dispatcher.IO), I thought that switching coroutine context doesn't work, while in fact in splash screen i launched super long operation on Dispatcher.IO, then later when trying to use the same Dispatcher.IO it didn't work or in other words it waited until the first work in splash screen finished then started the new work.
I have a legacy project where I want to use coroutines when contacting the backend. The backend have been handle by a sdk delivered by Hybris. It use volley for instance, and with some callbacks. What I want is to wrap those callbacks with a coroutine. But the problem I have is that the coroutine doesn't wait to be done, it start the coroutine, and keep going to next lines, and method returns a value, and long after that the coroutine finish.
My code:
suspend fun ServiceHelper.getList(): ListOfWishes {
return suspendCancellableCoroutine { continuation ->
getAllLists(object : ResponseReceiver<ListOfWishes> {
override fun onResponse(response: Response<ListOfWishes>?) {
continuation.resume(response?.data!!)
}
override fun onError(response: Response<ErrorList>?) {
val throwable = Throwable(Util.getFirstErrorSafe(response?.data))
continuation.resumeWithException(throwable)
}
}, RequestUtils.generateUniqueRequestId(), false, null, object : OnRequestListener {
override fun beforeRequest() {}
override fun afterRequestBeforeResponse() {}
override fun afterRequest(isDataSynced: Boolean) {}
})
}
}
The helper method:
suspend fun ServiceHelper.wishLists(): Deferred<ListOfWishes> {
return async(CommonPool) {
getWishList()
}
}
And where the coroutine is called:
fun getUpdatedLists(): ListOfWishes? {
val context = Injector.getContext()
val serviceHelper = Util.getContentServiceHelper(context)
var list = ListOfWishLists()
launch(Android) {
try {
list = serviceHelper.wishLists().await()
} catch (ex: Exception){
Timber.d("Error: $ex")
}
}
return list
So instead of waiting for serviceHelper.wishLists().await() is done, it return list. I have also tried to make the method return a runBlocking{}, but that only block the UI thread and doesn't end the coroutine.
Coroutines don't work like this. getUpdatedLists() method can't wait a coroutine to finish its execution without being a suspend method itself. If method getUpdatedLists() defined in Activity or Fragment, you can launch a coroutine and do something, e.g. update UI, in it after serviceHelper.wishLists().await() is executed. It will look something like this:
fun loadUpdatedLists() {
val context = Injector.getContext()
val serviceHelper = Util.getContentServiceHelper(context)
lifecycleScope.launch {
try {
val list = serviceHelper.wishLists().await()
// use list object, for example Update UI
} catch (ex: Exception){
Timber.d("Error: $ex")
}
}
}
lifecycleScope - CoroutineScope tied to LifecycleOwner's Lifecycle. To use it add dependency:
androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 or higher.