Why coroutine's await function doesn't finish? - android

I am trying coroutines to make network request using Retrofit on background thread. First of all, I changed Retrofit's Call to Deferred. Here's how it looks like:
#GET("some_endpoint")
fun getData(#Query("id") id: Int): Deferred<JsonObject>
So, in order to use above function, I created suspend function and used await. Here's how it looks:
suspend fun getNetworkData(id: Int): Resource<JsonObject> {
try {
val data = api.getData(id).await()
return Resource.success(data)
} catch (e: Exception) {
return Resource.error()
}
}
But, when I debug my app, await never finishes. Break point never comes to return statement. So, I decided to replace Deferred to Retrofit's Call again. And, instead of await, I decided to use Retrofit's execute. Of course, I removed suspend keyword.
Here's how it looks now:
#GET("some_endpoint")
fun getData(#Query("id") id: Int): Call<JsonObject>
fun getNetworkData(id: Int): Resource<JsonObject> {
try {
val data = api.getData(id).execute()
return Resource.success(data.body())
} catch (e: Exception) {
return Resource.error()
}
}
Then, it worked like a charm. I successfully got my data. But, I want to understand why this happened. In others words, why coroutine await call didn't finish or even didn't run catch block, when Retrofit's Call and execute finished task successfully?
If it is needed, I provide how I call above getNetworkData function:
launch {
val result = withContext(Dispatchers.IO) { repo.getNetworkData(id) }
//do something
}

Related

Is it necessary to use return withContext(Dispatchers.IO)?

I'm using Firebase authentication. In the repository I have this function:
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
This function is called from within the ViewModel class:
var response by mutableStateOf<Result<Boolean>>(Result.Success(false))
private set
fun signIn() = viewModelScope.launch {
response = repository.signIn()
}
Which works fine but I was suggested to use in the ViewModel:
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
To add Dispatchers.IO and inside the repository:
override suspend fun signIn(): Result<Boolean> {
return withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
To launch a coroutine using withContext. I and I don't understand why? I'm using Jetpack Compose.
Whoever suggested changing your code is wrong.
It is a general Kotlin coroutines convention that suspend functions never need to be called on a specific dispatcher, specifically because they must never block. They always internally delegate to a specific dispatcher if they need one. (But perhaps as an optimization, a private suspend function might avoid doing it for a function that must be called on the Main dispatcher.)
Since this is a convention, all the libraries from Google, Android, Square, etc. and anyone else who knows what they're doing, only have suspend functions that can be called from any dispatcher.
This includes the await() call you're using with Firebase. Therefore, your repository's signIn function is already perfectly fine as-is. Since it doesn't call any blocking functions, and the suspend function it calls is a proper suspend function that also does not block, it conforms to the standard (it doesn't block).
The function in your ViewModel is also fine. No dispatcher needs to be specified.
Actually, since you are already calling signIn from a coroutine started with Dispatchers.IO you don't have to use return withContext(...).
Since your repository method is suspend, it is able to call coroutines without special blocks like withContext.
// This line tells to launch code on separate IO thread, to avoid UI freezing
// Since default viewModelScope.launch runs on Dispatchers.Main, which is
// also used for rendering
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
response = repository.signIn()
}
In your repository you can just
// Since signIn was called on IO context from viewModel, it will also
// return on IO
override suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
We have a two ways to start coroutine async and launch.
launch will use to perform serial/sequence task in background.
async is used when we expect some result back and also want to perform parallel operation.
Same way withContext is nothing but another way of writing the async where one does not have to write await(). When withContext, is used, it runs the tasks in series instead of parallel. So one should remember that when we have a single task in the background and want to get back the result of that task, we should use withContext.
In your case you can change your code as below
fun signIn() = viewModelScope.launch(Dispatchers.IO) {
val response = async { repository.signIn()}.await()
}
and remove withContext
suspend fun signIn(): Result<Boolean> {
return try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
One more way if you don't want to use return with withContext
override suspend fun signIn() = {
withContext(Dispatchers.IO) {
try {
auth.signInAnonymously().await()
Result.Success(true)
} catch (ex: Exception) {
Result.Failure(ex)
}
}
}
In Nutshell if you expecting some result from your task then you have to use async or withContext.
Hope I am able to solve your problem or issue.

How to call multiple api concurrently and read headers using Retrofit, Coroutines with Async/Await method in Kotlin Android

My aim to call five apis and to get headers from those api response.
I have added my code below
Api service class
#GET("users")
suspend fun getUserList(): Call<List<FriendListModel>>
Repo class
suspend fun getList(): Response<List<FriendListModel>> {
return apiService.getUserList().execute()
}
ViewModel class
fun getFriends() {
viewModelScope.launch(Dispatchers.IO) {
val data =
async {
try {
val data = friendListRepo.getList()
val header = data.headers().get("id")
/*
* need to add header logic
*/
Resource.success(data)
} catch (throwable: Throwable) {
when (throwable) {
is HttpException -> {
Resource.error(false, throwable.response()?.message()?:"")
}
else -> {
Resource.error(false, "")
}
}
}
}
val res = data.await()
mutableFriendsList.postValue(res)
}
}
My question is, am I doing it in right way because I am getting a warning in repo class saying that "Inappropriate blocking method call" since I am calling execute() method though I am calling it in suspend function.
[I referred] Kotlin coroutines await for 2 or more different concurrent requests.
Is there any other approach to achieve this?
You should not combine suspend with Call. Call is for asynchronous work. suspend does asynchronous work synchronously by suspending. It can't be both at once. execute does a blocking synchronous fetch of the data, which shouldn't be done in a coroutine.
So, your functions should look like:
#GET("users")
suspend fun getUserList(): List<FriendListModel>
suspend fun getList(): List<FriendListModel> {
return apiService.getUserList()
}
Then when you use it in a coroutine, you don't need async because you're just calling a synchronous suspend function. You also don't need to fool with Dispatchers.IO since you're only using a suspend function (not doing blocking work). I also simplified your catch block in this example, but that's not related to the solution (I just couldn't help myself).
fun getFriends() {
viewModelScope.launch {
mutableFriendsList.value = try {
val data = friendListRepo.getList()
val header = data.headers().get("id")
/*
* need to add header logic
*/
Resource.success(data)
} catch (throwable: Throwable) {
Resource.error(false, (throwable as? HttpException)?.response()?.message.orEmpty())
}
}
}
Side note, even when you are calling blocking code, you should never need to use async immediately followed by an await() call on it. That is just a convoluted alternative to withContext.

Is it necessary to use .flowOn(Dispatchers.IO) inside repository class?

I'm trying to implement One Tap, so I have created this function:
override fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
emit(Result.Error(e.message))
}
}
//.flowOn(Dispatchers.IO)
And some programmer told me that I need to add .flowOn(Dispatchers.IO) to the above function, so it can be correct. My code work correct without it. Here is how I call this function in the ViewModel:
fun oneTapSignIn() = viewModelScope.launch {
repo.oneTapSignInWithGoogle().collect { response ->
oneTapSignInResponse = response
}
}
Is it really necessary to do that? I'm really confused.
You're calling beginSignIn which returns a Task, so it does its own stuff in the background. Now Task.await is suspending, not blocking, so it won't block the current thread while waiting for the task.
Therefore, the body of your flow doesn't contain any blocking stuff, so there is no reason to use flowOn(Dispatchers.IO) here.

