如何使用Retrofit库在安卓设备中下载文件?

49
我需要使用 Retrofit 库在我的应用中下载所有类型的文件(二进制、图像、文本等)。网上的所有示例都使用 HTML GET 方法。我需要使用 POST 方法来防止自动缓存。
我的问题是如何使用 POST 方法在 Retrofit 中下载文件?

尝试这个:https://dev59.com/01wY5IYBdhLWcg3wNFdY#36682027 - ugali soft
不要试这个 :) 这是对问题的第二个答案。 - CoolMind
9个回答

51

kotlin 中,做如下操作:

在您的服务中添加方法:

    @Streaming
    @GET
    suspend fun downloadFile(@Url fileUrl:String): Response<ResponseBody>

在ViewModel中调用此方法:

viewModelScope.launch {
     val responseBody=yourServiceInstance.downloadFile(url).body()
     saveFile(responseBody,pathWhereYouWantToSaveFile)
}

保存文件:

fun saveFile(body: ResponseBody?, pathWhereYouWantToSaveFile: String):String{
        if (body==null)
            return ""
        var input: InputStream? = null
        try {
            input = body.byteStream()
            //val file = File(getCacheDir(), "cacheFileAppeal.srl")
            val fos = FileOutputStream(pathWhereYouWantToSaveFile)
            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 pathWhereYouWantToSaveFile
        }catch (e:Exception){
            Log.e("saveFile",e.toString())
        }
        finally {
            input?.close()
        }
        return ""
    }

注意:

  1. 确保您的 refrofit 客户端的基本 url 和传递给 downloadFile 的 url 是有效的文件 url:

Retrofit 的基本 url + downloadFile 方法的 url = 文件 url

  1. downloadFile 前面加上 suspend 关键字,以便从 ViewModel 调用此方法,我使用了 viewModelScope.launch {},您可以根据您的调用端使用不同的协程作用域。

  2. 现在是 pathWhereYouWantToSaveFile,如果您想将文件存储到项目的文件目录中,可以这样做:

val fileName=url.substring(url.lastIndexOf("/")+1)
val pathWhereYouWantToSaveFile = myApplication.filesDir.absolutePath+fileName
如果您将下载的文件存储在文件夹或缓存目录下,则无需获取权限,否则对于公共存储,您知道流程。

使用JsonReader.setLenient(true)来接受位于第1行第1列路径$处的格式不正确的JSON。-> 这是我收到的错误。 - Mostafa Imani
1
在添加GsonBuilder().setLenient().create()之后,将其添加到addConverterFactory()中,我遇到了一个新的错误,即响应体没有参数。 - Mostafa Imani

19

使用 @Streaming

异步

编辑1

//On your api interface
@POST("path/to/your/resource")
@Streaming
void apiRequest(Callback<POJO> callback);

