如何使用Retrofit和Kotlin协程下载PDF文件?

时间:2019-07-10 16:21:56

标签: android kotlin retrofit2 kotlin-coroutines

我看到了诸如How to download file in Android using Retrofit library?之类的主题,它们使用@Streaming和RxJava /回调。

我有Kotlin,协程,翻新版2.6.0和类似https://stackoverflow.com/a/56473934/2914140的查询:

@FormUrlEncoded
@Streaming
@POST("export-pdf/")
suspend fun exportPdf(
    @Field("token") token: String
): ExportResponse

我有一个改造客户:

retrofit = Retrofit.Builder()
    .baseUrl(SERVER_URL)
    .client(okHttpClient)
    .build()

service = retrofit.create(Api::class.java)

如果令牌参数正确,查询将返回PDF文件:

%PDF-1.4
%����
...

如果错误,它将返回带有错误描述的JSON:

{
    "success": 0,
    "errors": {
        "message": "..."
    }
}

因此,ExportResponse是一个包含JSON字段POJO的数据类。

我无法使用

访问文件数据
Response response = restAdapter.apiRequest();

try {
    //you can now get your file in the InputStream
    InputStream is = response.getBody().in();
} catch (IOException e) {
    e.printStackTrace();
}

因为ExportResponse是数据类,所以val response: ExportResponse = interactor.exportPdf(token)将返回数据,而不是Retrofit对象。

3 个答案:

答案 0 :(得分:1)

您可以将exportPdf的返回类型更改为Call<ResponseBody>,然后检查响应代码。如果可以,请以流的形式读取正文。如果不是,请尝试反序列化ExportResponse。 我猜看起来像这样:

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
}

更新

这是我的考试的完整清单:

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

这是用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')

对我来说一切正常

更新2

看起来您必须在Api中编写此代码:

@FormUrlEncoded
@Streaming // You can also comment this line.
@POST("export-pdf/")
fun exportPdf(
    @Field("token") token: String
): Call<ResponseBody>

答案 1 :(得分:0)

由于@AndreiTanana,我发现了一个错误。请求定义中的suspend中存在问题。所有其他请求保留其suspend修饰符,但此请求将其删除。我更改了代码。

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
}

然后在实现中,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()
}

改装客户端:

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
}

交互者:

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

然后分段:

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

旧答案

此答案不适用于PDF之类的二进制文件。也许可以与文本文件一起使用。

ApiClient:

val okHttpClient = OkHttpClient().newBuilder()
    .connectTimeout(60, TimeUnit.SECONDS)
    .readTimeout(60, TimeUnit.SECONDS)
    .writeTimeout(60, TimeUnit.SECONDS)
    .build()

retrofit = Retrofit.Builder()
    .baseUrl(SERVER_URL)
    .client(okHttpClient)
    .addConverterFactory(ScalarsConverterFactory.create()) // It is used to convert Response<String>.
    // .addConverterFactory(GsonConverterFactory.create(gson)) - you can also add this line.
    .build()

Api:

@FormUrlEncoded
@Streaming // You can also comment this line.
@POST("export-pdf/")
suspend fun exportPdf(
    @Field("token") token: String
): Response<String>

交互者:

private val service by lazy {
    ApiClient.getRetrofit().create(Api::class.java)
}

suspend fun exportPdf(
    token: String
): Response<String> =
    service.exportPdf(token)

片段:

private fun exportPdf(token: String) {
    launch {
        try {
            val response = interactor.exportPdf(token)
            var error: String? = null
            if (response.headers().get("Content-Type")?.contains(
                    "application/json") == true) {
                // Received JSON with an error.
                val json: String? = response.body()
                error = json?.let {
                    val export = gson.fromJson(json, ExportPdfResponse::class.java)
                    export.errors?.message?.firstOrNull()
                } ?: getString(R.string.request_error)
            } else {
                // Received PDF.
                val buffer: ByteArrayInputStream? = response.body()?.byteInputStream()
                if (buffer != null) {
                    val file = context?.let { createFile(it, "pdf") }
                    if (file != null) {
                        copyStreamToFile(buffer, file)
                    }
                }
            }
        } catch (e: Exception) {
            launch(Dispatchers.Main) {
                if (isAdded) {
                    showErrorDialog(getString(R.string.connection_timeout))
                }
            }
        }
    }
}

我检查响应头,然后检测它是JSON还是PDF。对于PDF,我使用ScalarsConverterFactory将响应转换为字节流。 copyStreamToFile将字节复制到文件中,我在https://stackoverflow.com/a/56074084/2914140中找到了它。

它创建了113973字节的文件,而不是69857字节的文件,未渲染。我在里面看到了,它用其他代码更改了所有非拉丁符号。

我试图在方法中编写Response<Any>,但是导致错误:“ java.lang.IllegalArgumentException:无法为方法Api.exportPdf的类java.lang.Object创建转换器”。我尝试为Any编写我的ConverterFactory,但没有成功。

也更改了此代码并收到错误:

  • java.lang.IllegalStateException:无法读取转换后的主体的原始响应主体。
  • java.lang.IllegalArgumentException:无法为方法Api.exportPdf的类java.lang.String创建转换器
  • 无法创建用于Retrofit2的转换器。请调用方法Api.exportPdf。

答案 2 :(得分:0)

如果您要查找具有协程 GET请求的常规文件下载

另请参阅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方法:

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

片段:

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