Adrian Pascu
Adrian Pascu

Reputation: 1039

Android download multiple files with OkHttp and coroutine

In my app, I get a set of urls to some images from an api and need to create Bitmap objects out of those urls to be able do display the images in the UI. I saw that the android docs recommend using corutines for performing such async tasks, but I am not sure how to do it properly.

Using OkHttp for my http client, I tried the following approach:

GlobalScope.launch {
                    val gson = Gson();
                    val parsedRes = gson.fromJson(
                        response.body?.charStream(),
                        Array<GoodreadsBook>::class.java
                    );
                    // Create the bitmap from the imageUrl
                    for (i in 0 until parsedRes.size) {
                        val bitmap =
                            GlobalScope.async { createBitmapFromUrl(parsedRes[i].best_book.image_url) }
                        parsedRes[i].best_book.imageBitmap = bitmap.await();
                    }
                   searchResults.postValue(parsedRes)
                }

Where response is what I get back from my API, and searchResults is a LiveData that hold the parsed response. Also, here is how I am getting the images from those urls:

suspend fun createBitmapFromUrl(url: String): Bitmap? {
    val client = OkHttpClient();
    val req = Request.Builder().url(url).build();
    val res = client.newCall(req).execute();
    return BitmapFactory.decodeStream(res.body?.byteStream())
}

Even though every fetch action is done on a separate coroutine, it's still too slow. Is there a better way of doing it? I can use any other http client if there is one out there optimized for use with coroutines, although I am new to Kotlin so I don't know any.

Upvotes: 4

Views: 4100

Answers (2)

Yuri Schimke
Yuri Schimke

Reputation: 13448

Use a library like the following that doesn't use the blocking execute method and instead bridges from the async enqueue.

https://github.com/gildor/kotlin-coroutines-okhttp

suspend fun main() {
    // Do call and await() for result from any suspend function
    val result = client.newCall(request).await()
    println("${result.code()}: ${result.message()}")
}

What this basically does is the following

public suspend fun Call.await(): Response {
    return suspendCancellableCoroutine { continuation ->
        enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                continuation.resume(response)
            }

            override fun onFailure(call: Call, e: IOException) {
                if (continuation.isCancelled) return
                continuation.resumeWithException(e)
            }
        })

        continuation.invokeOnCancellation {
            try {
                cancel()
            } catch (ex: Throwable) {
                //Ignore cancel exception
            }
        }
    }
}

Upvotes: 0

Animesh Sahu
Animesh Sahu

Reputation: 8096

First of all the createBitmapFromUrl(url: String) does everything synchronously, you've to first stop them from blocking the coroutine thread, you may want to use Dispatchers.IO for that because callback isn't the most idomatic thing ever in coroutines.

val client = OkHttpClient()  // preinitialize the client

suspend fun createBitmapFromUrl(url: String): Bitmap? = withContext(Dispatchers.IO) {
    val req = Request.Builder().url(url).build()
    val res = client.newCall(req).execute()
    BitmapFactory.decodeStream(res.body?.byteStream())
}

Now, when you are calling bitmap.await() you are simply saying that "Hey, wait for the deferred bitmap and once it is finished resume the loop for next iteration"

So you may want to do the assignment in the coroutine itself to stop it from suspending the loop, otherwise create another loop for that. I'd go for first option.

scope.launch {
    val gson = Gson();
    val parsedRes = gson.fromJson(
        response.body?.charStream(),
        Array<GoodreadsBook>::class.java
    );
    // Create the bitmap from the imageUrl
    for (i in 0 until parsedRes.size) {
        launch {
            parsedRes[i].best_book.imageBitmap = createBitmapFromUrl(parsedRes[i].best_book.image_url)
        }
    }
}

Upvotes: 4

Related Questions