Download multiple images in a CoroutineScope

Viewed 1171

I need to download multiple images, and after all downloads are completed (outside of the Main Thread), perform other actions in the activity.

I am currently using Glide to download as follows:

ImageDownloader.kt

class ImageDownloader {
    fun downloadPack(context: Context, path: String, pack: PackModel) {
        for (image: ImageModel in pack.images) {
            Glide.with(context)
                .asBitmap()
                .load(image.imageFileUrl)
                .listener(object : RequestListener<Bitmap> {
                    override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap>?, isFirstResource: Boolean): Boolean {
                        return false
                    }

                    override fun onResourceReady(bitmap: Bitmap?, model: Any?, target: Target<Bitmap>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
                        saveImage(path, pack.id, image.imageFileName, bitmap!!)
                        return true
                    }
                }).submit()
        }
    }

    private fun saveImage(path: String, id: String, fileName: String, bitmap: Bitmap) {
        val dir = File(path + id)
        if (!dir.exists()) dir.mkdirs()
        val file = File(dir, fileName)
        val out = FileOutputStream(file)
        bitmap.compress(Bitmap.CompressFormat.PNG, 75, out)
        out.flush()
        out.close()
        // to check the download progress for each image in logcat
        println("done: $fileName")
    }
}

In the activity I call this method inside a CoroutineScope as follows:

PackActivity.kt

class PackActivity: AppCompatActivity() {
    private lateinit var bind: ActivityPackBinding
    private lateinit var path: String
    private lateinit var pack: PackModel
    // other basic codes

    override fun onCreate(savedInstanceState: Bundle?) {
        // other basic codes
        path = "$filesDir/images_asset/"
        pack = intent.getParcelableExtra(PACK_DATA)!!

        bind.buttonDownload.setOnClickListener {
            downloadPack()
        }
    }
    
    private fun downloadPack() {
        CoroutineScope(Dispatchers.IO).launch {
            val async = async {
                ImageDownloader().downloadPack(applicationContext, path, pack)
            }
            val result = async.await()
            withContext(Dispatchers.Main) {
                result.apply {
                    println("finished")
                    // other things todo
                }
            }
        }
    }
}

I am trying to proceed with other actions after downloading all the images in PackActivity.kt, but as a result, using println("finished") and checking the logcat, the code is starting even before the first download starts...

Some information:

PackModel and ImageModel are my data class, where PackModel has the Id of each pack and a list of ImageModel, which in turn has the ImageFileName and ImageFileUrl. All data is obtained from a web request.

I want the images to be saved in the folder data/data/AppPackageName/files/images_asset/PackID/... And with the tests I did, I was unable to use DownloadManager directing the images to this internal folder of the App, that's why I'm using Glide.

1 Answers

The first, crucial step is to adapt the async Glide request into a suspend fun using suspendCancellableCoroutine. Here's how:

private suspend fun downloadBitmap(
    context: Context,
    image: ImageModel
) = suspendCancellableCoroutine<Bitmap> { cont ->
    Glide.with(context)
        .asBitmap()
        .load(image.imageFileUrl)
        .listener(object : RequestListener<Bitmap> {
            override fun onLoadFailed(
                e: GlideException, model: Any?,
                target: Target<Bitmap>?, isFirstResource: Boolean
            ): Boolean {
                cont.resumeWith(Result.failure(e))
                return false
            }

            override fun onResourceReady(
                bitmap: Bitmap, model: Any?, target: Target<Bitmap>?,
                dataSource: DataSource?, isFirstResource: Boolean
            ): Boolean {
                cont.resumeWith(Result.success(bitmap))
                return false
            }
        }).submit()
}

With that done, now you'll have an easy time of making concurrent downloads and awaiting on all of them:

class ImageDownloader {
    suspend fun downloadPack(context: Context, path: String, pack: PackModel) {
        coroutineScope {
            for (image: ImageModel in pack.images) {
                launch {
                    val bitmap = downloadBitmap(context, image)
                    saveImage(path, pack.id, image.imageFileName, bitmap)
                }
            }
        }
    }


    private suspend fun saveImage(
        path: String, id: String, imageFileName: String, bitmap: Bitmap
    ) {
        withContext(IO) {
            // your code
        }
    }
}

Watch carefully which dispatchers I use above: using the Main dispatcher for everything except saveImage, which is the only place that contains code that is actually blocking.

Finally, to use everything, this is all you need:

private fun downloadPack() {
    GlobalScope.launch {
        ImageDownloader().downloadPack(applicationContext, path, pack)
        println("finished")
        // other things todo
    }
}

Again, everything on the Main dispatcher because the blocking code is safely corralled into the IO dispatcher.

I use GlobalScope above for the lack of knowledge of your larger context, but it's probably a bad idea. Writing CoroutineScope(IO).launch has all the same problems, plus allocating several more objects.

Think twice about what's going to happen to your downloads if the user navigates away from the app, or if they navigate away and back repeatedly, triggering a growing pile of background downloads. In the code above, i didn't treat cancellation within suspendCancellableCoroutine because I'm not that intimate with Glide. You should add a cont.onCancellation handler to be correct.

Related