Kotlin Coroutines Flow catch mechanism

In my sample I'm calling network operation and emitting success case but on error e.g 404 app crashes wihout emitting exception. Surrendering with try catch prevent crashes but I want to pass error till the ui layer like success case.
suspend fun execute(
params: Params,
):
Flow<Result<Type>> = withContext(Dispatchers.IO) {
flow {
emit(Result.success(run(params)))
}.catch {
emit(Result.failure(it))
}
}
There is a helpful function runCatching for creating a Result easily, but the problem in coroutines is that you don't want to be swallowing CancellationExceptions. So below, I'm using runCatchingCancellable from my answer here.
This shouldn't be a Flow since it returns a single item.
If run is a not a blocking function (it shouldn't be if you are using Retrofit with suspend functions), your code can simply be:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
run(params)
}
If it is a blocking function you can use:
suspend fun execute(params: Params): Result<Type> = runCatchingCancellable {
withContext(Dispatchers.IO) {
run(params)
}
}
If you were going to return a Flow (which you shouldn't for a returning a single item!!), then you shouldn't make this a suspend function, and you should catch the error inside the flow builder lambda:
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
run(params)
})
}
// or if run is blocking (it shouldn't be):
fun execute(params: Params): Flow<Result<Type>> = flow {
emit(runCatchingCancellable {
withContext(Dispatchers.IO) { run(params) }
})
}
If you want to use flows you can use the catch method of flows.
As you said you can use try-catch but it would break the structured concurrency since it would catch the cancellation exception as well or it would avoid the cancellation exception to be thrown.
One thing that you can do is to use an Exception handler at the point where you launch the root coroutine that calls the suspend function.
val coroutineExceptionHandler = CoroutineExceptionHandler { _, exception ->
// handle it
}
scope.launch(handler) { // root coroutine
execute(params)
somethingThatShouldBeExecutedOnlyIfPreviousCallDoesNotThrow()
}
This solution is good for both flows and non-flow coroutines.
In the solution with the runCatching you will have to manually check the result of the first execute to avoid the second one to run.
One interesting thread is here.

