如何确保Kotlin协程以正确的顺序完成?

问题描述

我正在尝试建立一个允许应用程序下载我提供的json文件的库,然后根据其内容从网络下载图像。到目前为止,我已经与Kotlin Coroutines以及Ktor一起实现了该功能,但是我遇到的一个问题是我无法理解该做什么。

这是我用来定义每个图像的数据类:

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

用户调用init函数,该函数将下载新的json文件。下载该文件后,应用程序需要使用getimages下载该文件定义的许多图像。然后使用数据类和适配器填充列表。

这是我用来提取文件代码

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
    }

  }
}

这有效并获取本地写入的文件。然后,我必须根据文件内容获取图像。

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 = [email protected]<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"))
            }
        }
    }

如果该文件已经在设备上,并且我没有尝试重新下载它,那么它也可以工作。问题是当我第一次运行该应用程序时,我尝试先获取文件,然后获取图像。这是一个我如何称呼它的例子:

  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
        }
  }

这崩溃了。因此,出现的两种情况是:

  1. 我按原样运行此代码:在应用程序因java.io.IOException: unexpected end of stream on the url崩溃之前,已下载文件,并下载了约75%的图像。因此,似乎在文件完全写入之前就开始下载图像。

  2. 我在没有图像代码的情况下运行了一次应用程序。已下载文件。我注释掉了文件下载代码,而取消注释了图像下载代码。图像已下载,该应用程序可以根据需要运行。这向我表明,如果第一个协程实际上在第二个协程开始之前就完成了,那就行得通。

我已经用尽可能多的方式编写和重写了此代码,但是我不能在成功完成文件写入和图像下载的过程中使它运行。

试图使这些协程连续完成,我在做错什么?

解决方法

在绊倒这个question之后,我才弄清楚了。看来我的HttpClient对象正在共享一个连接而不是创建一个新的对象,并且当服务器关闭该连接时,它导致运行中的操作意外结束。因此解决方案是:

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

我将此添加到了每个Ktor客户端调用中,现在可以正常使用了。