I spent about a decade working as an iOS programmer. On that platform, in that timeframe, if you wanted to write an operation that would take so long that it would disrupt the user interface, you needed to wire up callback functions for every eventuality. The most common example is a network request. You would typically need a callback that will be called if the request succeeds, and another one if it fails.
But now here we are in the bright shiny future. These days, callbacks are, like, so last-century. Who wants to write three functions — the network request itself, and its two mandatory callbacks — when you can instead write only one? Such is the promise of async/await, the brand-new hotness.
I could waste a lot of time and effort writing about whether we’ve gained anything with this move or not. (Spoiler alert: my own personal belief is that async/await is one step forward, two steps back.) But there is no point in bloviating on this topic any further. That ship has sailed. My side lost. (For the moment. These things have a way of going in cycles.) It’s better to just get on with it, and make peace with the status quo.
But. Here is something that has always bothered me about the concept of async/await. (Well, one of the many things that bothers me about it. Whoops, there I go again, bloviating. I will shut up now.) Okay, so you write one async function, instead of one function and two or more callbacks. Great. What if the async function needs to be cancelled partway through?
Let’s consider the typical network request. You are calling on a remote server to give you some bit of information. In the best case scenario, that server will return what you want in mere milliseconds, so quickly that the user won’t even notice the delay. In the worst case, it might take several minutes to determine that the server is down, or that it’s not listening to you, or you don’t have a working internet connection, or whatever. Are you going to make the user suffer through all of that? Wouldn’t it be better to give her the option to say “Forget this, I want to give up now?”
If you are writing network requests using callbacks, building in cancellation is a no-brainer. If you are using async/await, it is pretty close to impossible, as far as I can tell.
But! This is my superpower: When sufficiently motivated, I always find a way. I had to bang on this problem for lord knows how many hours, but I finally figured it out. Here I present a demo Flutter program that shows you one way to go about it.
Running the Demo Program
The code is here: Async/Await Cancel Demo
Download it to your local machine, open the project in Android Studio, and run it on either the iOS or Android simulator.
The demo app presents you with two buttons. One of them runs a typical network request that cannot be cancelled by the user. This mode is useful in cases where the user cannot meaningfully continue until the network operation has successfully completed, such as a login request. The other button starts a network request that can be cancelled by the user.
In either case, the app runs a simulated network request that will take five seconds to complete. If you press the first button, the network request will always take five seconds, and will always complete successfully. In the second case, it will either succeed in five seconds, or it will be aborted prematurely at the moment when the user presses the “Cancel” button.
Building Cancellation Into Your Own Flutter Programs
Unfortunately, this demo program contains a whole lot of code. I don’t see how I could have avoided that, while also presenting a usable, working demo that others can learn from.
The good news is that you are free to ignore most of the code. There is a lot of uninteresting boilerplate, which exists solely to support the interesting parts.
First, locate the NetResponse object. This one encapsulates everything you need to know about one network request and its eventual outcome. You can either add new fields to it to make it usable in the real world, or else replace it with some other, similar object you are already using for that purpose.
Next, locate the NetCancelable object. This is where the “secret sauce” lives. This is how we interface with the async/await machinery built-in to Dart, and interrupt async functions that are in-flight. This is a critical piece of the puzzle. You might be able to use this class unmodified, exactly as it exists now.
For better or worse, NetCancelable is intimately tied up in the details of NetResponse, so these two objects are inseparably linked together. I suppose it would be possible to write NetCancelable in a more generic way — perhaps it could work with any type of object that builds on an abstract base class and implements a few common methods, for example — but that was more flexibility than I needed, so I left it out.
Finally, have a look at the showNetRequest() function. This is the one you can call for everyday use, any time you need to perform a network request, cancellable or not, while keeping the user apprised of what’s happening. The most important parameter you have to pass in is the function that performs the network request, which must have this function signature:
Future<NetResponse> netRequestFunction() async
This is the function that performs the actual network request, and eventually returns a NetResponse object at the end.
That should be enough to get you going. I have been using this code in production programs for a few years now, so I have a lot of confidence in this method of async function cancellation.
One thing that I have not yet implemented: There should be a way for the network request function to report progress for lengthy uploads or downloads, so we can give the user an accurate representation of how much of the request is completed, and estimate how much time is remaining. So far, I haven’t needed that badly enough to work out all the details. Someday, perhaps!