I'm new to coroutines and having a hard time figuring out how to correctly wrap an existing callback in a coroutine.
My goal is to be able to do the following:
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val theResult = getPreRollAd() //1. call this suspending func and wait for result
doSomethingWithResult(theResult) //2. now that the result is returned from AdsWizz API (below), do something with it
}
}
Here is the AdsWizz API call that I'd like to "wrap":
val adReqInterface: AdRequestHandlerInterface = object :
AdRequestHandlerInterface {
override fun onResponseError(error: AdswizzSDKError) {
Timber.e("onResponseError $error")
}
override fun onResponseReady(adResponse: AdResponse) {
Timber.d( "onResponseReadySingleAd")
//this contains the url to the ad, title, etc..
!!!*** I WANT TO RETURN THE adResponse.mediaFile?.source string back to "theResult" variable above (in lifecycleScope.launch {.... )
}
}
try {
AdswizzSDK.getAdsLoader().requestAd(adReqParams, adReqInterface)
} catch (e: IllegalArgumentException) {
Timber.d( "IllegalArgumentException")
} catch (e: SecurityException) {
Timber.d( "SecurityException")
} catch (e: Exception) {
Timber.d( "other exception")
e.printStackTrace()
}
I've tried using suspendCoroutine {... to wrap but nothing is working. Really appreciate someones help re the right way to achieve this.
the right way to do it is to use suspendCancellableCoroutine. It can return a result or can be cancelled with an exception.
suspend fun getPreRollAd(): AdResponse {
return suspendCancellableCoroutine {
...
val adReqInterface: AdRequestHandlerInterface = object : AdRequestHandlerInterface {
override fun onResponseError(error: AdswizzSDKError) {
Timber.e("onResponseError $error")
it.cancel(error)
}
override fun onResponseReady(adResponse: AdResponse) {
Timber.d( "onResponseReadySingleAd")
it.resume(adResponse)
}
}
AdswizzSDK.getAdsLoader().requestAd(adReqParams, adReqInterface)
}
}
viewModelScope.launch {
val result = try {
getPreRollAd()
} catch(e: Throwable) {
null
}
...
}
Related
I'm using the code below for a network request throught okhttp3:
runOnDefaultDispatcher {
try {
val newsResponse = Xxxx.xxxxClient.getNews()
if (newsResponse.success && newsResponse.data != null && newsResponse.data.count() > 0) {
runOnMainDispatcher {
val adapter = NewsAdapter(newsResponse.data, getString(R.string.news)).also {
initListener(it, newsResponse.data)
}
binding.list.adapter = adapter
adapter.notifyDataSetChanged()
}
}
} catch (exception: Exception) {
runOnMainDispatcher {
binding.list.visibility = View.GONE
val errorLayout = view.findViewById<RelativeLayout>(R.id.error_layout)
errorLayout.visibility = View.VISIBLE
errorLayout.findViewById<TextView>(R.id.error_title).text = "Oops..."
errorLayout.findViewById<TextView>(R.id.error_message).text = exception.message
}
}
}
The implementation code of runOnDefaultDispatcher and runOnMainDispatcher is down below:
import kotlinx.coroutines.*
fun block(block: suspend CoroutineScope.() -> Unit): suspend CoroutineScope.() -> Unit {
return block
}
fun runOnDefaultDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.Default, block = block)
suspend fun <T> onDefaultDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.Default, block = block)
fun runOnIoDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.IO, block = block)
suspend fun <T> onIoDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.IO, block = block)
fun runOnMainDispatcher(block: suspend CoroutineScope.() -> Unit) =
GlobalScope.launch(Dispatchers.Main.immediate, block = block)
suspend fun <T> onMainDispatcher(block: suspend CoroutineScope.() -> T) =
withContext(Dispatchers.Main.immediate, block = block)
I except the exception would be caught and no crash would appear.
However the application still CRASH:
FATAL EXCEPTION: DefaultDispatcher-worker-2
Java.net.SocketException: Connection reset
The calls to launch don't work well with try/catch.
e.g. this will crash the app
try {
GlobalScope.launch { throw Excepton() }
} catch (e: Exception) {
}
On the other hand, suspend functions work with try/catch as you would expect so this example DOES NOT crash the app:
suspend fun bang(): Unit = throw Exception()
try {
bang()
} catch (e: Exception) {
}
In your code you have launch inside try/catch, meaning you have a scenario like the first example here.
The solution is to build your program as suspend functions, and only use launch one to execute the result (note: this doesn't apply universally but does apply in this scenario).
When running the program you probably want to use lifecycleScope.
Also you might want to consider using a ViewModel so that the network call survives configuration changes.
You can check the Kotlin Coroutines on Android guide for more.
You can see the difference between to code blocks. The first one will crash because exception occurs in a different thread. The second one will not crash because you are catching exception in the right place.
fun main() {
val scope = CoroutineScope(Job() + Dispatchers.Default)
//First way
try {
scope.launch {
exceptionThrowingFunction()
}
} catch (e: Exception) {
println(e.message)
}
//Second way
scope.launch {
try {
exceptionThrowingFunction()
} catch (e: Exception) {
println(e.message)
}
}
Thread.sleep(10000)
}
private suspend fun exceptionThrowingFunction() {
delay(10)
throw IllegalArgumentException("Test Error")
}
I have the following setup
Service
// ItunesService
suspend fun searchItunesPodcast(#Query("term") term: String): Response<PodcastResponse>
Repository
// ItunesRepo
override suspend fun searchByTerm(term: String) = withContext(ioDispatcher) {
return#withContext itunesService.searchItunesPodcast(term)
}
ViewModel
fun searchPodcasts(term: String) {
viewModelScope.launch {
_res.value = Result.loading()
try {
val response = itunesRepo.searchByTerm(term)
if (response.isSuccessful) { // Nothing from here when no internet
_res.value = Result.success(response.body())
} else {
_res.value = Result.error(response.errorBody().toString())
}
} catch (e: Exception) {
_res.value = Result.exception(e)
}
}
}
Everything works great until i turn off mobile data/internet on my testing device. _res value stuck on Loading state. I have tried adding break point at if (response.isSuccessful) when there is no internet and it seams like val response = itunesRepo.searchByTerm(term) never returns how can I fix this
I switched to using Flow api on my Repository
override suspend fun searchPodcasts(term: String) = flow {
emit(Result.Loading)
try {
val res = itunesService.searchItunesPodcast(term)
if (res.isSuccessful)
emit(Result.Success(res.body()))
else
emit(Result.Error("Generic error: ${res.code()}"))
} catch (e: Exception) {
emit(Result.Error("Unexpected error", e))
}
}.flowOn(ioDispatcher)
Then collect the results on my ViewModels
I am try to cancel to api request if user calls api to fast then only the latest api should return the result all previous requests should be discarded but this isn't working anyone knows the solution please help thanks
class CartViewModel(val store: Account) : BaseViewModel() {
private var requestCalculation: Job? = null
fun recalculate() {
requestCalculation.let {
if (it != null) {
if (it.isActive) {
requestCalculation!!.cancel()
}
}
}
requestCalculation = viewModelScope.launch(Dispatchers.IO) {
isLoading.postValue(true)
try {
val order = CCOrderManager.shared.calculateTaxesAndApplyRewards(store.id)
refreshOrder()
} catch (e: Exception) {
exception.postValue(e.localizedMessage ?: e.toString())
}
}
}
}
The order of cancellation and execution is wrong. When the function starts, requestCalculation is null, so it cannot be canceled. Make sure you start first the coroutine and cancel it later. For example:
private var requestCalculation: Job? = null
fun recalculate() {
requestCalculation = viewModelScope.launch(Dispatchers.IO) {
delay(10_000)
// do your work...
}
// now the job can be canceled
requestCalculation?.cancel()
}
Adding a check after api call this.isActive {return#launch} finally worked for me...
fun recalculate() {
calculationRequest?.cancel()
isLoading.postValue(true)
calculationRequest = viewModelScope.launch(Dispatchers.IO) {
try {
val order =
CCOrderManager.shared.calculateTaxesAndApplyRewards(store.id)
// this check is the solution *******
if (!this.isActive) {return#launch}
val catalog = CatalogManager.shared().catalog
} catch (e: Exception) {
}
}
}
I have two quite similar functions and I'm trying to avoid duplication in my code by the use of generics. The functions have both a try catch block and notify its observers with two MutableLiveData of two different types:
val noWasteRecipesPosts: MutableLiveData<List<Recipe>> = MutableLiveData()
val lastArticlesPosts: MutableLiveData<List<Article>> = MutableLiveData()
fun getNoWasteRecipesPosts() {
makeCall(service.getRecipes(), noWasteRecipesPosts)
scope.launch {
try {
val response = service.getRecipes().await()
when (response.isSuccessful) {
true -> {
response.body()?.let {
noWasteRecipesPosts.postValue(ArrayList(response.body()))
} ?: run {
errorLiveData.postValue(response.message())
}
}
false -> errorLiveData.postValue(response.message())
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
fun getLastArticlesPosts(excludeRecipes: Boolean) {
scope.launch {
try {
val response = when (excludeRecipes) {
true -> service.getLastArticles(categoriesToExclude = arrayListOf(BlogCategories.NO_WASTE_RECIPES.id))
.await()
false -> service.getLastArticles()
.await()
}
when (response.isSuccessful) {
true -> {
response.body()?.let {
lastArticlesPosts.postValue(ArrayList(response.body()))
} ?: run {
errorLiveData.postValue(response.message())
}
}
false -> errorLiveData.postValue(response.message())
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
To avoid code repeating I'm trying to use generics, but probably in the wrong way. I've defined a function that takes the Deferred api response as first parameter and I would like to pass a MutableLiveData to notify observers as the second parameter:
fun makeCall(function: Deferred<Response<*>>, successLiveData: MutableLiveData<*>) {
scope.launch {
try {
val response = function.await()
when (response.isSuccessful) {
true -> {
response.body()?.let {
successLiveData.postValue(it) // Compile error here
} ?: run {
errorLiveData.postValue(response.message())
}
}
false -> errorLiveData.postValue(response.message())
}
} catch (e: Exception) {
noConnectionLiveData.postValue(true)
}
}
}
Unfortunately I'm missing something and the IDE is giving me a Type mismatch error trying to post the LiveData value:
Type mismatch: Required : Nothing! Found: Any.
I'm quite confused, do you have any suggestion to make about MutableLiveData and Generics in kotlin?
The response.body() type and the MutableLiveData type must match. The function signature should be something like this:
fun <T> makeCall(function: Deferred<Response<T>>, successLiveData: MutableLiveData<T>)
In my android app I has long network operation. After operation is finished I need to update my ui.
So as result long operation need to execute in background thread.
Snippet:
private val isShowProgressLiveData = MutableLiveData<Boolean>()
// launch a new coroutine in background and continue
GlobalScope.launch() {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}
But I get error on
isShowProgressLiveData.value = false
Error message:
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:461)
at androidx.lifecycle.LiveData.setValue(LiveData.java:304)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
at com.myoperation.AddTraderViewModel$doClickStart$3.invokeSuspend(AddTraderViewModel.kt:55)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
I need to update my ui in main thread. So how I can fix this?
You can switch to main thread like with the withContext(Dispatchers.Main)
When you fire a coroutine with launch will start on the Dispatchers.Default. The best is to specify it like this:
GlobalScope.launch(Dispatchers.IO) {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
withContext(Dispatchers.Main){ //switched to Main thread
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
}
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}
EDIT: Or you can just use .postValue() instead of .value or .setValue() and forget about withContext().
You're still in background thread when you try to execute
isShowProgressLiveData.value = false
You can do something like this:
GlobalScope.launch() {
try {
val executeOperations = OperationFactory.createExecuteTraderOperation(Trader.Operation.CREATE, base, quote)
val response: Response<Void> = executeOperations.await()
if (response.isSuccessful) {
YourActivity.runOnUiThread(object:Runnable() {
override fun run() {
isShowProgressLiveData.value = false
isForwardToTradersLiveData.value = true
}
}
} else {
Debug.w(TAG, "doClickStart_error")
}
} catch (e: Throwable) {
Debug.e(TAG, "doClickStart_network error: $e.message", e)
}
}