async coroutine with volley - kotlin

I am trying to convert my pet app to use coroutines instead of callbacks.
I am half way, but I cannot see how to get around the callback I have in this function. Is there a way to use async to get rid of callbacks or am I climbing the wrong tree?
This is what I have so far:
const val url = "https://pokeapi.co/api/v2/pokemon/"
class PokeClient {
fun getPokemonData(context: Context, successCallBack: (Pokemon) -> Unit, pokemonName: String) = runBlocking {
val queue = Volley.newRequestQueue(context)
val request = url.plus(pokemonName)
var deferredResult = async {
val stringRequest = StringRequest(Request.Method.GET, request, Response.Listener<String> { response ->
val jObj = JSONObject(response)
val imgUrl = jObj
.getJSONObject("sprites")
.getJSONObject("other")
.getJSONObject("official-artwork")
.getString("front_default")
val inputStream = URL(imgUrl).openStream()
successCallBack(Pokemon(name = jObj.getString("name"), image = BitmapFactory.decodeStream(inputStream)))
}, Response.ErrorListener {
val toast = Toast.makeText(context, "error talking to professor Oak!", Toast.LENGTH_SHORT)
toast.show()
})
queue.add(stringRequest)
}
deferredResult.await()
}
}
Any ideas?
Thank you,
Android Newbie

Essentially you need to convert network call with callback code block into a suspending function which can be called from any coroutine, this can be done using suspendCoroutine function, it basically provides you with a continuation object, which can be used to return data from inside the Response callbacks in your case
suspend fun getPokemon() = suspendCoroutine<Pokemon> { cont ->
val queue = Volley.newRequestQueue(this)
val url = "https://pokeapi.co/api/v2/pokemon/"
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<Pokemon> { response ->
val jObj = JSONObject(response)
val imgUrl = jObj.getJSONObject("sprites")
.getJSONObject("other")
.getJSONObject("official-artwork")
.getString("front_default")
val inputStream = URL(imgUrl).openStream()
/* call continuation.resume and pass your object */
cont.resume(Pokemon(name = jObj.getString("name"), image = BitmapFactory.decodeStream(inputStream)))
},
Response.ErrorListener {
/* if network call fails then post appropriate error */
cont.resumeWithException(YourExceptoin)
})
queue.add(stringRequest)
}
Now you can call this function from a coroutine and get a Pokemon as following
runBlocking{
try { val pokeMon = getPokemon() }
catch(e: Exception) { Log.d(TAG, "Cant get pokemon") }
}
Note : its OK to use runBlocking only for learning and exploration otherwise its not a good idea, use launch or async
Edit : As noted in comment you can also use suspendCancellableCoroutine if you need to support cancellation (which you should for structured concurrency).

Related

Wait for all volley request in a for loop

