Performing HTTP requests is all fun and games until there's no internet connection. Sure, you can display a sad error message or a "Chrome dinosaur" to the user, but wouldn't it be better to take the initiative and automatically retry the request when the user connects to a WiFi network or enables mobile data? This is precisely what we're going to implement in this tutorial.
Starter project
We're going to use the dio HTTP client and the connectivity package to accomplish our goal. We're not using the default http package because it's pretty lame. I mean, come on! No support for interceptors? These will be central to make this auto-retry work.
All of the packages and the basic UI is prepared for you in the starter project so that you won't waste time if you want to follow along. By the end of this tutorial, we'll have created an app with the following behavior:
Creating an interceptor
Interceptors run every time you do some action on a Dio
object. You can then perform logic in three callback methods - onRequest
, onResponse
and onError
.
They are useful for plenty of things such as logging or, in our case, scheduling a request retry when we detect there's no connection. You are probably familiar with the dreaded SocketException
that gets thrown when the device isn't connected to a network. This means only one thing - we're going to utilize the onError
callback.
retry_interceptor.dart
class RetryOnConnectionChangeInterceptor extends Interceptor {
@override
Future onError(DioError err) async {
// TODO: Schedule a retry
}
}
This is the basic outline of our interceptor. We're going to get to the retry logic in just a bit but first, it's important to note that the onError
callback will run for all kinds of errors including status codes such as 401 or 503. I think it's a good idea to retry the request only when the error is the aforementioned SocketException
.
How can we find out which exact type of of an error occurred? We can use the handy fields of the DioError
object!
retry_interceptor.dart
@override
Future onError(DioError err) async {
if (_shouldRetry(err)) {
// TODO: Schedule a retry
}
// Let the error "pass through" if it's not the error we're looking for
return err;
}
bool _shouldRetry(DioError err) {
return err.type == DioErrorType.DEFAULT &&
err.error != null &&
err.error is SocketException;
}
DioErrorType.DEFAULT
means "some other error happened". This includes platform exceptions which is what we're interested in.Request retrier
Having the interceptor is only one part of the game. How can we actually schedule the failed requests to be retried as soon as the device is connected to a network? The answer is connectivity package, Stream
and a Completer
.
We could just plop this logic right into the RetryOnConnectionChangeInterceptor
but I'm a proponent of keeping the code focused on one task. Let's create a DioConnectivityRequestRetrier
.
dio_connectivity_request_retrier.dart
class DioConnectivityRequestRetrier {
final Dio dio;
final Connectivity connectivity;
DioConnectivityRequestRetrier({
@required this.dio,
@required this.connectivity,
});
Future<Response> scheduleRequestRetry(RequestOptions requestOptions) async {
// TODO: Implement
}
}
scheduleRequestRetry
will be passed the failed RequestOptions
object which will be used to perform the same request for the second time. We're then going to return the successful Response
back to the interceptor which scheduled a request retry.
To retry the request immediately when the network connection changes, the Connectivity
class offers a handy Stream
called onConnectivityChanged
.
dio_connectivity_request_retrier.dart
Future<Response> scheduleRequestRetry(RequestOptions requestOptions) async {
StreamSubscription streamSubscription;
streamSubscription = connectivity.onConnectivityChanged.listen(
(connectivityResult) async {
// We're connected either to WiFi or mobile data
if (connectivityResult != ConnectivityResult.none) {
// Ensure that only one retry happens per connectivity change by cancelling the listener
streamSubscription.cancel();
// Copy & paste the failed request's data into the new request
dio.request(
requestOptions.path,
cancelToken: requestOptions.cancelToken,
data: requestOptions.data,
onReceiveProgress: requestOptions.onReceiveProgress,
onSendProgress: requestOptions.onSendProgress,
queryParameters: requestOptions.queryParameters,
options: requestOptions,
);
}
},
);
}
SocketException
which you can catch as usual.You may have noticed something fishy going on in the code above. There's no return
statement! It's not as simple as returning the result of calling dio.request()
directly. After all, the request happens inside a closure of the listen
method and we want to return the Response
from the scheduleRequestRetry
method.
This is just the right occasion to use a Completer
. We can return its Future
from the whole scheduleRequestRetry
, which we will then complete from the closure.
dio_connectivity_request_retrier.dart
Future<Response> scheduleRequestRetry(RequestOptions requestOptions) async {
StreamSubscription streamSubscription;
final responseCompleter = Completer<Response>();
streamSubscription = connectivity.onConnectivityChanged.listen(
(connectivityResult) async {
if (connectivityResult != ConnectivityResult.none) {
streamSubscription.cancel();
// Complete the completer instead of returning
responseCompleter.complete(
dio.request(
requestOptions.path,
cancelToken: requestOptions.cancelToken,
data: requestOptions.data,
onReceiveProgress: requestOptions.onReceiveProgress,
onSendProgress: requestOptions.onSendProgress,
queryParameters: requestOptions.queryParameters,
options: requestOptions,
),
);
}
},
);
return responseCompleter.future;
}
Putting it all together
With this retrier class in place, we can now plug it into the interceptor.
retry_interceptor.dart
class RetryOnConnectionChangeInterceptor extends Interceptor {
final DioConnectivityRequestRetrier requestRetrier;
RetryOnConnectionChangeInterceptor({
@required this.requestRetrier,
});
@override
Future onError(DioError err) async {
if (_shouldRetry(err)) {
try {
return requestRetrier.scheduleRequestRetry(err.request);
} catch (e) {
// Let any new error from the retrier pass through
return e;
}
}
// Let the error pass through if it's not the error we're looking for
return err;
}
bool _shouldRetry(DioError err) {
return err.type == DioErrorType.DEFAULT &&
err.error != null &&
err.error is SocketException;
}
}
The main.dart file from the starter project contains a working code to perform GET requests with Dio
. All we need to do, is to plug the interceptor into it.
main.dart
class _HomePageState extends State<HomePage> {
Dio dio;
String firstPostTitle;
bool isLoading;
@override
void initState() {
super.initState();
dio = Dio();
firstPostTitle = 'Press the button 👇';
isLoading = false;
dio.interceptors.add(
RetryOnConnectionChangeInterceptor(
requestRetrier: DioConnectivityRequestRetrier(
dio: dio,
connectivity: Connectivity(),
),
),
);
}
...
}
And just like that, you now know how to retry requests automatically when device connection state changes. It's going to create a much better user experience than just passively saying "no connection, retry by pressing a button".
Thanks, Matt!
I had to place
streamSubscription.cancel();
directly after
responseCompleter.complete();
in order for it to work on my android device!
Cheers 😀
Not working
wonderful
if i have headers how how cam i add it requestOptions
How might you go about cancelling the retrier after it has activated, in case you don’t want it to continue watching for the change to happen (i.e. the user gets tired of waiting, or decides they will come back later).?
how do I get an error message for example error 201 ?
options: requestOptions,
is giving error saying
The argument type ‘RequestOptions’ can’t be assigned to the parameter type ‘Options’
Yes, there has been some breaking changes in dio 4.0.0 🙁
https://github.com/flutterchina/dio/issues/1078
Hi , This is not working in dio 4.0.0 .Can you give any suggestions how to make work this concept with dio 4.0.0.
I was able to make it work with dio 4.0.0 using the handler. You need to pass it to your scheduleRequestRetry function and resolve or pass the response of retry request. Something like this:
void scheduleRequestRetry(DioError error, ErrorInterceptorHandler handler) {
late StreamSubscription streamSubscription;
streamSubscription = connectivity.onConnectivityChanged.listen(
(connectivityResult) async {
if (connectivityResult != ConnectivityResult.none) {
streamSubscription.cancel();
try {
var response = await dio.fetch(error.requestOptions);
handler.resolve(response);
} on DioError catch (retryError) {
handler.next(retryError);
}
}
},
);
}
and the retry_interceptore becomes the following:
if (_shouldRetry(err)) {
return requestRetrier.scheduleRequestRetry(err, handler);
}
return super.onError(err, handler);
I am not sure this is the best way to do it but it’s working.