Suspend Coroutine Hangs - android

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()
}

Related

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.

Non suspend function is not executed after suspend function

I have a suspend function userRepo.updateUserWithNewPersonalDetails(details) and after executing this function I want to execute success() function which is a call back.
but the issue is success() is not getting executed.
any suggestions on how to get this to work.
this sequence does not work
SUCCESS -> {
progress.postValue(GONE)
userRepo.updateUserWithNewPersonalDetails(details) // EXECUTED
success() // NOT EXECUTED
}
if I change it to call success() first then save to repo, it works fine. but this is not the right way of doing it I think.
could you suggest please
SUCCESS -> {
progress.postValue(GONE)
success() // EXECUTED
userRepo.updateUserWithNewPersonalDetails(details) // EXECUTED
}
Fragment call
viewModel.save(personalDetails) { activity?.onBackPressed() }
ViewModel
fun save(details: PersonalDetails, success: () -> Unit) {
viewModelScope.launch {
userRepo.savePersonalDetails(details).collect {
when (it.status) {
LOADING -> {
progress.postValue(VISIBLE)
}
SUCCESS -> {
progress.postValue(GONE)
userRepo.updateUserWithNewPersonalDetails(details)
success() // THIS IS NOT EXECUTED
}
ERROR -> {
progress.postValue(GONE)
error.postValue(ErrorResult(errorCode = SNACKBAR_ID_USER_DETAILS_SAVE_FAIL))
}
}
}
}
}
userRepository
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
userDao.get().collect { cachedUser ->
val updatedCachedUser = UserDB(cachedUser.id, etc..)
userDao.save(updatedCachedUser)
}
}
Can you please show me the function that you call?, did you already use the breakpoint to make sure the function it self was called?. cause if you dont, i think you might use nullable variable and the value will retrieved after the suspend function (userRepo.blabla()) finished, if yes. maybe you can call .invokeOnCompletion { /your Success Function/ success() }
success() method isn't called because you collect Flow in updateUserWithNewPersonalDetails method:
userDao.get().collect {...}
It suspends a coroutine execution. My guess is that it is an infinite Flow, which doesn't complete until coroutine is completed. That's why userDao.get().collect suspends execution.
I don't quite understand what you are trying to achieve in the updateUserWithNewPersonalDetails method, but it seems it doesn't update the DB. If you want to update user details in the DB, you don't need to collect Flow. You should have something like this:
suspend fun updateUserWithNewPersonalDetails(details: PersonalDetails) {
userDao.update(details)
}
where userDao.update(details) is a suspend method, which updates DB:
suspend fun update(details: PersonalDetails)

Kotlin Coroutine wait for Retrofit Response

I'm trying to use the Android MVVM pattern with a repository class and Retrofit for network calls. I have the common problem that I can't get the coroutine to wait for the network response to return.
This method is in my ViewModel class:
private fun loadConfigModel() {
val model = runBlocking {
withContext(Dispatchers.IO) {
configModelRepository.getConfigFile()
}
}
configModel.value = model
}
In ConfigModelRepository, I have this:
suspend fun getConfigFile(): ConfigModel {
val configString = prefs.getString(
ConfigViewModel.CONFIG_SHARED_PREF_KEY, "") ?: ""
return if (configString.isEmpty() || isCacheExpired()) {
runBlocking { fetchConfig() }
} else {
postFromLocalCache(configString)
}
}
private suspend fun fetchConfig(): ConfigModel {
return suspendCoroutine { cont ->
dataService
.config() // <-- LAST LINE CALLED
.enqueue(object : Callback<ConfigModel> {
override fun onResponse(call: Call<ConfigModel>, response: Response<ConfigModel>) {
if (response.isSuccessful) {
response.body()?.let {
saveConfigResponseInSharedPreferences(it)
cont.resume(it)
}
} else {
cont.resume(ConfigModel(listOf(), listOf()))
}
}
override fun onFailure(call: Call<ConfigModel>, t: Throwable) {
Timber.e(t, "config fetch failed")
cont.resume(ConfigModel(listOf(), listOf()))
}
})
}
}
My code runs as far as dataService.config(). It never enters onResponse or onFailure. The network call goes and and returns properly (I can see this using Charles), but the coroutine doesn't seem to be listening for the callback.
So, my question is the usual one. How can I get the coroutines to block such that they wait for this callback from Retrofit? Thanks.
The problem must be that response.body() returns null since that is the only case that is missing a call to cont.resume(). Make sure to call cont.resume() also in that case and your code should at least not get stuck.
But like CommonsWare points out, even better would be to upgrade to Retrofit 2.6.0 or later and use native suspend support instead of rolling your own suspendCoroutine logic.
You should also stop using runBlocking completely. In the first case, launch(Dispatchers.Main) a coroutine instead and move configModel.value = model inside of it. In the second case you can just remove runBlocking and call fetchConfig() directly.

Why coroutine's await function doesn't finish?

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
}

Kotlin Coroutine to escape callback hell

I'm trying to use Kotlin's coroutines to avoid callback hell, but it doesnt look like I can in this specific situation, I would like some thougths about it.
I have this SyncService class which calls series of different methods to send data to the server like the following:
SyncService calls Sync Student, which calls Student Repository, which calls DataSource that makes a server request sending the data through Apollo's Graphql Client.
The same pattern follows in each of my features:
SyncService -> Sync Feature -> Feature Repository -> DataSource
So every one of the method that I call has this signature:
fun save(onSuccess: ()-> Unit, onError:()->Unit) {
//To Stuff here
}
The problem is:
When I sync and successfully save the Student on server, I need to sync his enrollment, and if I successfully save the enrollment, I need to sync another object and so on.
It all depends on each other and I need to do it sequentially, that's why I was using callbacks.
But as you can imagine, the code result is not very friendly, and me and my team starting searching for alternatives to keep it better. And we ended up with this extension function:
suspend fun <T> ApolloCall<T>.execute() = suspendCoroutine<Response<T>> { cont ->
enqueue(object: ApolloCall.Callback<T>() {
override fun onResponse(response: Response<T>) {
cont.resume(response)
}
override fun onFailure(e: ApolloException) {
cont.resumeWithException(e)
}
})
}
But the function in DataSource still has a onSuccess() and onError() as callbacks that needs to be passed to whoever call it.
fun saveStudents(
students: List<StudentInput>,
onSuccess: () -> Unit,
onError: (errorMessage: String) -> Unit) {
runBlocking {
try {
val response = GraphQLClient.apolloInstance
.mutate(CreateStudentsMutation
.builder()
.students(students)
.build())
.execute()
if (!response.hasErrors())
onSuccess()
else
onError("Response has errors!")
} catch (e: ApolloException) {
e.printStackTrace()
onError("Server error occurred!")
}
}
}
The SyncService class code changed to be like:
private fun runSync(onComplete: () -> Unit) = async(CommonPool) {
val syncStudentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncStudents()
}
val syncEnrollmentProcess = async(coroutineContext, start = CoroutineStart.LAZY) {
syncEnrollments()
}
syncStudentProcess.await()
syncEnrollmentProcess.await()
onComplete()
}
It does execute it sequentially, but I need a way to stop every other coroutine if any got any errors. Error that might come only from Apollo's
So I've been trying a lot to find a way to simplify this code, but didn't get any good result. I don't even know if this chaining of callbacks can be simplify at all. That's why I came here to see some thoughts on it.
TLDR: I want a way to execute all of my functions sequentially, and still be able to stop all coroutines if any got an exception without a lot o chaining callbacks.

Categories

Resources