restAdapter.apiRequest(new Callback<POJO>() {
        @Override
        public void success(POJO pojo, Response response) {
            try {
                //you can now get your file in the InputStream
                InputStream is = response.getBody().in();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void failure(RetrofitError error) {

        }
    });

同步的

//On your api interface
@POST("path/to/your/resource")
@Streaming
Response apiRequest();

Response response = restAdapter.apiRequest();

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

谢谢您的回复,我会尝试并会再与您联系。 - Dr. Ehsan Ali
那是一个对象类,你可以将它更改为 Object - NaviRamyle
我遇到了这个错误:只有返回类型为Response的方法才允许使用@Streaming注解。我正在使用异步模式。 - Dr. Ehsan Ali
你知道如何使用Retrofit 2.0.0 beta实现相同的功能吗? - Sivakumar S
1
@Ehsan,你可以使用Response(import retrofit.client.Response;)作为模型,并从该Response对象中获取一个字符串。 - bheatcoker
显示剩余2条评论

10
这是如何在Retrofit 2中下载文件的方法。
public interface ServerAPI {
        @GET
        Call<ResponseBody> downlload(@Url String fileUrl);

        Retrofit retrofit =
                new Retrofit.Builder()
                        .baseUrl("http://192.168.43.135/retro/") // REMEMBER TO END with /
                        .addConverterFactory(GsonConverterFactory.create())
                 .build();

}

    //How To Call
public void download(){
        ServerAPI api = ServerAPI.retrofit.create(ServerAPI.class);
        api.downlload("http://192.168.43.135/retro/pic.jpg").enqueue(new Callback<ResponseBody>() {
                    @Override
                    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                        try {
                            File path = Environment.getExternalStorageDirectory();
                            File file = new File(path, "file_name.jpg");
                            FileOutputStream fileOutputStream = new FileOutputStream(file);
                            IOUtils.write(response.body().bytes(), fileOutputStream);
                        }
                        catch (Exception ex){
                        }
                    }

                    @Override
                    public void onFailure(Call<ResponseBody> call, Throwable t) {
                    }
                });
}

2
这段代码甚至无法编译。你不能在“接口”中初始化东西。 - Bryan Bryce
3
你尝试过编译它吗?对我来说可行,这是我的使用方法。 - ugali soft
1
对于IOUtils,请添加依赖项implementation 'org.apache.directory.studio:org.apache.commons.io:2.4' - tendai
java.lang.RuntimeException: 调用公共的com.squareup.okhttp.ResponseBody()构造函数失败,没有参数。这是错误。 - Mostafa Imani
@ugalisoft 我可以在使用 Retrofit 下载时传递一个与 Retrofit 配置的基本 URL 无关的不同 URL 吗?这样做会有效果吗? - undefined

10

使用 Kotlin,它会变得更加简单。

API 服务

@GET
@Streaming
fun download(@Url url: String): Call<ResponseBody>

API客户端

object ApiClient {
    private val retrofit = ...

    val service: ApiService = retrofit.create(ApiService::class.java)
}

下载功能

fun download(urlString: String, target: File) {
    val response = ApiClient.service.download(urlString).execute()
    response.body()?.byteStream()?.use {
        target.parentFile?.mkdirs()
    
        FileOutputStream(target).use { targetOutputStream ->
            it.copyTo(targetOutputStream)
        }
    } ?: throw RuntimeException("failed to download: $urlString")
}

3
如果你使用Retrofit 2.0.0,你可以参考我的答案在这个问题下 -- 使用retrofit下载图片文件
关键点是使用okhttp3.ResponseBody来接收原始二进制数据,而不是任何POJO对象。
如果你想使用POST方法获取文件,很容易,只需将@GET更改为@POST,但它取决于您的服务器是否支持POST方法!

3

您可以使用以下代码以带有进度的方式进行下载(Kotlin)

Retrofit Api服务

@Streaming
@GET
fun downloadFile(@Url fileUrl: String): Observable<Response<ResponseBody>>

请确保在下载大文件时添加@Streaming注释。

然后将以下代码粘贴到您的Activity或Fragment中。

fun downloadfileFromRetrofit() {
    val retrofit = Retrofit.Builder()
        .baseUrl("ENTER_YOUR_BASE_URL")
        .client(OkHttpClient.Builder().build())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()
    val downloadService = retrofit.create(RetrofitApi::class.java)

   downloadService.downloadFile("FILE_URL_PATH").observeOn(AndroidSchedulers.mainThread())
        .subscribeOn(Schedulers.io()).subscribe({
            val task = object : AsyncTask<Void, Integer, Void>() {
                override fun doInBackground(vararg voids: Void): Void? {
                    val writtenToDisk =writeResponseBodyToDisk(it.body()!!)
                    println("file download was a success? $writtenToDisk")
                    return null
                }
            }
            task.execute()
        }, {
            print(it.message)
        })
}

以下是writeResponseBodyToDisk方法。
fun writeResponseBodyToDisk(body: ResponseBody): Boolean {
    val appDirectoryName = "YOUR_DIRECTORY_NAME"
    val filename = "YOUR_FILE_NAME"
    val apkFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename)
    try {

        var inputStream: InputStream? = null
        var outputStream: OutputStream? = null
        try {
            val fileReader = ByteArray(4096)
            val fileSize = body.contentLength()
            var fileSizeDownloaded: Long = 0
            inputStream = body.byteStream()
            outputStream = FileOutputStream(apkFile)
            while (true) {
                val read = inputStream!!.read(fileReader)
                if (read == -1) {
                    break
                }
                outputStream.write(fileReader, 0, read)
                fileSizeDownloaded += read.toLong()

           calulateProgress(fileSize.toDouble(),fileSizeDownloaded.toDouble()
                println("file downloading $fileSizeDownloaded of $fileSize")

            outputStream.flush()

            return true
        } catch (e: Exception) {
            println(e.toString())
            return false
        } finally {
            if (inputStream != null) {
                inputStream!!.close()
            }
            outputStream?.close()
        }
    } catch (e: Exception) {
        println(e.toString())
        return false
    }

}

以下方法用于计算进度:
 fun calulateProgress(totalSize:Double,downloadSize:Double):Double{
    return ((downloadSize/totalSize)*100)
}

这个解决方案需要 RxJava 和 AsyncTask。 - CoolMind
13
既然有了RxJava,为什么还要使用AsyncTask呢? - Carson Holzheimer
6
@CarsonHolzheimer 或者更好,为什么要使用 RxJava 或者 AsyncTask,当你可以使用 Kotlin 协程呢? :D - Akbolat SSS
2
如果您正在阅读此内容,请不要使用上面的答案。它混合了不同的编程方法,没有任何优势。这会使您的应用程序容易出现错误、缺乏可读性等问题。 - Fattum
1
如果有更好的方法,请发布替代方案。 - user3561494

2
我使用了以下代码来使用Retrofit下载任何类型的文件...
 File file = new File("Your_File_path/name");

  private void startDownload() {

    if (!NetWorkUtils.getInstance(context).isNetworkAvailable()) {
        Toast.makeText(context, "No data connection available", Toast.LENGTH_SHORT).show();
        return;
    }

    showProgressDialog();

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(FILE_BASE_URL)
            .build();

    FileHandlerService handlerService = retrofit.create(FileHandlerService.class);

    Call<ResponseBody> call = handlerService.downloadFile(mFileName);
    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
            dismissProgressDialog();
            if (response.isSuccessful()) {
                if (writeResponseBodyToDisk(response.body())) {
                    listener.onFileLoaded(file);
                }
            } else {
                listener.onDownloadFailed("Resource not Found");
            }
        }

        @Override
        public void onFailure(Call<ResponseBody> call, Throwable t) {
            dismissProgressDialog();
            listener.onDownloadFailed("Download Failed");
            t.printStackTrace();
        }
    });

}


