Download Files in Android - Kotlin & Java

Unless you are writing a Download Manager, downloading files in an Android application is never going to be the main business logic to solve but based on the requirement, you may need to download metafiles, media files, or documents over the Internet within your Android application. However, a reliable solution to download files in an Android application poses its own challenges including but not limited to the following list:


1. Thread Management

IO operations must be executed in a separate thread. Though there are plenty of options to achieve this such as now deprecated AsyncTask, Kotlin coroutines, or RxJava, still complex for a novice developer to properly handle the concurrency problems. Sending signals to the downloading thread to cancel or pause the download may introduce additional synchronization issues.


2. Error Handling

What if the Internet connection dropped while downloading the file? What if the application is closed while downloading/saving the file? How to handle all HTTP errors? the list goes on. Addressing all these scenarios is tedious and most of the time not possible.


3. Updating the UI

While downloading the file in a separate thread, updating a progress bar in the main thread is a challenge unless the concurrency framework you use provides an easy way. The same goes to update the UI after the file is downloaded.


4. Advanced Features

Though not all applications need it, sometimes you may need to pause and resume a download. Implementing such advanced features requires complex coding.

Of course, you can write the code from scratch to download a file, but it is hard to write a reliable solution addressing all the above-listed challenges. Especially if your business logic is something else, the time invested to solve these problems may not be productive.

There are plenty of libraries out there solving all these problems and provide easy to use APIs to download files. This article introduces such a library: PRDownloader a feature-rich but simple library to download files. PRDownloader offers the following features in addition to solving all the above listed problems.

  • PRDownloader can be used to download any type of files like image, video, pdf, apk and etc.
  • This file downloader library supports pause and resume while downloading a file.
  • Supports large file download.
  • This downloader library has a simple interface to make download request.
  • We can check if the status of downloading with the given download Id.
  • PRDownloader gives callbacks for everything like onProgress, onCancel, onStart, onError and etc while downloading a file.
  • Supports proper request canceling.
  • Many requests can be made in parallel.
  • All types of customization are possible.

The library is inter-operable with both Java and Kotlin. This article is using Kotlin code to demonstrate the library but it can be used with Java too.

Let's get our hands dirty. The following sample project demonstrates how to use PR Downloader to download a file, show the progress in a progress bar and show a Toast message once the file is successfully downloaded or if there is an error.



Step 1:

Create a new Android project with an Empty Activity in your Android Studio with the following properties:

Name: PRDownloader Demo
Package name: com.example.prdownloaderdemo
Language: Kotlin (You can choose Java if you prefer Java)
Leave other fields with their default value.


Step 2:

First thing first. Add the following permission to the Android Manifest file to access the Internet. You may also need to have External Storage permission if you want to save the file to an external storage location. To keep the article simple, we will save the file in the internal storage of the application

<uses-permission android:name="android.permission.INTERNET" />

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


After the changes, the Android Manifest.xml should look like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.prdownloaderdemo">

<uses-permission android:name="android.permission.INTERNET" />

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.PRDownloaderDemo">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
</manifest>


Step 3:

Open the build.gradle (Module) file and add the following dependency.

implementation 'com.mindorks.android:prdownloader:0.6.0'

After adding the dependency, the build.gradle should look like this. Click the "Sync Now" link to synchronize the project after saving the changes made in the build.gradle file.


Step 4:
Modify the activity_main.xml as shown below. A download button and a progress bar are added to the layout.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent" />

    <Button
        android:id="@+id/btnDownload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Download"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


Step 5:
Open the MainActivity.kt file and initialize the references for the ProgressBar and Button widgets and initialize them in the onCreate method as shown below:
package com.example.prdownloaderdemo

import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity


class MainActivity : AppCompatActivity() {

    private lateinit var progressBar: ProgressBar
    private lateinit var btnDownload: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Find the widgets
        this.progressBar = findViewById(R.id.progressBar)
        this.btnDownload = findViewById(R.id.btnDownload)
    }
}

Step 6:
Initialize the PRDownloader in the onCreate function as shown below:
// Initialize PRDownloader with read and connection timeout
val config = PRDownloaderConfig.newBuilder()
    .setReadTimeout(30000)
    .setConnectTimeout(30000)
    .build()
PRDownloader.initialize(applicationContext, config)

In the above code, read and connection timeout values are set to limit the waiting time if the server is down. PRDownloader offers more configuration options to pause and resume downloads but this article will stick to a simple implementation. After the changes, the MainActivity.kt should look like this:

package com.example.prdownloaderdemo

import android.os.Bundle
import android.widget.Button
import android.widget.ProgressBar
import androidx.appcompat.app.AppCompatActivity
import com.downloader.PRDownloader
import com.downloader.PRDownloaderConfig


class MainActivity : AppCompatActivity() {

    private lateinit var progressBar: ProgressBar
    private lateinit var btnDownload: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Find the widgets
        this.progressBar = findViewById(R.id.progressBar)
        this.btnDownload = findViewById(R.id.btnDownload)

        // Initialize PRDownloader with read and connection timeout
        val config = PRDownloaderConfig.newBuilder()
            .setReadTimeout(30000)
            .setConnectTimeout(30000)
            .build()
        PRDownloader.initialize(applicationContext, config)
    }
}


Step 7:

