我看到了诸如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对象。
答案 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,但没有成功。
也更改了此代码并收到错误:
答案 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()
}
}
}