Kotlin Coroutines: Issue with job-scheduling.(invokeOnCompletion) - android

I am fairly new to this kotlin-coroutine thing and i have an issue with job-scheduling.In this code below, first i fetch topic names from user's cache in the fragment.(topicsList)
And then, i need to fetch these topics from API one by one. What i want to do is loop through the topicsList, make a request for each topic and get all the responses once at the completion of all requests. In order to achieve that, in getEverything() method(which fires up a request), i am adding the responses into an arraylist for every time.(responseList)
In for loop, i am firing up all the requests. After the completion of the job, job.invokeOnCompletion{} is called and i set my liveData to responseList. However, this approach doesn't work. Problem is, i am updating the liveData before the setting the responseList. I don't know how can it be possible. Could anybody help me about this?
Here is my CoroutineScope in myFragment:
val topicsList = dataMap["topics"] // GOT THE TOPICS
topicsList?.let {
var job: Job
CoroutineScope(Dispatchers.Main).launch {
job = launch {
for (topic in topicsList) {
mViewModel.getEverything(topic, API_KEY)
}
}
job.join()
job.invokeOnCompletion {
mViewModel.updateLiveData()
}
}
} ?: throw Exception("NULL")
getEverything() method in viewModel:
suspend fun getEverything(topic: String, apiKey: String) {
viewModelScope.launch {
_isLoading.value = true
withContext(Dispatchers.IO) {
val response = api.getEverything(topic, apiKey)
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
if (response.body() != null) {
responseList.add(response.body()!!)
println("Response is successful: ${response.body()!!}")
_isLoading.value = false
_isError.value = false
}
}
else {
Log.d(TAG, "getEverything: ${response.errorBody()}")
_isError.value = true
_isLoading.value = false
}
}
}
}
}
And, updateLiveData method:
fun updateLiveData() {
_newsResponseList.value = responseList
println("response list : ${responseList.size}")
responseList.clear()
}
And this is how it looks in the logs: Logs
Logs for you who cannot open the image :
I/System.out: response list : 0
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=techcrunch, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=wired, ...
I/System.out: Response is successful: NewsResponse(articleList=[Article(source=Source(id=the-verge, ...
Btw data is fetched without an error and its correct. I've no issue with that.

The issue is that getEverything uses launch to create a background job, then returns before it knows the job is complete.
To fix this, have getEverything return the data directly:
suspend fun getEverything(topic: String, apiKey: String): Response? {
_isLoading.value = true
val response = withContext(Dispatchers.IO) {
api.getEverything(topic, apiKey)
}
_isLoading.value = false
return response.takeIf { it.isSuccessful }?.body()?.let { body ->
println("Response is successful: $body")
}.also {
_isError.value = it == null
}
}
In your Fragment, request the results and assign them:
lifecycleScope.launch {
_responseList.value = topicsList.mapNotNull { topic ->
model.getResponse(topic, apiKey)
}
}

Related

My patch request response me 500 Internal Server Error

My ViewModel function
patchProfileEmailAddress fuction wants the emailAddress variable
'
private fun saveUserEmailChanges(email: String?) {
profileRepository.patchProfileEmailAddress(emailAddress)
.onEach {
when (it) {
is Result.Success -> {
setLoading(false)
emailAddress = email
updateActionState(
MyProfilePersonInformationASMActionState.DismissBottomSheet)
updateActionState(MyProfilePersonInformationASMActionState.OnSuccess)}
is Result.Error -> {
setLoading(false)
updateActionState(
MyProfilePersonInformationASMActionState
.ShowErrorMessage(it.errorResponse?.message))}
is Result.Loading -> setLoading(true)} }
.launchIn(viewModelScope)}'
My Fragment part
'
var usersNewMail : String? =null
private fun setOnClickListeners() {
binding.apply {
adressArrowImageView.setOnClickListener{ openBodyBottomSheet() }
mailArrowImageView.setOnClickListener{ clickMailArrowImageView() }
checkOkeyImageView.setOnClickListener{ clickOkeyCheckImageView() }}}
private fun getMailChange(){
viewModel.saveUserEmailChanges(usersNewMail)
}
private fun clickMailArrowImageView(){
binding.apply {
txtEditMail.isEnabled = true
checkOkeyImageView.isVisible = true
mailArrowImageView.isVisible = false
}
}
private fun clickOkeyCheckImageView(){
binding.apply {
txtEditMail.isEnabled = false
checkOkeyImageView.isVisible = false
mailArrowImageView.isVisible = true
usersNewMail = txtEditMail.text.toString()
getMailChange()
}
}'
Postman works fine. In application patch response 500 Internal Server Error. My API wants string and I'm giving string.
It's certain you are sending something wrong if it works in Postman, so the first you have to do in any case is to know what you are sending; which can be done in various ways.
For example, if you are using OkHttp then have an interceptor for logging.
This way you can tell in LogCat what's going on.
After finding out what you are sending, if you still need help just update your question and I'll update my answer.

How to Wait response from Server in forEach with Coroutines

I recently started working with coroutines.
The task is that I need to check the priority parameter from the List and make a request to the server, if the response from the server is OK, then stop the loop.
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
makeRequest(model.value)
minPriority = model.priority
}
}
private fun makeRequest(value: String) {
scope.launch() {
val response = restApi.makeRequest()
if response.equals("OK") {
**stop list foreach()**
}
}
}
In RxJava, this was done using the retryWhen() operator, tell me how to implement this in Coroutines?
I suggest making your whole code suspendable, not only the body of makeRequest() function. This way you can run the whole operation in the background, but internally it will be sequential which is easier to code and maintain.
It could be something like this:
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
val response = restApi.makeRequest()
if response.equals("OK") {
return#forEach
}
minPriority = model.priority
}
}
}
Of if you need to keep your makeRequest() function separate:
fun myFunction() {
scope.launch() {
var minPriority = 0
list.forEach { model ->
if (model.priority > minPriority) {
if (makeRequest(model.value)) {
return#forEach
}
minPriority = model.priority
}
}
}
}
private suspend fun makeRequest(value: String): Boolean {
val response = restApi.makeRequest()
return response.equals("OK")
}