interface FileHandlerService {

    @GET("uploads/documents/{file_name}")
    Call<ResponseBody> downloadFile(
            @Path("file_name") String imageName);
}

private boolean writeResponseBodyToDisk(ResponseBody body) {
    try {

        InputStream inputStream = null;
        OutputStream outputStream = null;

        try {
            byte[] fileReader = new byte[4096];

            long fileSize = body.contentLength();
            long fileSizeDownloaded = 0;

            inputStream = body.byteStream();
            outputStream = new FileOutputStream(file);

            while (true) {
                int read = inputStream.read(fileReader);

                if (read == -1) {
                    break;
                }

                outputStream.write(fileReader, 0, read);

                fileSizeDownloaded += read;

                Log.d(TAG, "file download: " + fileSizeDownloaded + " of " + fileSize);
            }

            outputStream.flush();

            return true;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }
            if (outputStream != null) {
                outputStream.close();
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
}

它在我的Android 5.1上可以运行,但在其他设备上无法运行。我已经在Android 6上测试过了,但是不起作用。 - Rodrigo

1
在MainActivity.java中包含以下函数:
void getRetrofitImage() {

    Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(url)
            .addConverterFactory(GsonConverterFactory.create())
            .build();

    RetrofitImageAPI service = retrofit.create(RetrofitImageAPI.class);

    Call<ResponseBody> call = service.getImageDetails();

    call.enqueue(new Callback<ResponseBody>() {
        @Override
        public void onResponse(Response<ResponseBody> response, Retrofit retrofit) {

            try {

                Log.d("onResponse", "Response came from server");

                boolean FileDownloaded = DownloadImage(response.body());

                Log.d("onResponse", "Image is downloaded and saved ? " + FileDownloaded);

            } catch (Exception e) {
                Log.d("onResponse", "There is an error");
                e.printStackTrace();
            }

        }

        @Override
        public void onFailure(Throwable t) {
            Log.d("onFailure", t.toString());
        }
    });
}

图像下载的文件处理部分将是:

private boolean DownloadImage(ResponseBody body) {

    try {
        Log.d("DownloadImage", "Reading and writing file");
        InputStream in = null;
        FileOutputStream out = null;

        try {
            in = body.byteStream();
            out = new FileOutputStream(getExternalFilesDir(null) + File.separator + "AndroidTutorialPoint.jpg");
            int c;

            while ((c = in.read()) != -1) {
                out.write(c);
            }
        }
        catch (IOException e) {
            Log.d("DownloadImage",e.toString());
            return false;
        }
        finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.close();
            }
        }

        int width, height;
        ImageView image = (ImageView) findViewById(R.id.imageViewId);
        Bitmap bMap = BitmapFactory.decodeFile(getExternalFilesDir(null) + File.separator + "AndroidTutorialPoint.jpg");
        width = 2*bMap.getWidth();
        height = 6*bMap.getHeight();
        Bitmap bMap2 = Bitmap.createScaledBitmap(bMap, width, height, false);
        image.setImageBitmap(bMap2);

        return true;

    } catch (IOException e) {
        Log.d("DownloadImage",e.toString());
        return false;
    }
}

