Clean Flutter Network Architecture Using Dio (2022) | Part 1— Dio Service

Abdur Rafay Saleem
Flutter App Development
4 min readJul 4, 2022

Missed out on previous part? Check out Part 0 — Introduction

Flutter Dio Networking Architecture
Flutter Dio Networking Architecture

Dio Service

First, we will create a wrapper class around the Dio package called dio_service.dart

It will contain 3 variables:

class DioService {/// An instance of [Dio] for executing network requests.
final Dio _dio;
/// A set of cache options to be used for each request.
final CacheOptions? globalCacheOptions;
/// An instance of [CancelToken] used to pre-maturely cancel
/// network requests.
final CancelToken _cancelToken;

Next, we will create a constructor to initialize these variables:

/// A public constructor that is used to create a Dio service and initialize the underlying [Dio] client.
///
/// * [interceptors]: An [Iterable] for attaching custom
/// [Interceptor]s to the underlying [_dio] client.
/// * [httpClientAdapter]: Replaces the underlying
/// [HttpClientAdapter] with this custom one.
DioService({
required Dio dioClient,
this.globalCacheOptions,
Iterable<Interceptor>? interceptors,
HttpClientAdapter? httpClientAdapter,
}) : _dio = dioClient,
_cancelToken = CancelToken() {
if (interceptors != null) _dio.interceptors.addAll(interceptors);
if (httpClientAdapter != null)
_dio.httpClientAdapter = httpClientAdapter;
}

This constructor takes in:

  • Dio Object: Used to combine network functionality and make requests.
  • CacheOptions(Optional): Used to set decide a caching strategy.
  • List of Interceptors(Optional): These are added to the Dio object and are used for purposes like logging, refreshing token and error handling.
  • HttpClientAdapter (Optional): This replaces the underlying adapter for Dio. It is used to force usage of HTTP 2.0 protocol.

The HTTP methods

Now let’s move on to implementing the methods GET, POST, PATCH, DELETE. Take a look at this file:

Future<ResponseModel<R>> get<R>({
required String endpoint,
JSON? queryParams,
Options? options,
CacheOptions? cacheOptions,
CancelToken? cancelToken,
}) async {
final response = await _dio.get<JSON>(
endpoint,
queryParameters: queryParams,
options: _mergeDioAndCacheOptions(
dioOptions: options,
cacheOptions: cacheOptions,
),
cancelToken: cancelToken ?? _cancelToken,
);
return ResponseModel<R>.fromJson(response.data!);
}
Future<ResponseModel<R>> post<R>({
required String endpoint,
JSON? data,
Options? options,
CancelToken? cancelToken,
}) async {
final response = await _dio.post<JSON>(
endpoint,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
return ResponseModel<R>.fromJson(response.data!);
}
Future<ResponseModel<R>> patch<R>({
required String endpoint,
JSON? data,
Options? options,
CancelToken? cancelToken,
}) async {
final response = await _dio.patch<JSON>(
endpoint,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
return ResponseModel<R>.fromJson(response.data!);
}
Future<ResponseModel<R>> delete<R>({
required String endpoint,
JSON? data,
Options? options,
CancelToken? cancelToken,
}) async {
final response = await _dio.delete<JSON>(
endpoint,
data: data,
options: options,
cancelToken: cancelToken ?? _cancelToken,
);
return ResponseModel<R>.fromJson(response.data!);
}

Each of these methods except GET contains the following common parameters:

  • String endpoint: The path after the baseUrl.
  • JSON data: The request body that needs to be sent.
  • Options options: Custom request options that are used to add headers, set custom timeouts, etc. for this specific request.
  • CancelToken? cancelToken: Used to cancel this specific request only, if not set, then the global cancelToken will be passed. The global one cancels all requests.

The GET method contains the following different parameters to customize querying and caching:

  • JSON queryParams: Instead of data it uses a set of query params that are added to the url internally.
  • CacheOptions? cacheOptions: This decides the caching strategy for this specific request and merges it with the global one and overrides the common settings.
  • Since dio required an Options object only, the cache options are merged with the passed Options using the method _mergeDioAndCacheOptions:
/// A utility method used to merge together [Options]
/// and [CacheOptions].
///
/// Returns an [Options] object with [CacheOptions] stored
/// in the [options.extra] key.
Options? _mergeDioAndCacheOptions({
Options? dioOptions,
CacheOptions? cacheOptions,
}) {
if (dioOptions == null && cacheOptions == null) {
return null;
} else if (dioOptions == null && cacheOptions != null) {
return cacheOptions.toOptions();
} else if (dioOptions != null && cacheOptions == null) {
return dioOptions;
}
final _cacheOptionsMap = cacheOptions!.toExtra();
final options = dioOptions!.copyWith(
extra: <String, dynamic>{...dioOptions.extra!, ..._cacheOptionsMap},
);
return options;
}

This is how the entire file looks like:

Creating an Instance

Finally, this is how you create instance of this service:

final baseOptions = BaseOptions(
baseUrl: "www.sample-url.com",
);
final _cacheOptions = CacheOptions(
store: HiveCacheStore(pp.getApplicationDocumentsDirectory()),
policy: CachePolicy.noCache, // Bcz we force cache on-demand in repositories
maxStale: const Duration(days: 30), // No of days cache is valid
keyBuilder: (options) => options.path,
);
DioService(
dioClient: _dio,
globalCacheOptions: _cacheOptions,
interceptors: [
// Order of interceptors very important
ApiInterceptor(),
DioCacheInterceptor(options: _cacheOptions),
if (kDebugMode) LoggingInterceptor(),
RefreshTokenInterceptor()
],
);

The HiveCacheStore comes from the package flutter cache_manager_hive and pp comes from path_provider. You can use your choice of dependency injection service to provide this service to all layers.

Summary

We have built a highly proficient and generic dio service with internal support for caching and interceptors. However, this is just the first step in building our network layer and in the next part, we will see how to use this service effectively in combination with other parts.

I encourage you guys to share your views regarding this article and help me improve on any lacking areas. If you liked this article, then please clap on this and share it with your network.

Read on Part 2: Custom Exception and Response Model

--

--

Abdur Rafay Saleem
Flutter App Development

Flutter enthusiast by day, flutter enthusiast by night. A passionate computer science student with a focus on learning new technologies.