Multiple Retrofit calls with Flow

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

Using Coroutine runblock with the Authenticator to handle 401 response from retrofit

I am trying to use the Authenticator to handle 401 response. What I have done is
fun provideAccessTokenAuthenticator(
mainApiServiceHolder: MainApiServiceHolder,
preferences: SharedPreferences
) = object : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val accessToken = preferences.getString(ACCESS_TOKEN, null)
if (!isRequestWithAccessToken(response) || accessToken == null) {
return null
}
synchronized(this) {
val newAccessToken = preferences.getString(ACCESS_TOKEN, null)!!
// Access token is refreshed in another thread.
if (accessToken != newAccessToken) {
return newRequestWithAccessToken(response.request, newAccessToken)
}
// Need to refresh an access token
val refreshTokenResponse = runBlocking {
Log.d("zzzzzzzzzz", "refresh token is running")
mainApiServiceHolder.mainApiService?.refreshToken(
"refresh_token",
preferences.getString(REFRESH_TOKEN, null)!!,
AuthRepository.CLIENT_ID,
AuthRepository.CLIENT_SECRET
)
}
Log.d("zzzzzzzzzz", refreshTokenResponse?.body()?.access_token!!)
return if (refreshTokenResponse?.isSuccessful!!) {
Log.d("zzzzzzzzzz", "refresh token is successful")
newRequestWithAccessToken(
response.request,
refreshTokenResponse.body()?.access_token!!
)
} else {
Log.d("zzzzzzzzzz", "refresh token is unsuccessful")
response.request.newBuilder().header("Content-Type", "application/json").build()
}
}
}
Now, it gets called when there is a 401 response. The refresh token call is also fired (from Log). However, it never gets the result in the refreshTokenResponse and nothing happens after that. I think its a wrong way of using runBlock. The api is
#FormUrlEncoded
#POST("/api/auth/token/")
suspend fun refreshToken(
#Field("grant_type") grant_type: String,
#Field("refresh_token") refresh_token: String,
#Field("client_id") client_id: String,
#Field("client_secret") client_secret: String
): Response<LoginResponse>
Any help would be really appreciated. Thanks
In the Retrofit API, consider replacing your async runBlocking{} suspend fun with a synchronous Call. I had the most luck avoiding the use of coroutines inside the Authenticator.
I was having the same problem. The token request went straight into a black hole. The app froze. The request was never seen again. No error, no nothing.
But everywhere else in the app, the suspend fun came back just fine. From ViewModels, from WorkManager, it worked every time. But from the Authenticator, never. What was wrong with the Authenticator? What was special about the Authenticator that made it act this way?
Then I replaced the runBlocking{} coroutine with a straightforward Call. This time, the request came back and the token arrived without a fuss.
The way I got the API to work looked like this:
#FormUrlEncoded
#POST("token")
fun refreshTokenSync(
#Field("refresh_token") refreshToken: String,
): Call<RefreshMyTokenResponse>
Then, in the Authenticator:
val call = API.refreshTokenSync(refreshToken)
val response = call.execute().body()
I hope this helps someone else who ran into the same issue. You may receive a warning from Android Studio that this is an inappropriate blocking call. Ignore it.
Refresh token only once for multiple requests
Log out user if refreshToken failed
Log out if user gets an error after first refreshing
Queue all requests while token is being refreshed
https://github.com/hoc081098/Refresh-Token-Sample/blob/master/app/src/main/java/com/hoc081098/refreshtokensample/data/remote/interceptor/AuthInterceptor.kt
class AuthInterceptor #Inject constructor(
private val userLocalSource: UserLocalSource,
private val apiService: Provider<ApiService>,
) : Interceptor {
private val mutex = Mutex()
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request().also { Timber.d("[1] $it") }
if (NO_AUTH in req.headers.values(CUSTOM_HEADER)) {
return chain.proceedWithToken(req, null)
}
val token =
runBlocking { userLocalSource.user().first() }?.token.also { Timber.d("[2] $req $it") }
val res = chain.proceedWithToken(req, token)
if (res.code != HTTP_UNAUTHORIZED || token == null) {
return res
}
Timber.d("[3] $req")
val newToken: String? = runBlocking {
mutex.withLock {
val user =
userLocalSource.user().first().also { Timber.d("[4] $req $it") }
val maybeUpdatedToken = user?.token
when {
user == null || maybeUpdatedToken == null -> null.also { Timber.d("[5-1] $req") } // already logged out!
maybeUpdatedToken != token -> maybeUpdatedToken.also { Timber.d("[5-2] $req") } // refreshed by another request
else -> {
Timber.d("[5-3] $req")
val refreshTokenRes =
apiService.get().refreshToken(RefreshTokenBody(user.refreshToken, user.username))
.also {
Timber.d("[6] $req $it")
}
val code = refreshTokenRes.code()
if (code == HTTP_OK) {
refreshTokenRes.body()?.token?.also {
Timber.d("[7-1] $req")
userLocalSource.save(
user.toBuilder()
.setToken(it)
.build()
)
}
} else if (code == HTTP_UNAUTHORIZED) {
Timber.d("[7-2] $req")
userLocalSource.save(null)
null
} else {
Timber.d("[7-3] $req")
null
}
}
}
}
}
return if (newToken !== null) chain.proceedWithToken(req, newToken) else res
}
private fun Interceptor.Chain.proceedWithToken(req: Request, token: String?): Response =
req.newBuilder()
.apply {
if (token !== null) {
addHeader("Authorization", "Bearer $token")
}
}
.removeHeader(CUSTOM_HEADER)
.build()
.let(::proceed)
}