您可以查看完整教程:使用Retrofit 2.0进行图像下载

0

下载文件的请求声明如下所示

// option 1: a resource relative to your base URL
@GET("/resource/example.zip")
Call<ResponseBody> downloadFileWithFixedUrl();

// option 2: using a dynamic URL
@GET
Call<ResponseBody> downloadFileWithDynamicUrlSync(@Url String fileUrl);  

在声明请求调用后,像这样

FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);

Call<ResponseBody> call = downloadService.downloadFileWithDynamicUrlSync(fileUrl);

call.enqueue(new Callback<ResponseBody>() {  
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        if (response.isSuccess()) {
            Log.d(TAG, "server contacted and has file");

            boolean writeToDisk = writeToDisk(response.body());

            Log.d(TAG, "file downloaded " + writtenToDisk);
        } else {
            Log.d(TAG, "server error");
        }
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        Log.e(TAG, "error");
    }
});

同时将此方法实现为将文件保存到您的SD卡中。

private boolean writeToDisk(ResponseBody body) {  
    try { File mediaStorageDir = new File(
                Environment
                        .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
                "ProfileImage");

        // Create the storage directory if it does not exist
        if (!mediaStorageDir.exists()) {
            if (!mediaStorageDir.mkdirs()) {
                Log.e("ProfileImage", "Oops! Failed create "
                        + "ProfileImage" + " directory");
            }
        }
        File futureStudioIconFile = new File(mediaStorageDir.getPath() + File.separator
                + "userImage.png");

        InputStream inputStream = null;
        OutputStream outputStream = null;

        try {
            byte[] fileReader = new byte[4096];

            long fileSize = body.contentLength();
            long fileSizeDownloaded = 0;

            inputStream = body.byteStream();
            outputStream = new FileOutputStream(futureStudioIconFile);

            while (true) {
                int read = inputStream.read(fileReader);

                if (read == -1) {
                    break;
                }

                outputStream.write(fileReader, 0, read);

                fileSizeDownloaded += read;

                Log.d(TAG, "file download: " + fileSizeDownloaded + " of " + fileSize);
            }

            outputStream.flush();

            return true;
        } catch (IOException e) {
            return false;
        } finally {
            if (inputStream != null) {
                inputStream.close();
            }

            if (outputStream != null) {
                outputStream.close();
            }
        }
    } catch (IOException e) {
        return false;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接