使用 Retrofit 和 Kotlin 协程从 url 下载文件

Download file from url using Retrofit and Kotlin coroutines

我有以下方法从 url 下载 pdf 文件。

我的ApiService.kt

interface ApiService {

    
    @GET("8d370189-4ec8-11ec-8469-005056ae4067/Aktionsprospekt-06-12-2021-11-12-2021-06.pdf?_ga=2.119177993.1645728632.1638630355-145806302.1638630355")
    suspend fun getData(): Response<ResponseBody>
}

我的ApiClient.kt

object ApiClient {
    fun getClient(): ApiService {
        return Retrofit.Builder().baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create()).build()
            .create(ApiService::class.java)
    }
}

在 ViewModel 中,我调用了协程作用域,如下所示

class MainActivityViewModel(private val apiService: ApiService, private val context: Context) : ViewModel() {

    fun downloadFile() = viewModelScope.launch {
        val responseBody = apiService.getData().body()
        saveFile(responseBody)
    }

    private fun saveFile(body: ResponseBody?) : String {
        if (body == null) {
            return ""
        }

        var input: InputStream? = null
        try{
            input = body.byteStream()
            val path = context.getExternalFilesDir(null)!!.path
            val fos = FileOutputStream(path)
            fos.use { output ->
                val buffer = ByteArray(4 * 1024) // or other buffer size
                var read: Int
                while (input.read(buffer).also { read = it } != -1) {
                    output.write(buffer, 0, read)
                }
                output.flush()
            }
            return path
        } catch(e: Exception) {
            Log.e("saveFile",e.toString())
        }
        finally {
            input?.close()
        }
        return ""
    }

}

在 Main Activity 中,我调用了 downloadFile() 的上述函数,如下所示:

val apiService: ApiService = ApiClient.getClient()
        viewModel = getViewModel(apiService)
        downloadBtn.setOnClickListener {
            viewModel.downloadFile()
        }

我在 saveFile() 函数中遇到以下错误

2021-12-05 02:50:04.023 7027-7027/com.mms.compareandchoose E/saveFile: java.io.FileNotFoundException: /storage/emulated/0/Android/data/com.mms.compareandchoose/files (Is a directory)
2021-12-05 02:50:04.055 7027-7027/com.mms.compareandchoose E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.mms.compareandchoose, PID: 7027
    android.os.NetworkOnMainThreadException
        at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1303)
        at com.android.org.conscrypt.Platform.blockGuardOnNetwork(Platform.java:300)
        at com.android.org.conscrypt.OpenSSLSocketImpl$SSLOutputStream.write(OpenSSLSocketImpl.java:839)
        at okio.OutputStreamSink.write(JvmOkio.kt:53)
        at okio.AsyncTimeout$sink.write(AsyncTimeout.kt:103)
        at okio.RealBufferedSink.flush(RealBufferedSink.kt:247)
        at okhttp3.internal.http2.Http2Writer.rstStream(Http2Writer.kt:135)
        at okhttp3.internal.http2.Http2Connection.writeSynReset$okhttp(Http2Connection.kt:354)
        at okhttp3.internal.http2.Http2Stream.close(Http2Stream.kt:240)
        at okhttp3.internal.http2.Http2Stream.cancelStreamIfNecessary$okhttp(Http2Stream.kt:506)
        at okhttp3.internal.http2.Http2Stream$FramingSource.close(Http2Stream.kt:488)
        at okio.ForwardingSource.close(ForwardingSource.kt:34)
        at okhttp3.internal.connection.Exchange$ResponseBodySource.close(Exchange.kt:309)
        at okio.RealBufferedSource.close(RealBufferedSource.kt:498)
        at okio.ForwardingSource.close(ForwardingSource.kt:34)
        at okio.RealBufferedSource.close(RealBufferedSource.kt:498)
        at okio.RealBufferedSource$inputStream.close(RealBufferedSource.kt:170)
        at com.mms.compareandchoose.models.MainActivityViewModel.saveFile(MainActivityViewModel.kt:46)
        at com.mms.compareandchoose.models.MainActivityViewModel$downloadFile.invokeSuspend(MainActivityViewModel.kt:20)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6776)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1518)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1408)

有人能说说为什么会出现这个错误吗??还请指教这种做法好不好??

您在调用 input.read(buffer) 时从远程流读取数据。但是 viewModelScope 默认情况下在主线程上启动代码。要修复异常,您应该明确指定在另一个 (IO) 线程上执行下载。

fun downloadFile() = viewModelScope.launch(Dispatchers.IO) {
        val responseBody = apiService.getData().body()
        saveFile(responseBody)
}