Flutter Dio Complete Guide

Created At: 2023-02-23 19:34:54 Updated At: 2023-09-04 09:18:08

Flutter networking could be done using Dio package. Dio package provides many convenient methods for http network request and handling response and errors. The official document says

A powerful HTTP client for Dart/Flutter, which supports global configuration, interceptors, FormData, request cancellation, file uploading/downloading, timeout, and custom adapters etc.

In my project chat app I have used all the features and here I will explain some of the core functionalities of the package. Visit this Flutter Riverpod app with backend.

Here we will cover get(), post(), dio interceptors.

Install & Example

Make sure you have installed the package using the command below

flutter pub add dio

Very simple example of it

import 'package:dio/dio.dart';

void main() async {
  final dio = Dio();
  final response = await dio.get('https://pub.dev');
  print(response.data);
}

That should work but it's too simple. We don't know what's inside response object and we also did not configure Dio(). 

If we did not configure Dio will use the default configuration.

Wrapper class

Just like http.get request of http package, Dio http works the same way. We will create a special class, this class would contain all of the Dio methods and other helper function.

We will call this class HttpUtil, and it would be a Singleton class. Singleton class it means we will only create one instance of this.

class HttpUtil {
  static final HttpUtil _instance = HttpUtil._internal();
  factory HttpUtil() => _instance;

}

This Singleton class would contain all of our Dio methods. Since it's a Singleton class, we have a factory constructor to return instance of this class. In our case, it is _instance.

Init Dio

Let's create a new Dio instance inside the class as a late variable.

 late Dio dio;

This variable dio, we will assign an instance later once we are done with BaseOptions.

BaseOptions

We also have to set BaseOptions constructor using various parameters like

  1. baseUrl
  2. connectionTimeout
  3. receiveTimeout
  4. headers
  5. contentType
  6. responseType
 BaseOptions options =  BaseOptions(
      baseUrl: AppConstants.SERVER_API_URL,


      connectTimeout: const Duration(seconds: 5),

      receiveTimeout: const Duration(seconds: 5),

      headers: {},


      contentType: 'application/json; charset=utf-8',

      responseType: ResponseType.json,
    )

BaseOptions() works for configuration setup, and these are global set up. Make sure you set up SERVER_API_URL correctly. You may simply set up your domain name or IP address for baseUrl. If you use localhost make sure you do 127.0.0.1 for iOS and 10.0.2.2 for Android devices.

You may check this link for better configuration of IP address.

After that actually we are ready to use Dio methods like get() and post(). Let's see the class.

class HttpUtil {
  static final HttpUtil _instance = HttpUtil._internal();
  factory HttpUtil() => _instance;

  late Dio dio;

  HttpUtil._internal() {
   BaseOptions options =  BaseOptions(
      baseUrl: AppConstants.SERVER_API_URL,


      connectTimeout: const Duration(seconds: 5),

      receiveTimeout: const Duration(seconds: 5),

      headers: {},


      contentType: 'application/json; charset=utf-8',

      responseType: ResponseType.json,
    )


    dio = Dio(options);

    /**
    We will add interceptors here
   **/
}

We created a Singleton class of HttpUtil class, so that, we will have only one instance of it, through out of our app and it's lifecycle. 

Creatting multiple instance of it would create a problem. We have followed a basic pattern of creating _internal(), factory constructor, and body of internal().

dio get()

After that, we can set up our http get() method. For Dio package users we will see dio.get() for get() request. 

    var response = await dio.get(
      path,
      queryParameters: queryParameters,
      options: options,
    );
    return response.data;

After that, we return a response. It's a JSON response. It's upto you how much information you return from your sever side. Look at my server side repsonse

 return ["code"=>0, 'data'=>$res, "msg"=>"got all the users info"];

Here you see, I return many fields in the JSON reponse. But my data field only contains the result I want. You may return as many fields as you want. I can do 

print("${response.data['data']}");

And use the response in your front end Flutter app. 

Inside dio.get() method, only path parameter is the compulsory one. Others are optional.  We may use a wrapper for dio.get() , that wrapper would help you with the optional parameters.

