How to download PDF file with Retrofit and Kotlin coroutines?

后端 未结 3 775
长发绾君心
长发绾君心 2020-12-21 02:23

I saw topics like How to download file in Android using Retrofit library?, they use @Streaming and RxJava / callbacks.

I have Kotlin, coroutines, Retrof

相关标签:
3条回答
  • 2020-12-21 02:40

    Thanks to @AndreiTanana I found a mistake. A problem was in suspend in the request definition. All other requests retain their suspend modifier, but this request removed it. I changed the code so.

    interface Api {
        @FormUrlEncoded
        @Streaming
        @POST("export-pdf/")
        fun exportPdf(
            @Field("token") token: String
        ): Call<ResponseBody>
    
        // Any another request. Note 'suspend' here.
        @FormUrlEncoded
        @POST("reject/")
        suspend fun reject(): RejectResponse
    }
    

    Then in it's implementation, ApiImpl:

    class ApiImpl : Api {
    
        private val retrofit by lazy { ApiClient.getRetrofit().create(Api::class.java) }
    
        override fun exportPdf(
            token: String
        ): Call<ResponseBody> =
            retrofit.exportPdf(token)
    
        override suspend fun reject(): RejectResponse =
            // Here can be another instance of Retrofit.
            retrofit.reject()
    }
    

    Retrofit client:

    class ApiClient {
    
        companion object {
    
            private val retrofit: Retrofit
    
    
            init {
    
                val okHttpClient = OkHttpClient().newBuilder()
                    .connectTimeout(60, TimeUnit.SECONDS)
                    .readTimeout(60, TimeUnit.SECONDS)
                    .writeTimeout(60, TimeUnit.SECONDS)
                    .build()
    
                val gson = GsonBuilder().setLenient().create()
    
                retrofit = Retrofit.Builder()
                    .baseUrl(SERVER_URL)
                    .client(okHttpClient)
                    // .addConverterFactory(GsonConverterFactory.create(gson)) - you can add this line, I think.
                    .build()
            }
    
            fun getRetrofit(): Retrofit = retrofit
    }
    

    Interactor:

    interface Interactor {
        // Note 'suspend' here. This is for coroutine chain.
        suspend fun exportPdf(
            token: String
        ): Call<ResponseBody>
    }
    
    class InteractorImpl(private val api: Api) : Interactor {
        override suspend fun exportPdf(
            token: String
        ): Call<ResponseBody> =
            api.exportPdf(token)
    }
    

    Then in fragment:

    private fun exportPdf(view: View, token: String) {
        showProgress(view)
        launch(Dispatchers.IO) {
            try {
                val response = interactor.exportPdf(token).execute()
                var error: String? = null
                if (response.headers().get("Content-Type")?.contains(
                        "application/json") == true) {
                    // Received JSON with an error.
                    val json: String? = response.body()?.string()
                    error = json?.let {
                        val export = ApiClient.getGson().fromJson(json,
                            ExportPdfResponse::class.java)
                        export.errors?.common?.firstOrNull()
                    } ?: getString(R.string.request_error)
                } else {
                    // Received PDF.
                    val buffer = response.body()?.byteStream()
                    if (buffer != null) {
                        val file = context?.let { createFile(it, "pdf") }
                        if (file != null) {
                            copyStreamToFile(buffer, file)
                            launch(Dispatchers.Main) {
                                if (isAdded) {
                                    hideProgress(view)
                                }
                            }
                        }
                    }
                }
                if (error != null) {
                    launch(Dispatchers.Main) {
                        if (isAdded) {
                            hideProgress(view)
                            showErrorDialog(error)
                        }
                    }
                }
            } catch (e: Exception) {
                launch(Dispatchers.Main) {
                    if (isAdded) {
                        showErrorDialog(getString(R.string.connection_timeout))
                        hideProgress(view)
                    }
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-21 02:46

    You can change the return type of exportPdf to Call<ResponseBody> and then check the response code. If it's ok then read the body as a stream. If it's not then try to deserialize ExportResponse. It will look something like this I guess:

    val response = restAdapter.apiRequest().execute()
    if (response.isSuccessful) {
        response.body()?.byteStream()//do something with stream
    } else {
        response.errorBody()?.string()//try to deserialize json from string
    }
    

    Update

    Here is a complete listing of my test:

    import okhttp3.HttpUrl
    import okhttp3.OkHttpClient
    import okhttp3.ResponseBody
    import retrofit2.Call
    import retrofit2.Retrofit
    import retrofit2.http.GET
    import retrofit2.http.Url
    import java.io.File
    import java.io.InputStream
    
    fun main() {
        val queries = buildQueries()
        check(queries, "http://127.0.0.1:5000/error")
        check(queries, "http://127.0.0.1:5000/pdf")
    }
    
    private fun check(queries: Queries, url: String) {
        val response = queries.exportPdf(HttpUrl.get(url)).execute()
        if (response.isSuccessful) {
            response.body()?.byteStream()?.saveToFile("${System.currentTimeMillis()}.pdf")
        } else {
            println(response.errorBody()?.string())
        }
    }
    
    private fun InputStream.saveToFile(file: String) = use { input ->
        File(file).outputStream().use { output ->
            input.copyTo(output)
        }
    }
    
    private fun buildRetrofit() = Retrofit.Builder()
        .baseUrl("http://127.0.0.1:5000/")
        .client(OkHttpClient())
        .build()
    
    private fun buildQueries() = buildRetrofit().create(Queries::class.java)
    
    interface Queries {
        @GET
        fun exportPdf(@Url url: HttpUrl): Call<ResponseBody>
    }
    

    and here is simple sever built with Flask:

    from flask import Flask, jsonify, send_file
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello():
        return 'Hello, World!'
    
    
    @app.route('/error')
    def error():
        response = jsonify(error=(dict(body='some error')))
        response.status_code = 400
        return response
    
    
    @app.route('/pdf')
    def pdf():
        return send_file('pdf-test.pdf')
    

    all works fine for me

    Update 2

    Looks like you have to write this in your Api:

    @FormUrlEncoded
    @Streaming // You can also comment this line.
    @POST("export-pdf/")
    fun exportPdf(
        @Field("token") token: String
    ): Call<ResponseBody>
    
    0 讨论(0)
  • 2020-12-21 02:48

    If you are looking for usual file downloading with coroutines and GET-request.

    See also How to download file in Android using Retrofit library?.

    class ApiClient {
    
        companion object {
    
            private val gson: Gson
            private val retrofit: Retrofit
    
            init {
    
                val okHttpClient = OkHttpClient().newBuilder()
                    .connectTimeout(60, TimeUnit.SECONDS) // You can remove timeouts.
                    .readTimeout(60, TimeUnit.SECONDS)
                    .writeTimeout(60, TimeUnit.SECONDS)
                    // Warning! It shouldn't be any interceptor changing a response here.
                    // If you have some, you will get a wrong binary file.
                    .build()
    
                gson = GsonBuilder().setLenient().create()
    
                retrofit = Retrofit.Builder()
                    .baseUrl(ApiConst.SERVER_URL)
                    .client(okHttpClientWithoutConversion)
                    // Optionally add JSON converter factory.
                    .addConverterFactory(GsonConverterFactory.create(gson))
                    .build()
            }
    
            fun getGson(): Gson = gson
    
            fun getRetrofit(): Retrofit = retrofit
    }
    

    API method:

    interface Api {
    
        @Streaming
        @GET
        fun downloadFile(@Url fileUrl: String): Call<ResponseBody>
    }
    
    class ApiImpl : Api {
    
        private val service = ApiClient.getRetrofit().create(Api::class.java)
    
        override fun downloadFile(fileUrl: String): Call<ResponseBody> =
            service.downloadFile(fileUrl)
    }
    

    Fragment:

    private lateinit var interactor: Api
    
    interactor = ApiImpl()
    
    private fun openPdf(view: View, fileName: String, url: String) {
        job = launch {
            try {
                val file = withContext(Dispatchers.IO) {
                    val response = interactor.downloadFile(url).execute()
                    val buffer = response.body()?.byteStream()
                    var file: File? = null
                    if (buffer != null) {
                        file = context?.let { createFile(it, fileName, "pdf") }
                        if (file != null) {
                            copyStreamToFile(buffer, file)
                        }
                    }
                    file
                }
                if (isAdded) {
                    if (file == null) {
                        // show error.
                    } else {
                        // sharePdf(file, context!!)
                    }
                }
            } catch (e: Exception) {
                if (isAdded) {
                    // show error.
                }
            }
        }
    }
    
    private fun createFile(context: Context, fileName: String, fileExt: String): File? {
        val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.path
        var file = File("$storageDir/$fileName.$fileExt")
        return storageDir?.let { file }
    }
    
    private fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
        inputStream.use { input ->
            val outputStream = FileOutputStream(outputFile)
            outputStream.use { output ->
                val buffer = ByteArray(4 * 1024)
                while (true) {
                    val byteCount = input.read(buffer)
                    if (byteCount < 0) break
                    output.write(buffer, 0, byteCount)
                }
                output.flush()
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题