In my function, I need to return a list that is populated by a for loop with some Volley Request. So I need to wait that all of these requests to be terminated before return the list.
I think I need the async CoroutineScope to do this work but I don't know how can I wait for all of that response.
This is my code:
suspend fun getListOfAbility(pokemon: Pokemon) : MutableList<Ability> {
val listOfAbility: MutableList<Ability> = emptyList<Ability>() as MutableList<Ability>
CoroutineScope(Dispatchers.IO).launch {
/**
* get the pokemon json
*/
val pokemonJsonObjectRequest = JsonObjectRequest(
Request.Method.GET,
"$pokemonUrl${pokemon.id}",
null,
{
/**
* onResponse
*
* get the list of pokemon abilities
*/
val abilitiesJO = it.getJSONObject("abilities")
val abilityObjectType = object : TypeToken<List<PokemonGson.AbilityObjectGson>>() { }.type
val abilityListGson = Gson().fromJson<List<PokemonGson.AbilityObjectGson>>(abilitiesJO.toString(), abilityObjectType)
/**
* for each ability listed on pokemon info get the full Ability Object
*/
for((index, abilityObjectGson) in abilityListGson.withIndex()) {
val abilityJsonObjectRequest = JsonObjectRequest(
Request.Method.GET,
abilityObjectGson.ability.url,
null,
{
abilityJson ->
/**
* onResponse
*
* get the full ability info
*/
val abilityType = object : TypeToken<AbilityGson>() { }.type
val abilityGson = Gson().fromJson<AbilityGson>(abilityJson.toString(), abilityType)
/**
* fill the Ability entry of listOfAbility with the correct language
*/
val ability = Ability(abilityGson, abilityListGson[index].is_hidden)
listOfAbility.add(ability)
},
{
/**
* onError
*/
Log.d("POKEMON", "Pokemon ability error")
}
)
requestQueue.add(abilityJsonObjectRequest)
}
},
{
/**
* onError
*/
Log.d("POKEMON", "Pokemon request error")
}
)
requestQueue.add(pokemonJsonObjectRequest)
}
//wait
return listOfAbility
}
To use callback-based code in a suspend function, you need to convert it to a suspend function using suspendCoroutine or suspendCancellableCoroutine. So in this case to replace the action of creating a JSONObjectRequest and listener, queuing it to the RequestQueue, and waiting for it somehow, I would create a suspend function like this:
suspend inline fun RequestQueue.getJSONObjectOrNull(
method: Int,
url: String,
jsonRequest: JSONObject?,
crossinline onError: (VolleyError)->Unit = {}
): JSONObject? = suspendCancellableCoroutine { continuation ->
val request = JsonObjectRequest(
method,
url,
jsonRequest,
{ result: JSONObject -> continuation.resume(result) },
{ error ->
onError(error)
continuation.resume(null)
}
)
add(request)
continuation.invokeOnCancellation { request.cancel() }
}
It directly returns the JSONObject result, or null if there's a failure. You can optionally run a callback on errors in case you want to log it.
Then you can use it to write a more sequential version of your function instead of the callback-based version. You can use the pattern of coroutineScope { async { list.map { ... } } }.awaitAll() to convert each item of a list to something else using parallel coroutines.
Here is an untested version of your function. I am having it return an empty list on failure. You could alternatively return null on failure, which might be more useful so the calling function can decide to do something differently when there's a failure.
private fun VolleyError.logDebug() {
Log.d("POKEMON", "Pokemon request error: $this")
}
suspend fun getListOfAbility(pokemon: Pokemon): List<Ability> {
val pokemonJsonObject = requestQueue.getJSONObjectOrNull(Request.Method.GET, "$pokemonUrl${pokemon.id}", null, VolleyError::logDebug)
pokemonJsonObject ?: return emptyList()
val abilitiesJO = pokemonJsonObject.getJSONObject("abilities")
val abilityObjectType = object : TypeToken<List<PokemonGson.AbilityObjectGson>>() {}.type
val abilityListGson: List<Wrapper> = Gson().fromJson<List<PokemonGson.AbilityObjectGson>>(
abilitiesJO.toString(),
abilityObjectType
)
return coroutineScope {
abilityListGson.map {
async {
requestQueue.getJSONObjectOrNull(Request.Method.GET, it.ability.url, null, VolleyError::logDebug)
}
}
}
.awaitAll()
.filterNotNull()
.map { abilityJson ->
val abilityType = object : TypeToken<AbilityGson>() {}.type
val abilityGson = Gson().fromJson<AbilityGson>(abilityJson.toString(), abilityType)
Ability(abilityGson, abilityListGson[index].is_hidden)
}
}

rxjava, how to inspect the result of a Single

using kotlin, having code
fun fetchRemoteDataApi(): Single<RemoteDataResponse> = networkApi.getData()
// it is just a retrofit
#GET(".../api/getData")
fun getData() : Single<RemoteDataResponse>
fun mergeApiWithDb(): Completable = fetchRemoteDataApi()
.zipWith(localDao.getAll())
.flatMapCompletable { (remoteData, localData) ->
doMerge(remoteData, localData) //<== return a Completable
}
the code flow:
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache() //<=== would like do some inspection at this level
PublishSubject.create<Unit>().toFlowable(BackpressureStrategy.LATEST)
.compose(Transformers.flowableIO())
.switchMap {
//merge DB with api, or local default value first then listen to DB change
mergeApiDbCall.andThen(listAllTopics())
.concatMapSingle { topics -> remoteTopicUsers.map { topics to it } }
}
.flatMapCompletable { (topics, user) ->
// do something return Completable
}
.subscribe({
...
}, { throwable ->
...
})
and when making the call
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache()
the question is if would like to inspect on the Singles<RemoteDataResponse> returned from fetchRemoteDataApi() (i.e. using Log.i(...) to printout the content of RemoteDataResponse, etc.), either in got error or success case, how to do it?
/// the functions
fun listAllTopics(): Flowable<List<String>> = localRepoDao.getAllTopics()
// which a DAO:
#Query("SELECT topic FROM RemoteDataTable WHERE read = 1")
fun getAllTopics(): Flowable<List<String>>
///
private val remoteTopicUsers: Single<List<User>>
get() {
return Single.create {
networkApi.getTopicUsers(object : ICallback.IGetTopicUsersCallback {
override fun onSuccess(result: List<User>) = it.onSuccess(result)
override fun onError(errorCode: Int, errorMsg: String?) = it.onError(Exception(errorCode, errorMsg))
})
}
}
You cannot extract information about elements from the Completable. Though you can use doOnComplete() on Completable, it will not provide you any information about the element.
You can inspect elements if you call doOnSuccess() on your Single, so you need to incorporate this call earlier in your code. To inspect errors you can use doOnError() on both Completable or Single.