Suspend Coroutine Hangs

Trying to get a deeper into coroutines. I have a suspendCancellableCoroutine that is supposed to fetch a network response. I can see in Charles that the network call is dispatched and returns successfully. However, my app just hangs on the network request line.
private suspend fun fetchVisualElementsFromServer(clubId: String): VisualElements {
return suspendCancellableCoroutine { cont ->
visualElementsService.fetchVisualElementsForClub(clubId)
.enqueue(object : Callback<ResultVisualElements> {
override fun onResponse(
call: Call<ResultVisualElements>,
response: Response<ResultVisualElements>
) {
if (response.isSuccessful) {
response.body()?.let {
if (it.result == RESULT_SUCCESS) {
saveVisualElementsResponseInSharedPreferences(it.visual_elements)
cont.resume (it.visual_elements)
} else {
cont.cancel() //edit
}
} ?: cont.cancel() //edit
} else {
cont.cancel(IOException("${response.code()}: ${response.errorBody()}"))
}
}
override fun onFailure(call: Call<ResultVisualElements>, t: Throwable) {
Timber.e(t, "visual elements fetch failed")
cont.cancel() // edit
}
})
}
}
This where it hangs:
VisualElementsService.kt
fun fetchVisualElementsForClub(clubId: String): Call<ResultVisualElements> {
return dataFetcherService.getVisualElementsForClub(clubId)
}
What am I missing here? I tried to make the fetchVisualElementsForClub() a suspend function, but that just makes the suspendCancellableCoroutine throw a Suspension functions can only be called within coroutine body error. But I thought that his was within a coroutine body?
Any help appreciated. Thanks.
EDIT
I response to Rene's answer below, I want to add a few things.
You are right, I am missing three cont.cancel() calls. I've modified the OP. Good points.
I have breakpoints all over the suspendCancellableCoroutine such that any possible scenario (success, failure, etc.) will be hit. But that callback never registers.
Wondering if there is something missing in fetchVisualElementsForClub() that is needed to pass the callback up to the suspendCancellableCoroutine. That seems to be where this is hanging.
You must call cont.resume() or cont.cancel() on every branch in your callback handling.
But in your example at least three cases are missing.
If the response is successful but no body is provided, you call nothing.
If the response is successful, the body is not null, but the it.result is not RESULT_SUCCESS you call nothing.
If something goes wrong in onFailure, you call nothing.
As long as neither resume or cancel is invoked, the coroutine will stay suspended, means hangs.
when you use suspend keyword your are telling that function shoud be called inside a coroutine bode, for example:
suspend fun abc(){
return
}
when you want to call above function you have to call it inside coroutines such as below:
GlobalScope.launch {
abc()
}

Categories

Resources