0

I am attempting to build a library that allows an app to download a json file I provide, and then based on its contents, download images from the web. I have implemented it thus far with Kotlin Coroutines along with Ktor, but I have an issue which is evading my grasp of what to do.

This is the data class I am using to define each image:

  data class ListImage(val name: String, val url: String)

The user calls an init function which downloads the new json file. Once that file is downloaded, the app needs to download a number of images as defined by the file using getImages. Then a list is populated using the data class and an adapter.

Here is the code I am using to fetch the file:

fun init(context: Context, url: String): Boolean {
  return runBlocking {
    return@runBlocking fetchJsonData(context, url)
  }
}

private suspend fun fetchJsonData(context: Context, url: String): Boolean {
  return runBlocking {
    val client: HttpClient(OkHttp) {
      install(JsonFeature) {}
    }
    
    val data = async {
      client.get<String>(url)
    }


    try {
      val json = data.await()
      withContext(Dispatchers.IO) {
        context
          .openFileOutput("imageFile.json", Context.MODE_PRIVATE)
          .use { it.write(json.toByteArray()) }
      }
    } catch (e: Exception) {
      return@runBlocking false
    }

  }
}

This works and gets the file written locally. Then I have to get the images based on the contents of the file.

suspend fun getImages(context: Context) {
  val client = HttpClient(OkHttp)

  // Gets the image list from the json file
  val imageList = getImageList(context)

  for (image in imageList) {
    val imageName = image.name
    val imageUrl = image.url
                
    runBlocking {
      client.downloadFile(context, imageName, imageUrl)
            .collect { download ->
              if (download == Downloader.Success) {
                Log.e("SDK Image Downloader", "Successfully downloaded $imageName.")
              } else {
                Log.i("SDK Image Downloader", "Failed to download $imageName.")
              }
            }
    }
  }
}

private suspend fun HttpClient
            .downloadFile(context: Context, fileName: String, url: String): Flow<Downloader> {
        return flow {
            val response = this@downloadFile.request<HttpResponse> {
                url(url)
                method = HttpMethod.Get
            }
            val data = ByteArray(response.contentLength()!!.toInt())
            var offset = 0
            do {
                val currentRead = response.content.readAvailable(data, offset, data.size)
                offset += currentRead
            } while (currentRead > 0)
            if (response.status.isSuccess()) {
                withContext(Dispatchers.IO) {
                    val dataPath =
                        "${context.filesDir.absolutePath}${File.separator}${fileName}"
                    File(dataPath).writeBytes(data)
                }
                emit(Downloader.Success)
            } else {
                emit(Downloader.Error("Error downloading image $fileName"))
            }
        }
    }

If the file is already on the device and I am not attempting to redownload it, this also works. The issue is when I try to get the file and then the images in order when the app is first run. Here is an example of how I am trying to call it:

  lateinit var loaded: Deferred<Boolean>
  lateinit var imagesLoaded: Deferred<Unit>

  @InternalCoroutinesApi
  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
      setSupportActionBar(toolbar)

      val context: Context = this

      loaded = GlobalScope.async(Dispatchers.Default) {
          init(context)
      }
      GlobalScope.launch { loaded.await() }

      imagesLoaded = GlobalScope.async(Dispatchers.Default) {
          getDeviceImages(context)
      }
      GlobalScope.launch { imagesLoaded.await() }

      configureImageList(getImageList(context))
  }

  fun configureImageList(imageList: MutableList<Image>) {
        val imageListAdapter = ImageListAdapter(this, imageList) 
        with(image_list) {
            layoutManager = LinearLayoutManager(context)
            setHasFixedSize(true)
            itemAnimator = null
            adapter = imageListAdapter
        }
  }

This falls apart. So the two scenarios that play out are:

  1. I run this code as-is: The file is downloaded, and ~75% of the images are downloaded before the app crashes with a java.io.IOException: unexpected end of stream on the url. So it seems that the images are starting to download before the file is fully written.

  2. I run the app once without the image code. The file is downloaded. I comment out the file downloading code, and uncomment out the image downloading code. The images are downloaded, the app works as I want. This suggests to me that it would work if the first coroutine was actually finished before the second one started.

I have written and rewritten this code as many ways as I could think of, but I cannot get it to run without incident with both the file writing and image downloading completing successfully.

What am I doing wrong in trying to get these coroutines to complete consecutively?

smkarber
  • 577
  • 5
  • 18

1 Answers1

0

I just figured it out after stumbling upon this question. It appears as though my HttpClient objects were sharing a connection instead of creating new ones, and when the server closed the connection it caused in-flight operations to unexpectedly end. So the solution is:

val client = HttpClient(OkHttp) {
                defaultRequest {
                    header("Connection", "close")
                }
            }

I added this to each of the Ktor client calls and it now works as intended.

smkarber
  • 577
  • 5
  • 18