【问题标题】:How to download PDF file with Retrofit and Kotlin coroutines?如何使用 Retrofit 和 Kotlin 协程下载 PDF 文件?
【发布时间】:2019-11-20 07:53:59
【问题描述】:

我看到像How to download file in Android using Retrofit library? 这样的主题,他们使用@Streaming 和RxJava / 回调。

我有 Kotlin、协程、Retrofit 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)

如果某个token参数正确,则查询返回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 对象。

【问题讨论】:

  • 您可以将exportPdf的返回类型更改为Response<Any>,然后查看响应码。如果没问题,则将正文作为流读取。如果不是,请尝试反序列化ExportResponse
  • @AndreiTanana,非常感谢!我明天检查。如果是对的,会请你在这里回答。
  • @AndreiTanana,请在此处添加您的答案,以便我接受。稍后我会添加我的答案。我改了一下:Response<String>.addConverterFactory(ScalarsConverterFactory.create()),之后就可以拿到文件了。
  • 好的,我想我找到了更好的解决方案

标签: android kotlin retrofit2 kotlin-coroutines


【解决方案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>

【讨论】:

  • 谢谢!我还将文件作为字节流读取。测试后会尽快添加我的答案。
  • 抱歉,此变体导致错误:“java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call for method exportPdf”。我尝试了第一个变体(使用 Response 和 ScalarsConverterFactory),它几乎可以工作,但返回的 PDF 的大小几乎是原始文件的 2 倍。我一直在为 Retrofit 而不是 ScalarsConverterFactory 创建自定义 ConverterFactory。
  • 这很奇怪。我已将我的测试查询类声明为 interface Queries { @GET fun exportPdf(@Url url: HttpUrl): Call&lt;ResponseBody&gt; } 并且转换器没有错误
  • 谢谢!我试过了,可能做错了什么,但再次出现异常:“java.lang.IllegalArgumentException: Unable to create converter for retrofit2.Call for method Api.exportPdf”。我使用改造 2.6.0。另外,我使用 POST 查询。
  • 您的代码适用于 GET 和 POST 请求。我添加了 Internet、存储权限、FileProvider、协程、更改的链接。然后我更改为 POST 请求并得到异常:“无法为方法 Queries.exportPdf 创建retrofit2.Call 的转换器。原因:java.lang.IllegalArgumentException:找不到retrofit2.Call 的ResponseBody 转换器”。难以置信,但我的错误在于请求!经过一天的游览,我找到了它。我在fun exportPdf中使用了suspend,导致各种异常!!!
【解决方案2】:

感谢@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)
                }
            }
        }
    }
}

【讨论】:

  • 您是否尝试过使用Call&lt;ResponseBody&gt; 而不是Response&lt;String&gt;?对我来说,它工作正常并且不会使文件大小加倍。我已经用完整列表更新了我的答案
  • @AndreiTanana,是的,我试过了,也收到很多错误。非常感谢!
猜你喜欢
  • 2022-01-10
  • 2021-07-31
  • 2017-02-24
  • 1970-01-01
  • 2015-09-16
  • 2020-10-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多