A good experience in a mobile application rests heavily on good communication. You communicate your intent to the application, and it communicates back to you. The better you and the application communicate, the better the experience will be. Uploading and downloading files is an operation that can take some time, so it is important that we get the communication right when this is happening.
If you purchase an item online, the experience generally feels better when the seller provides you with a tracking number with detailed information on the progress of the order (e.g. when the order has been processed, when it has been packaged, when it has been shipped, when it has arrived at a depot, and so on). Knowing that the order is proceeding as expected, and being able to estimate how long it will be until the package arrives makes for a much better experience then just ordering something and it showing up a week later.
The same goes for uploads and download. If I’m uploading a large file or files, it is unsettling to just click the Upload button and then wait around for maybe 5 minutes as nothing is visually happening on screen. It would be a much better experience if something is displayed on the screen that indicates the current progress of the upload. In this tutorial, we are going to cover how we can go about reporting upload and download progress in our Ionic applications.
This tutorial will just be focusing on the client-side and assumes that you already have somewhere to upload a file to or download a file from. In a future tutorial I will cover the basics of how to handle uploading and downloading files on a NestJS server.
I will be using an example in this tutorial from one of my own projects that involves uploading multipart/form-data
data containing potentially many different files to a NestJS server (the fact that it is a NestJS server isn’t really relevant).
The code for this upload functionality, before adding the upload/download progress code, looks like this:
upload(data, files){
let formData = new FormData();
formData.append('title', data.title);
files.forEach((file) => {
formData.append('files[]', file.rawFile, file.name);
});
return this.http.post(this.url, formData, {
responseType: 'arraybuffer'
});
}
With the code above, there is no way to tell how much an upload or download has progressed. Instead, the user will just have to wait an indeterminate amount of time until the operation completes. At best, all you can really do here is show some kind of loading indicator. It would be much better to have some kind of progress bar that gives an indication of the time remaining to the user, and that is what we are going to modify this to do.
We won’t actually be covering creating the actual UI element for a progress bar in this tutorial, we are just interested in a number from 0
to 100
that updates to indicate the current progress - you can then use this data to do whatever you like. If you would like more instruction on creating the progress bar itself, I do have a rather old tutorial on creating a custom progress bar component - there will be some minor things that need changing but the basic idea is the same. You could also likely find pre-existing packages that you could install into your project to get a progress bar.
Before We Get Started
Last updated for Ionic 4.0.0
This is an advanced tutorial that assumes you already have a decent working knowledge of the Ionic framework. If you require more introductory level content on Ionic I would recommend checking out my book or the Ionic tutorials on my website.
Reporting Download and Upload Progress
In order to get and process progress events from our uploads or downloads we need to modify our request a little bit. First of all, we are just going to be using a generic HttpRequest
and we will need to supply the reportProgress
option:
let req = new HttpRequest('POST', this.url, formData, {
responseType: 'arraybuffer',
reportProgress: true,
});
return this.http.request(req).pipe(
map((event) => this.getStatusMessage(event)),
tap((message) => console.log(message)),
last()
);
The responseType
of arraybuffer
is not important here, that is just something I am using in my own code. We then pass that request, to the HttpClient
by calling request
. Typically, we would just return this observable and subscribe to it from wherever we want to trigger it, but in this case we are going to pipe
some operators to interact with the observable. We want to achieve a few things here.
The map
is a commonly used operator which allows for modifying the data emitted by an observable, we are just using it here to modify the response we get into something easier to understand (we will implement getStatusMessage
in a moment). The other two operators we are using are much less common.
The tap
operator will allow us to perform a “side effect” each time some data is emitted from the observable. It is similar to simply subscribing to an observable and running a function, except that it won’t trigger the observable if it isn’t subscribed to elsewhere. In this case, we just want to log out the value every time some value is emitted.
The last
operator will make sure that only the last bit of data from the observable stream will be sent to whatever is subscribed to this observable, and it will contain the final response from the server (e.g. the one we are usually interested in). This allows us to handle all of our progress events from the observable as we wish, and then only the final response will be sent to whatever subscribes to this observable (this way, all of our “progress” events won’t mess up our application logic).
The end result here is that map
will modify the emitted values into something useful, tap
will allow us to log out the values we are receiving (this is just for debugging purposes), and last
will send the final response from the server to whatever subscribed to the observable.
The important part here is getStatusMessage
which is what we are going to use to determine what kind of “progress event” we received, and also to keep track of the current upload/download progress. We are going to set up two behaviour subjects that look like this:
public uploadProgress: BehaviorSubject<number> = new BehaviorSubject<number>(0);
public downloadProgress: BehaviorSubject<number> = new BehaviorSubject<number>(0);
You would add these to the top of whatever service you are creating to handle launching your HTTP request. By having these two behaviour subjects, we can update them as new progress data comes in, and anything that is subscribed to these behaviour subjects will immediately get the updated data (e.g. we could tie these values into some kind of progress bar element).
The getStatusMessage
function would look like this:
getStatusMessage(event){
let status;
switch(event.type){
case HttpEventType.Sent:
return `Uploading Files`;
case HttpEventType.UploadProgress:
status = Math.round(100 * event.loaded / event.total);
this.uploadProgress.next(status);
return `Files are ${status}% uploaded`;
case HttpEventType.DownloadProgress:
status = Math.round(100 * event.loaded / event.total);
this.downloadProgress.next(status); // NOTE: The Content-Length header must be set on the server to calculate this
return `Files are ${status}% downloaded`;
case HttpEventType.Response:
return `Done`;
default:
return `Something went wrong`
}
}
We can get a variety of different event types from our request - you can find an explanation of what these event types mean here. We want to figure out which event type we are dealing with, and then handle it accordingly.
In each case, we return a message that our tap
operator is going to log out for us (again, just for debugging purposes in this instance). We are also specifically interested in the UploadProgress
and DownloadProgress
event types. In this case, we use the loaded
and total
values to figure out how much of the upload or download has completed. We then report those values by triggering next
on the behaviour subejcts that we just set up.
An important note about the download progress is that the server must respond with a Content-Length
header for the download, otherwise event.total
will be undefined
and you won’t be able to calculate the progress (we can’t work out how much is left to download if we don’t know how big the download is). If the server does not respond with a Content-Length
header then the total
property will not exist.
Displaying Download and Upload Progress
Now that we have everything in place, we just need to make use of it. You might also want to add an additional function to your service if you intend to handle multiple uploads/downloads in order to reset the progress bar for new batches:
resetProgress(){
this.uploadProgress.next(0);
this.downloadProgress.next(0);
}
To display progress updates, you should just trigger your HTTP request as you usually would by subscribing to it, but you should also subscribe to either the upload or download observables we set up:
this.uploader.resetProgress();
this.uploader.uploadProgress.subscribe((progress) => {
this.uploadProgress = progress;
});
Each time there is a change in the upload progress we are setting the uploadProgress
member variable to whatever that value is. You can then just bind that value to some kind of progress bar component like the one in the tutorial I mentioned before:
<my-progress-bar [progress]="uploadProgress"></my-progress-bar>
Now, as your files are uploading (or downloading), you will be able to see the progress bar updating as updates come in.
Summary
It might not always be necessary to set up progress events for HTTP requests, especially if they are executed quickly. In fact, you should avoid using reportProgress
if it is not required as it will trigger additional unnecessary change detections if you are not making use of the values. However, if you are handling requests that will result in a significant delay, adding a progress bar can go a long way to improving user experience.