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")
}
Related
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 have written a new usecase to communicate to api which uses Flow, I am guessing I am not handing the threading properly in the Usecase between Main thread and IO thread,
This is the error I get
-01-18 02:20:40.555 26602-26870/com.xxx.xx.staging E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-4
Process: com.xxx.xx.staging, PID: 26602
java.lang.IllegalStateException: Event bus [Bus "fill_order"] accessed from non-main thread null
at com.squareup.otto.ThreadEnforcer$2.enforce(ThreadEnforcer.java:47)
at com.squareup.otto.Bus.post(Bus.java:317)
at com.xxx.xx.fragments.filller.fillorder.BasefillOrderFragment.postFldeckStatusUpdateEvent(BasefillOrderFragment.java:117)
at com.xxx.xx.fragments.filller.fillorder.fillOrderDataFragment.postFldeckStatusUpdateEvent(fillOrderDataFragment.java:1955)
at com.xxx.xx.fragments.filller.fillorder.fillOrderDataFragment.updateView(fillOrderDataFragment.java:845)
at com.xx.xx.fragments.filller.fillorder.fillOrderDataFragment.legacyUpdateView(fillOrderDataFragment.java:2317)
at com.xxx.xx.clean.fillorder.presenter.BasefillDataPresenter.onStartfilllingSuccess(BasefillDataPresenter.kt:460)
at com.xxx.xx.clean.fillorder.presenter.BasefillDataPresenter.handleStartfilllingClicked(BasefillDataPresenter.kt:315)
at com.xxx.xx.clean.fillorder.presenter.BasefillDataPresenter.access$handleStartfilllingClicked(BasefillDataPresenter.kt:49)
The error is at handleStartfilllingClicked(view, it) in . collect
I am calling startfilllingUseCaseFlow usecase which might be the issue
#FlowPreview
fun initFlowSubscription(view: View) {
launch {
view.startfilllingObservableFlow
.conflate()
.catch {
onStartfilllingError(view)
}
.flatMapMerge {
if (!hasOpenInopIncidents()) {
equipmentProvider.get()?.let {
startfilllingUseCaseFlow(StartfilllingUseCaseFlow.Params(it))
}!!
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
flowOf(incidentOpenResponse)
}
}
.collect {
handleStartfilllingClicked(view, it) // ERROR IS HERE
}
}
}
private fun handleStartfilllingClicked(view: View, response: GenericResponse) {
if (response.success == false && response.error == OPEN_INCIDENTS) {
view.showCannotProceedInopIncidentDialog()
view.hideLoader(false)
return
}
onStartfilllingSuccess(view) // Error is here
}
StartfilllingUseCaseFlow
class StartfilllingUseCaseFlow #Inject constructor(
private val currentOrderStorage: CurrentOrderStorage,
private val fillOrderRepository: fillOrderRepository,
private val app: App
): FlowUseCase<StartfilllingUseCaseFlow.Params, GenericResponse>() {
override suspend fun run(params: Params): Flow<GenericResponse> {
val startTime = DateTime()
val action = TimestampedAction(
app.session.user.id, null, startTime
)
return flowOf(fillOrderRepository.startfilllingSuspend(
currentOrderStorage.fillOrder!!.id,
action
)).onEach { onSuccess(startTime, params.equipment) }
.catch { e -> e.message?.let { onError(it) } }
.flowOn(Dispatchers.IO)
}
private fun onSuccess(startTime: DateTime, equipment: Equipment) {
if (currentOrderStorage.getfillOrder() == null) return
currentOrderStorage.getfillOrder()!!.setStatus(fillOrderData.STATUS_fillLING)
equipment.times.start = startTime
app.saveState()
}
private fun onError(errorMessage: String) {
Timber.e(errorMessage, "Error calling started fillling! %s", errorMessage)
}
data class Params(val equipment: Equipment)
}
I am guessing I am not handing IO and Main thread properly here
abstract class FlowUseCase<in Params, out T>() {
abstract suspend fun run(params: Params): Flow<T>
suspend operator fun invoke(params: Params): Flow<T> = run(params).flowOn(Dispatchers.IO)
}
Could you suggest where I am gettig it wrong
Thanks
R
You are trying to update the view in Coroutines default thread. All views updates must be in the MainThread.
try:
fun initFlowSubscription(view: View) {
launch(Dispatchers.Main) {
//enter code here
}
}
This might give another error because you are doing too much process in the main thread. To avoid that you. could use "async" and update your view after:
Exemple:
fun initFlowSubscription(view: View) {
launch(Dispatchers.Main) {
val asyncValue = async(Dispatchers.IO) {
//Do yours suspend fun
}
val value = asyncValue.await()
}
}
This example should yours fine and avoid stopping the users UI
Coroutines sometimes consume unhandled exceptions (this seems to be more prevalent when using async/await). Anyway, add a CoroutineExceptionHandler in these cases.
CoroutineScope(IO + coroutineExceptionHandler).launch {
//perform background task
}
val coroutineExceptionHandler = CoroutineExceptionHandler{_, throwable ->
Log.d("coroutineExceptionHandler", "yes this happened")
throwable.printStackTrace()
}
I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}
For example i have construction like this:
lifecycleScope.launch {
viewModel.handleAppLoad() {
val app = AppFactory.createApp(
context = Application.instance.applicationContext
)
app.doSmth()
startActivity(
SuccessActivity.createIntent(
requireContext()
)
)
}
}
In my fragment code, when i clicked on some button.
suspend fun handleAppLoad(
scope: CoroutineScope = viewModelScope,
block: suspend () -> Unit
) {
scope.launch {
progress.value = true
try {
delay(1000)
block()
} catch (ex: MsalOperationCanceledException) {
// B2C process was cancelled, do nothing
} catch (ex: MsalException) {
_msalErrorEvent.emit(ex)
Timber.e(ex)
}
progress.value = false
}
}
^ My coroutine wrapper
Also i have this code in AppFactory.
object AppFactory {
suspend fun createApp(
context: Context
): App {
return suspendCoroutine { cont ->
App.create(
context,
object : IApp.ApplicationCreatedListener {
override fun onCreated(application: App) {
cont.resume(application)
}
override fun onError(exception: Exception) {
cont.resumeWithException(exception)
}
}
)
}
}
}
The problem is that when the application goes to the background and the callback cont.resume(application) works in the background, the coroutine does not stop, but continues to wait for the same cont.resume(application), so that's why my progress stay active, while cont.resume(application)already happened. I know a way to fix it by removing the callback->coroutine construction, but I am interested in the way to fix the current version, since the coroutine has a wrapper that controls the progress at the start and end of the coroutine.
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
}
...
}