Crash in coroutine

My function is quite straightforward,
Main Thread: Initializes a variable ->
Background Thread: Fire network request, assign the result back to the previous variable ->
Main Thread: Display that variable
Code below:
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
lateinit var response: NewIdea
try {
withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
response = webservice.createIdea(tripId, map, "Bearer $token")
getTrip(context, token)
}
} catch (e: Exception) {
Log.e(TAG, e.message)
}
newCity.value = response
}
But sometimes (it only happened 2 times actually) crashlytics reports crash for this line newCity.value = response
Fatal Exception: kotlin.UninitializedPropertyAccessException: lateinit property response has not been initialized
I don't really understand how that can happen.
Is this the correct way to return value from coroutine function?
thanks
Well if try block fails, it might happen that the lateinit variable isn't set at all. You should put the ui update code inside the try block as well, and handle the Exception separately:
Sidenote: withContext is well-optimized to return values, so you can make use of it.
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
try {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
A quick tip (optional): You can wrap the UI updating code with a withContext that dispatches the work to Dispatchers.Main when not running in main thread, while if running in main do nothing:
withContext(Dispatchers.Main.immediate) {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
}

Can you get the response data from Volley outside of the stringRequest variable in Kotlin?

I want to get the Volley stringRequest response from my website outside of the variable.
val queue = Volley.newRequestQueue(this)
val url = ""
// Request a string response from the provided URL.
val stringRequest = StringRequest(
Request.Method.GET, url,
Response.Listener<String> { response ->
var obj = JSONObject(response) <-- cant access this variable outside of stringRequest
},
Response.ErrorListener { textView3.text = "That didn't work!" })
stringRequest.body.toString() <-- cant covert null to string
stringRequest.headers.toString() <-- output is {}
//here i want to do something like
if (response == "True") {
//do something
}
On the website that I'm accessing there is nothing more than {"check":"True"}
This implementation is asynchronous in the way its built in, What you actually can do to look it more like synchronous is if you use coroutines in your project, You can use suspendCoroutine, see
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines.experimental/suspend-coroutine.html
Example:
suspend fun getData(url: String) = suspendCoroutine<String?> { cont ->
val queue = Volley.newRequestQueue(this)
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<String> { response ->
cont.resume(response)
},
Response.ErrorListener { cont.resume(null) })
queue.add(stringRequest)
}
Now you can access the response string out of Response.Listener()
Edit: Additionally you can do cont.resumeWithException(e) instead if you dont want to return nullable expression and check nullability every time you use this function.

API call and coroutines issues

I have an issue with Kotlin and coroutines. I've written this method to call an api and get some information from this. I call calculateDistance, which works out the distances for me in KM. The issue that I have is that the UI loads before this method is completed. I've been trying to work out a way around this using coroutines, however, I seem to be coming stuck.
This function returned String is then used to render in an Activity.
Thanks
private fun getPostcodeLocation(listOfPostCodes: List<Pair<Boolean, String>>): String {
val jsonList = JSONObject()
jsonList.put("postcodes", JSONArray(listOfPostCodes.map { it.second}))
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonList.toString())
val request = okhttp3.Request.Builder().url("https://api.postcodes.io/postcodes" ).post(body).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body()?.string()
val jsonTree = JsonParser().parse(responseBody).asJsonObject
val resultJsonObj = jsonTree.asJsonObject.getAsJsonArray("result").asJsonArray.iterator()
resultJsonObj.forEach {
val resultObject = Gson().toJsonTree(it).asJsonObject.getAsJsonObject("result")
val lat = resultObject.get("latitude").asDouble
val lon = resultObject.get("longitude").asDouble
listOfLonLat.add(PostcodeLongLat(lon, lat))
}
distance = calculateDistance(listOfLonLat)
Log.d("PostCodeChecker", "This is the distance inside the callback $distance")
}
override fun onFailure(call: Call, e: IOException) {
println(call)
println(e)
Log.d("PostCodeChecker", "Failed to get the information from the API")
distance = "Not Available"
}
})
Log.d("PostCodeChecker", "This is the distance $distance")
return distance
}
Expected a string representing the distance between two long/lat points on a map, get Null.
this doesn't seem like a coroutines-related question. Your api call is happening asynchronously, hence you can't expect to get the result before returning distance. What you can do is access the UI element you need in onResponse method. E.g.
override fun onResponse(call: Call, response: Response) {
//parsing, calculations etc.
yourTextView.text = distance.toString
}