Kotlin Coroutine Retrofit - Chain network calls

I'm trying to use Kotlin Coroutines + Retrofit to make my network calls, but my current implementation has two problems.
A) It only returns once my loop has completed.
B) it seems to wait for each call in my loop to complete before making the next one.
The API I'm interacting with requires me to make an initial fetch, returning an array of itemId's
[ 1234, 3456, 3456 ... ]
and for each item in the above response, fetch that item with id
{ id: 1234, "name": "banana" ... }
My current implementation is as follows, what am I doing wrong?
suspend operator fun invoke(feedType: String): NetworkResult<List<MyItem>> = withContext(Dispatchers.IO) {
val itemList: MutableList< MyItem > = mutableListOf()
val result = repository.fetchItems()
when (result) {
is NetworkResult.Success -> {
itemList.addAll(result.data)
for (i in itemList) {
val emptyItem = result.data[i]
val response = repository.fetchItem(emptyItem.id)
when (response) {
is NetworkResult.Success -> {
val item = response.data
emptyItem.setProperties(item)
}
}
}
}
is NetworkResult.Error -> return#withContext result
}
return#withContext NetworkResult.Success(itemList)
}
I would like to propose you to use async to process every item separately:
suspend operator fun invoke(feedType: String): NetworkResult<List<MyItem>> = withContext(Dispatchers.IO) {
when (val result = repository.fetchItems()) { // 1
is NetworkResult.Success -> {
result.data
.map { async { fetchItemData(it) } } // 2
.awaitAll() // 3
NetworkResult.Success(result.data)
}
is NetworkResult.Error -> result
}
}
private suspend fun fetchItemData(item: MyItem) {
val response = repository.fetchItem(item.id)
if (response is NetworkResult.Success) {
item.setProperties(response.data)
}
}
In this code, at first, we make a call to fetchItems to get the items ids (1). Then we make a call to fetchItem for every item at the same time (2). It can be easily done with coroutines and async. Then we wait until all data will be fetched (3).

Categories

Resources