async coroutine with volley

Viewed 283

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

1 Answers

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