How to make multiple global loaders in Flutter
In almost every mobile app we have some async work to do, getting the current user's info from the server, backing up their work, performing a large search, or maybe all of them at once. Often we are tempted to let the user just wait a moment while we do our magic, and just hope they don’t click anything or break our async task in the middle. But then we remember that the user is a human and does things that humans do. So we must find a real solution. In the quest for the best loader package, I just couldn’t find what I was looking for. Here are some of my requirements that didn’t exist in any pre-build package:
- Listen to multiple tasks and hide the loader only after they are all finished.
- Update the message on the loader in real-time with the progress.
- A minimum display time for the loader. We don't want the loader to flash on and off. If we have a task that takes 100 milliseconds, we prefer for it to be up for one or two seconds. You know, some good UX principles.
- Control the loader from anywhere, not only from the widget we created.
1. Project setup
Ok now we’ve got over the cat images — let's dig into building the app. In it, we use Mobx and Provider to handle the state of the loaders, I suggest getting familiar with them first.
The code is structured in a very similar way to a package, where it is generic and can be implemented once and used throughout the application. Maybe one day I’ll actually turn it into a package, but for now you’ll need to implement it yourself :)
Start by creating a Flutter project by running the following in the terminal flutter create my_app_name
, and make sure it works with the command flutter run
.
2. Demo tasks
Let's create some fake async tasks. Add some task generators in the MyHome
class in main.dart
:
The function _multipleRandomTasks
creates a random number of tasks, and for each, it prints the time the task takes. Add multiple task buttons to the children of the column in the build widget, each one to run different numbers of async tasks:
Run the app and test the buttons. Look in the logs to verify everything is printed properly.
3. Create a LayoutBuilder
To display the loading dialog above all other widgets, use a Layout builder that gives a new context for the child widget. Update the MyApp
class in main.dart
:
4. The loader model
Every time we start an async process and want to wait for the result, a Loader
object is created. The Loader
object is very simple - it has an id
that can be automatically generated, a message
property to display to the user, and a visable
flag that is automatically set to true. It also has some advanced features, all optional, index
number determines the order of messages displayed, minDuration
is the duration property for the minimum time the loader needs to be displayed, and startDate
is for the loading dialog to use, so don’t set it. Create a file loader.dart
with the following code:
5. Provider and Mobx
Assuming you have some basic knowledge of the Provider and Mobx packages, we are going to install them in pubspec.yaml
. Mobx is a local store, where we save the current state of the loaders. Provider allows us to use the state throughout the app. Mobx uses code generators for generating boilerplate code, we’ll install them too. Add the packages to the dependencies
and Mobx code generator to dev_dependencies
:
6. Mobx store
Time to use Mobx! Create a file called overlayStore.dart
with the following code:
This class exposes some important methods that we can use later:
addLoader
— Adds a loader to the store.removeLoader
— A time-aware function that removes the loader after theminDuration
has passed.changeLoaderMessage
— Changes the message of a loader (e.g. if we are showing the progress in percentages).updateLoader
— Updates the loader with new properties (minDruation
,index
etc.).- The computed values
get loaders
andget displayLoading
are used by the loading overly in part 8, below.
To generate the boilerplate code for Mobx, run the following code in the terminal: flutter packages pub run build_runner build
.
7. Creating the widgets
This is the fun part! First, create a folder called widgets
and then add the following file to it:
dialog.dart
— A generic dialog widget (so you can use it in other places too).loading.dart
— A generic loading widget (you can also use it in other places).loadingDialog
— The dialog widget we are using for the loading screens. This is also a somewhat generic widget that can be used in other places too.- And finally
loadingOverlay.dart
— An implementation of the loading dialog.
The code for these widgets can be found in this GitHub commit, it is very generic and simple so it can be modified to fit your needs.
8. Adding the loading overlay
Wrap the MyApp()
function with the provider, and add a function called _insertOverlays
that add the overlays on top of the main app.
Now we can use the overlay in the LayoutBuilder
:
The loading overlay is always preset above all screens and if the flag overlayStore.displayOverlays
is true, it displays the loading overlay. Otherwise, it is just an invisibleContainer
.
9. Using the loading overlay
Time to use the overlay! Create an instance of it in the MyHome
widget, and pass it to the task buttons:
Now we can create the loaders. Update the random tasks generators so that, instead of printing, it create a loader object, adds it to the overlayStore, and finally after the task is finished, removes it from the store:
10. Using it!
Run the app and test the buttons. This is just a simple implementation of the app. As I mentioned in the beginning, there are more advanced use cases for this loader. Here are some:
- In a case where every time the user opens the app, we connect to the server and verify their account. Sometimes this can take a few milliseconds and sometimes a few good seconds. If the loading is just 200 milliseconds, we prefer not to show the loading screen for at least one second, so the screen won’t flash at the user and provide a bad UX experience. Here is how you can get around it
LoaderModel(message: “connecting…”, startTime: DateTime.now().add(Duration(milliseconds: 300)));
Now the Loader dialog appears only after 300 milliseconds. - We are uploading a big file and want to show the progress. This is super simple. Every time we get a status update call
overlayStore.changeLoaderMessage(loader.id, “progress is $percentage%”);
.
11. A few words about routing
In this tutorial, we didn’t use any routing. If you do want to, update the MyApp
home widget to the following:
Hope you found this article helpful, all code is available on GitHub for you to download. I’ll be happy to help with any questions, here or on my website zektec.com.