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:
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.
EmoticonEmoticon