  Future get(
    String path, {
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    Options requestOptions = options ?? Options();
    var response = await dio.get(
      path,
      queryParameters: queryParameters,
      options: options,
    );
    return response.data;
  }

From your UI, you may call HttpUtil().get() with required parameter path, if you don't know what else to send, skip others. 

So the get method sends an HTTP GET request to the server with the specified path and query parameters. It also allows you to pass additional options such as headers, response type, and timeout. The method returns a Future object that resolves with a Response object containing the HTTP response from the server. 

In general your server response is inside response.data object.

dio.post()

The post method of Dio is more like dio.get(). Everything else stays the same, but we need to provide headers info and data to send to the server.

Most of the post method requires sending special kind of token to send to server. We will send that in a header. First let's work on method that would find the token from the local device and then return that header. The returned header we will use in dio.post() method.

  Map<String, dynamic>? getAuthorizationHeader() {
    var headers = <String, dynamic>{};
    if (Get.isRegistered<UserStore>() && UserStore.to.hasToken == true) {
      headers['Authorization'] = 'Bearer ${UserStore.to.token}';
    }
    return headers;
  }

Here in the method we see that we have a condition and in this we use UserStore.to.hasToken to check if the device has saved any token or not. 

If there's a token then we save in headers map and return the map.

We also need to send data to server. dio.post() takes dynamic data type that means we can send Map data to the post method.

 var response = await dio.post(
      path,
      data: data,
      queryParameters: queryParameters,
      options: requestOptions,
    );

Now let's the the complete post method

  Future post(
    String path, {
    dynamic data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {

    Options requestOptions = options ?? Options();
    requestOptions.headers = requestOptions.headers ?? {};
    Map<String, dynamic>? authorization = getAuthorizationHeader();
    if (authorization != null) {
      requestOptions.headers!.addAll(authorization);
    }
    var response = await dio.post(
      path,
      data: data,
      queryParameters: queryParameters,
      options: requestOptions,
    );

    return response.data;
  }

So the post method is similar to the get method but with the addition of a data parameter, which represents the request body. The request headers are obtained using the getAuthorizationHeader method and merged with any options provided. The request is made using dio.post and the response data is returned if successful. If the request fails due to a Dio error, an ErrorEntity is thrown.

dioHttpClientAdapter

Dio http client adapter is used to verify client side and server side certificate authentication. In general most client needs ssl certificates from the server to verify with the clients, that the server is real and authenticate.

 (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
        (HttpClient client) {
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
      return client;
    };

You may put the above code in the body HttpUtil._internal(){} method after dio = Dio(options);

This code sets a callback function that is triggered when Dio creates a new HttpClient instance.

The purpose of this callback is to configure the HttpClient to ignore SSL/TLS certificate errors. By default, HttpClient performs strict certificate validation, which can cause requests to fail if the server's certificate is not trusted or does not match the expected hostname. However, in some cases (e.g. when testing against a self-signed certificate), it may be desirable to ignore certificate errors and proceed with the request anyway.

The callback function takes three arguments: the server's certificate, the hostname being connected to, and the port being used. In this case, the function simply returns true, indicating that all certificate errors should be ignored. However, you could modify this function to perform more specific validation, if desired.

Handle Error With Dio

To handle error with Dio we need to set up it with interceptors. Because of interceptors, we can deal with the request before we send to server, we can deal with the response once we get correct response from the server and as well as if we get errors from the server.

Without interceptors, we can still use Dio, but it's not optimized for catching and handling the errors and observing request and response. If we use interceptor then our Singleton class constructor would look like below

  HttpUtil._internal() {
    BaseOptions options =  BaseOptions(
      baseUrl: AppConstants.SERVER_API_URL,


      connectTimeout: const Duration(seconds: 5),

      receiveTimeout: const Duration(seconds: 5),

      headers: {},


      contentType: 'application/json; charset=utf-8',

      responseType: ResponseType.json,
    );

    dio =  Dio(options);

    (dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
        (HttpClient client) {
      client.badCertificateCallback =
          (X509Certificate cert, String host, int port) => true;
      return client;
    };

    dio.interceptors.add(InterceptorsWrapper(
      onRequest: (options, handler) {
        // Do something before request is sent
        return handler.next(options); //continue

      },
      onResponse: (response, handler) {
        // Do something with response data
        return handler.next(response); // continue

      },
      onError: (DioError e, handler) {

        ErrorEntity eInfo = createErrorEntity(e);
        onError(eInfo);
        return handler.next(e); //continue

      },
    ));
  }

From the code above, you see we have onRequest, onResponse and onError property which are available inside InterceptorsWrapper. Each of them takes a call back function and gets triggered when certain things happens. 

InterceptorsWrapper makes it possible to observe or intercept everything that is API request.

You may put logs between each of them and you will see that, they all triggered at correct time.

Here you may see that we can print options.data, response.data and e for error printing.

=====> Now, if you do any network request, onRequest section would get called. 

=====> For any response the network onResponse would get called.

=====> For any errors onError would get called.

For us the most important section is catching errors using onError property. 

Complete code

class HttpUtil {
  late Dio dio;

  static final HttpUtil _instance = HttpUtil._internal();

  factory HttpUtil() {
    return _instance;
  }

  HttpUtil._internal() {
    BaseOptions options = BaseOptions(
        baseUrl: AppConstants.SERVER_API_URL,
        connectTimeout: const Duration(seconds: 5),
        receiveTimeout: const Duration(seconds: 5),
        headers: {},
        contentType: "application/json: charset=utf-8",
        responseType: ResponseType.json);
    dio = Dio(options);

    dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
     // print("app request data ${options.data}");
      return handler.next(options);
    }, onResponse: (response, handler) {
      if (kDebugMode) {
        print("app response data ${response.data}");
      }
      return handler.next(response);
    }, onError: (DioException e, handler) {
      if (kDebugMode) {
        print("app error data $e");
      }
      ErrorEntity eInfo = createErrorEntity(e);
      onError(eInfo);
    }));
  } //finish internal()

  Map<String, dynamic>? getAuthorizationHeader() {
    var headers = <String, dynamic>{};
    var accessToken = Global.storageService.getUserToken();
    if (accessToken.isNotEmpty) {
      headers['Authorization'] = 'Bearer $accessToken';
    }
    return headers;
  }

  Future post(
    String path, {
    Object? data,
    Map<String, dynamic>? queryParameters,
    Options? options,
  }) async {
    Options requestOptions = options ?? Options();
    requestOptions.headers = requestOptions.headers ?? {};

    Map<String, dynamic>? authorization = getAuthorizationHeader();

    if (authorization != null) {
      requestOptions.headers!.addAll(authorization);
    }

    var response = await dio.post(path,
        data: data, queryParameters: queryParameters, options: requestOptions);

    return response.data;
  }
}

class ErrorEntity implements Exception {
  int code = -1;
  String message = "";

  ErrorEntity({required this.code, required this.message});

  @override
  String toString() {
    if (message == "") return "Exception";

    return "Exception code $code, $message";
  }
}

ErrorEntity createErrorEntity(DioException error){
  switch(error.type){
    case DioExceptionType.connectionTimeout:
      return ErrorEntity(code: -1, message: "Connection timed out");

    case DioExceptionType.sendTimeout:
      return ErrorEntity(code: -1, message: "Send timed out");

    case DioExceptionType.receiveTimeout:
      return ErrorEntity(code: -1, message: "Receive timed out");

    case DioExceptionType.badCertificate:
      return ErrorEntity(code: -1, message: "Bad SSL certificates");

    case DioExceptionType.badResponse:

      switch(error.response!.statusCode){
        case 400:
          return ErrorEntity(code: 400, message: "Bad request");
        case 401:
          return ErrorEntity(code: 401, message: "Permission denied");
        case 500:
          return ErrorEntity(code: 500, message: "Server internal error");
      }
      return ErrorEntity(code: error.response!.statusCode!, message: "Server bad response");

    case DioExceptionType.cancel:
    return ErrorEntity(code: -1, message: "Server canceled it");

    case DioExceptionType.connectionError:
      return ErrorEntity(code: -1, message: "Connection error");

    case DioExceptionType.unknown:
      return ErrorEntity(code: -1, message: "Unknown error");
  }
}

void onError(ErrorEntity eInfo){
  print('error.code -> ${eInfo.code}, error.message -> ${eInfo.message}');
  switch(eInfo.code){
    case 400:
      print("Server syntax error");
      break;
    case 401:
      print("You are denied to continue");
      break;
    case 500:
      print("Server internal error");
      break;
    default:
      print("Unknown error");
      break;

  }
}

In the above code ErrorEntity to create an object from the caught error and print them during log using dot operator.

The method createErrorEntity() is the actual method, that catches the exact error. We have created each of the possible error types and catch them and pass to ErrorEntity.

Hit End Point With Dio

Here you will learn how to quickly or easily hit or call rest api from flutter app. Actually it's same for web application or react native applications that uses Dio as a package for http protocol. 

All you need to do is that, on a button click you need to call dio.post() or dio.get() method. Make sure inside the method you have your baseUrl or endpoint correctly set up.

I am gonna do it like below

  Future post(String path,
      {dynamic data, Map<String, dynamic>? queryParameters}) async {
    var response =
        await dio.post(path, data: data, queryParameters: queryParameters);

    print("my response is ${response.toString()}");
    print("my status code is ${response.statusCode}");
    return response.data;
  }

Your end point could be in Java, Php, Python related framework. But they all return 200 for successful response from the server.

We will do the same thing. In my case I am using the code below

public function login(){

  return response()->json([
                    'status' => true,
                    'msg' => 'hit end point',
                    'data' => "dummy data"
            ], 200);

......................................................
......................................................

}

In my end point, the method returns the above syntax like that.

..........coming more..........

Comment

Add Reviews