Flutter Tutorial - Dio Connectivity Retry Interceptor

Flutter Tutorial - Dio Connectivity Retry Interceptor

We're going to use the dio HTTP client and the connectivity package to accomplish our goal. 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.

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?

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.

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;
}

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,
        );
      }
    },
  );
}

The connectivity package does not guarantee that the user is actually connected to the world-wide web. The user may just happen to use a WiFi network without any Internet access. That's not a problem though as in that case, the retry will again fail with a 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".

flutter mobileapps webdev

What is Geek Coin

What is GeekCash, Geek Token

Best Visual Studio Code Themes of 2021

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Google's Flutter 1.20 stable announced with new features - Navoki

Google has announced new flutter 1.20 stable with many improvements, and features, enabling flutter for Desktop and Web

What is Flutter and why you should learn it?

Flutter is an open-source UI toolkit for mobile developers, so they can use it to build native-looking Android and iOS applications from the same code base for both platforms. Flutter is also working to make Flutter apps for Web, PWA (progressive Web-App) and Desktop platform (Windows,macOS,Linux).

Adobe XD plugin for Flutter with CodePen Tutorial

Recently Adobe XD releases a new version of the plugin that you can use to export designs directly into flutter widgets or screens.

Flutter - How to Add AdMob Real Ads in Flutter App | Flutter AdMob Tutorial

Hello Whats is up Everyone So, Today I am going to show u How to Add Admob Real ads in Flutter apps which are very Easy Implement After watching this video u...

Flutter App Development Trends 2020

As the new decade dawns upon us, a slew of technologies has been making a lot of noise to grab the developers’ attention. While native app development is going strong, the trade winds are now blowing towards going cross-platform.