Architecting Network Layer in a flutter application (Scalability)

Harsh Vardhan Gautam
5 min readJan 29, 2023

--

If you’re here reading this article, it’s very likely that you already developed some application using flutter and now as every good software engineer should think, you also started thinking about scaling your existing flutter application. But soon you realised that while development, you did your best to keep code clean but somehow the networking code still doesn’t look good. It’s spreaded all across the codebase & it usually becomes the culprit whenever some changes in one part of the code are made that breaks the code written somewhere else in the codebase.

Don’t worry. It’s very common. But if we know some way to structure our application’s network layer in a scalable way, it will really be great achievement. And that too if we architect it right from the beginning, it’s will be cherry on the cake.

Without further ado, let’s devise an architecture that will help us to manage our network layer & at the same time, improves the scalability of the app.

The very first principle, while talking about maintaining the codebase & keeping it scalable, is separation of concern. One part of code i.e module (I’ll refer to module in the rest of the article) should be loosely coupled from the rest. For example, Payment module should be solely responsible for payment related stuff. It is not expected that Payment module should entertain any user profile related stuff.

Basically, we’ll apply this principle to develop our Network Layer. We will create our own separate module (particularly, a flutter package) named network_adapter. This will help us to utilise the capabilities of the network_adapter package in our application and we can maintain & scale our network_adapter in isolation as and when we want. It won’t break our main application.

Okay, let’s jump into some code.

We’ll start with creating our package using following command line provided from flutter team.

flutter create --template=package network_adapter

To know more about developing packages using flutter, see here.

We’re ready to start writing the code but before that I would like to mention the dependency that our package will be using.

network_adapter will be consuming a networking package named dio. It’s really a good package. We’ll be developing our package as a wrapper over this package.
So, before starting with the code, we’ll add dio as dependency in our package’s pubspec.yml file.

dio: ^4.0.6

Now, you’ll be having your lib/network_adapter.dart file ready. This file will server as the base for our library. See the below code which will reside in this file.

library network_adapter;

import 'package:dio/dio.dart';

export 'package:dio/dio.dart';

export 'api_controller.dart';

class NetworkAdapter {
late Dio client;

NetworkAdapter() {
client = Dio();
}
}

We’ve just created a very basic class named NetworkAdapter, instance of which gives us access to the underlying dio instance.

You must be thinking, what’s the point of creating this class, right? So, creating this class leaves the choice of underlying networking client (i.e. dio in this case) upto us. We can anytime change that dependency. And it won’t make any difference to the application which is consuming the network_adapter.

Let’s create one more file under lib directory named api_controller.dart. Dump the below code into this file. I’ll explain every line below.

import 'package:network_adapter/network_adapter.dart';

class APIController {
static List<Interceptor> interceptors = [];
static Map<String, dynamic> headers = {};
late NetworkAdapter _networkAdapter;
APIController(
{bool copyGlobalInterceptors = true, bool copyGlobalHeaders = true}) {
_networkAdapter = NetworkAdapter();
if (copyGlobalInterceptors) {
_networkAdapter.client.interceptors.addAll(interceptors);
}
if (copyGlobalHeaders) {
_networkAdapter.client.options.headers.addAll(headers);
}
}

Future<Response<T>> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onReceiveProgress,
}) {
return _networkAdapter.client.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onReceiveProgress: onReceiveProgress,
);
}

Future<Response<T>> post<T>(
String path, {
data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return _networkAdapter.client.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}

Future<Response<T>> put<T>(
String path, {
data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) {
return _networkAdapter.client.put(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
);
}

Future<Response<T>> delete<T>(
String path, {
data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) {
return _networkAdapter.client.delete(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
}
}

Calm down! It’s not that complicated as it looks. Let’s understand it. So we’ve created class named APIController which has three members named interceptors, headers and _networkAdapter. The interceptors and headers are kept static so that they can serve as global configuration for our APIController to instantiate the underlying NetworkAdapter accordingly.

You can have a look at the constructor of APIController to understand how we’re initialising the underlying NetworkAdapter instance based on the configuration. Keeping our network client flexible based on configuration is crucial for applications because based on various business logics, we face variety of situations where we want to configure our network client as per our convenience.

Rest of the methods named get, post, put and delete are just the wrapper over the dio client’s method. We’ve just encapsulated them so as to keep our library loosely coupled with the dio.

Now comes the part of using our network_adapter in our application. So to consume network_adapter in our application, we can either publish this on pub.dev or we can also link our local package. You can explore this on flutter’s official documentation.

We’ve added the library to our application. Now, in our application if we want to setup some global configuration for the network_adapter , we have to do this before we use the library anywhere in the code. This is really important !! For example, we can set global interceptors like below.

APIController.interceptors.add(LogInterceptor()); 

Now, to make a network call using our library, we can do something like this.

static APIController authApiController = APIController(copyGlobalHeaders: false, copyGlobalInterceptors: false);
await authApiController.get(<any-url-to-hit-will-go-here>);

That’s it.

We’re done. Give yourself a pat on back. I appreciate that you’ve taken some time out of your usual stuff to learn something new.

I hope that the article has added some value to your time. I would love to hear the feedback from the audience like you. It helps me improve.

Thank You!

--

--