Retrofit 2 — How to Download Files from Server

In this blog post of our Retrofit launch sequence we'll show you one of the most requested topics: how to download files. We'll give you all the insight and snippets you need to use Retrofit to download everything, from tiny .png's to large .zip files.

If this is your first Retrofit post on futurestud.io, feel free to browse our other Retrofit posts:

Retrofit Series Overview

  1. Callbacks (Coming soon)
  2. Annotations (Coming soon)
  3. Fluent Interface with Builders (Coming soon)

How to Specify the Retrofit Request

If you're reading this and you haven't written code for any Retrofit requests yet, please check our previous blog posts to get started. For all you Retrofit experts: the request declaration for downloading files looks almost like any other request:

// 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);  

If the file you want to download is a static resource (always at the same spot on the server) and on the server your base URL refers to, you can use option 1. As you can see, it looks like a regular Retrofit 2 request declaration. Please note, that we're specifying ResponseBody as return type. You should not use anything else here, otherwise Retrofit will try to parse and convert it, which doesn't make sense when you're downloading a file.

The second option is new to Retrofit 2. You can now easily pass a dynamic value as full URL to the request call. This can be especially helpful when downloading files, which are dependent of a parameter, user or time. You can build the URL during runtime and request the exact file without any hacks. If you haven't worked with dynamic URLs yet, feel free to head over to our blog post for that topic: dynamic urls in Retrofit 2

Pick what kind of option is useful to you and move on to the next section.

How to Call the Request

After declaring our request, we need to actually call it:

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 writtenToDisk = writeResponseBodyToDisk(response.body());

            Log.d(TAG, "file download was a success? " + writtenToDisk);
        } else {
            Log.d(TAG, "server contact failed");
        }
    }

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

If you're confused by the ServiceGenerator.create(), head over to our first blog post to get started. Once we've created the service, we'll make the request just like any other Retrofit call!

There is just one thing left, which currently hides behind the function writeResponseBodyToDisk(): writing the file to the disk!

How to Save the File

The writeResponseBodyToDisk() method takes the ResponseBody object and reads and writes the byte values of it to the disk. The code looks much more difficult than it actually is:

private boolean writeResponseBodyToDisk(ResponseBody body) {  
    try {
        // todo change the file location/name according to your needs
        File futureStudioIconFile = new File(getExternalFilesDir(null) + File.separator + "Future Studio Icon.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;
    }
}

Most of it is just regular Java I/O boilerplate. You might need to adjust the first line on where and with what name your file is being saved. When you have done that, you're ready to download files with Retrofit!

But we're not completely ready for all files yet. There is one major issue: by default, Retrofit puts the entire server response into memory before processing the result. This works fine for some JSON or XML responses, but large files can easily cause Out-of-Memory-Errors.

If your app needs to download even slightly larger files, we strongly recommend reading the next section.

Beware with Large Files: Use @Streaming!

If you’re downloading a large file, Retrofit would try to move the entire file into memory. In order to avoid that, we've to add a special annotation to the request declaration:

@Streaming
@GET
Call<ResponseBody> downloadFileWithDynamicUrlAsync(@Url String fileUrl);  

The @Streaming declaration doesn't mean you're watching a Netflix file. It means that instead of moving the entire file into memory, it'll pass along the bytes right away. But be careful, if you're adding the @Streaming declaration and continue to use the code above, Android will trigger a android.os.NetworkOnMainThreadException.

Thus, the final step is to wrap the call into a separate thread, for example with a lovely ASyncTask:

final FileDownloadService downloadService =  
        ServiceGenerator.createService(FileDownloadService.class);

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

            new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... voids) {
                    boolean writtenToDisk = writeResponseBodyToDisk(FileDownloadActivity.this, response.body(), null);

                    Log.d(TAG, "file download was a success? " + writtenToDisk);
                    return null;
                }
            }.execute();
        }
        else {
            Log.d(TAG, "server contact failed");
        }
    }

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

You can still use the same writeResponseBodyToDisk() method. If you remember the @Streaming declaration and this snippet, you can download even large files with Retrofit efficiently.

Next: Download Files with Progress Updates

If you download files in the foreground and they are not small, you might want to inform the user on your actions. Ideally, you would display progress updates how much you've downloaded already. We've another tutorial on how to download files with progress updates.

Summary

In this tutorial you've seen how to download files with Retrofit efficiently. This is everything you need to know to download files with Retrofit.

If you've any questions, let us know in the comments or on twitter.

Explore the Library

Find interesting tutorials and solutions for your problems.

Miscellaneous