Create a new function named "readFile" to read a given text file from the internal storage and show it in a Toast. This function is optional as we need this only to validate the downloaded file. Also note that, showing a large text in a Toast is not a good idea but this article is using a tiny README file from the GitHub repository for the demo therefore the content of the text file is shown in a Toast.

private fun readFile(fileName: String) {
    return try {
        val reader = BufferedReader(InputStreamReader(baseContext.openFileInput(fileName)))
        reader.use {
            val sb = StringBuilder()
            var line: String?
            while (reader.readLine().also { line = it } != null) {
                sb.append(line)
            }
            val text = sb.toString()
            Toast.makeText(baseContext, text, Toast.LENGTH_LONG).show()
        }
    } catch (ex: FileNotFoundException) {
        Toast.makeText(baseContext, "Error in reading the file $fileName", Toast.LENGTH_SHORT)
            .show()
    }
}


Step 8:

Create another function named "download" with the following code. This function is responsible to download the file, save it to the internal storage and call the readFile method after downloading the file successfully.

private fun download(url: String, fileName: String) {
    PRDownloader.download(
        url,
        baseContext.filesDir.absolutePath,
        fileName
    )
        .build()
        .setOnProgressListener {
            // Update the progress
            progressBar.max = it.totalBytes.toInt()
            progressBar.progress = it.currentBytes.toInt()
        }
        .start(object : OnDownloadListener {
            override fun onDownloadComplete() {
                // Update the progress bar to show the completeness
                progressBar.max = 100
                progressBar.progress = 100

                // Read the file
                readFile(fileName)
            }

            override fun onError(error: Error?) {
                Toast.makeText(baseContext, "Failed to download the $url", Toast.LENGTH_SHORT)
                    .show()
            }

        })
}

As implemented in the above function, we only need to pass the URL, output destination, and output file name. PR Downloader will take care of all the IO operations and error handling.


Step 9:

Call the download function from the button click event in the onCreate function as shown below. Here a hard-coded URL and a file name are used for simplicity but you can extend this application to download from any URL you want.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Find the widgets
    this.progressBar = findViewById(R.id.progressBar)
    this.btnDownload = findViewById(R.id.btnDownload)

    // Initialize PRDownloader with read and connection timeout
    val config = PRDownloaderConfig.newBuilder()
        .setReadTimeout(30000)
        .setConnectTimeout(30000)
        .build()
    PRDownloader.initialize(applicationContext, config)

    this.btnDownload.setOnClickListener {
        val url =
            "https://raw.githubusercontent.com/javahelps/externalsqliteimporter/master/README.md"
        val fileName = "readme.md"

        download(url, fileName)
    }
}

After following all these steps, your final code should look like this:
package com.example.prdownloaderdemo

import android.os.Bundle
import android.widget.Button
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.downloader.Error
import com.downloader.OnDownloadListener
import com.downloader.PRDownloader
import com.downloader.PRDownloaderConfig
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.InputStreamReader


class MainActivity : AppCompatActivity() {

    private lateinit var progressBar: ProgressBar
    private lateinit var btnDownload: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // Find the widgets
        this.progressBar = findViewById(R.id.progressBar)
        this.btnDownload = findViewById(R.id.btnDownload)

        // Initialize PRDownloader with read and connection timeout
        val config = PRDownloaderConfig.newBuilder()
            .setReadTimeout(30000)
            .setConnectTimeout(30000)
            .build()
        PRDownloader.initialize(applicationContext, config)

        this.btnDownload.setOnClickListener {
            val url =
                "https://raw.githubusercontent.com/javahelps/externalsqliteimporter/master/README.md"
            val fileName = "readme.md"

            download(url, fileName)
        }
    }

    private fun download(url: String, fileName: String) {
        PRDownloader.download(
            url,
            baseContext.filesDir.absolutePath,
            fileName
        )
            .build()
            .setOnProgressListener {
                // Update the progress
                progressBar.max = it.totalBytes.toInt()
                progressBar.progress = it.currentBytes.toInt()
            }
            .start(object : OnDownloadListener {
                override fun onDownloadComplete() {
                    // Update the progress bar to show the completeness
                    progressBar.max = 100
                    progressBar.progress = 100

                    // Read the file
                    readFile(fileName)
                }

                override fun onError(error: Error?) {
                    Toast.makeText(baseContext, "Failed to download the $url", Toast.LENGTH_SHORT)
                        .show()
                }

            })
    }

    private fun readFile(fileName: String) {
        return try {
            val reader = BufferedReader(InputStreamReader(baseContext.openFileInput(fileName)))
            reader.use {
                val sb = StringBuilder()
                var line: String?
                while (reader.readLine().also { line = it } != null) {
                    sb.append(line)
                }
                val text = sb.toString()
                Toast.makeText(baseContext, text, Toast.LENGTH_LONG).show()
            }
        } catch (ex: FileNotFoundException) {
            Toast.makeText(baseContext, "Error in reading the file $fileName", Toast.LENGTH_SHORT)
                .show()
        }
    }
}


Step 10:

Save all the changes and run the app. Once the app is ready, click on the "Download" button and see if the file is downloaded. You may not see the progress bar being updated while downloading the given URL in this article because the file is too small and it will be downloaded within a fraction of a second. To see the progress, you have to download a large file but do not call the readFile function after downloading a large file.







To learn more about the PR Downloader, please take a look at the official GitHub page. If you have any questions, feel free to comment below.

Previous
Next Post »

Contact Form

Name

Email *

Message *