Transforming Flutter Networking with network_plus
In the rapidly evolving landscape of mobile app development, effective networking is a cornerstone of delivering seamless user experiences. As Flutter gains traction, developers often face challenges related to API interactions. Today, I’m excited to introduce network_plus, a powerful networking package designed to address these common pain points.
The Pain Points of Networking in Flutter
- Complexity of HTTP Requests: Making HTTP requests in Flutter can become cumbersome, especially as applications grow. Developers often end up with repetitive boilerplate code for handling requests, responses, and error management.
- Inefficient Data Management: Without proper caching mechanisms, applications frequently make unnecessary network calls, leading to increased latency and diminished user experience.
- Token Management: Managing access and refresh tokens is a critical but often complicated task. Developers find themselves implementing token authentication from scratch, which can lead to security vulnerabilities if not done correctly.
- Logging and Debugging: Troubleshooting network issues can be challenging without robust logging. Developers need a straightforward way to track requests, responses, and errors.
- Local Data Storage: Many applications require local data storage for offline capabilities or to enhance performance. Managing local storage effectively is often overlooked, leading to data inconsistency.
Introducing network_plus
The network_plus
package is specifically designed to alleviate these pain points. Built on top of the DIO library, it streamlines networking, making it easier for developers to focus on building features rather than dealing with complexities.
Key Features
- DIO Wrapper: Simplifies HTTP requests and responses, reducing boilerplate code.
- Caching Mechanisms: Built-in caching capabilities enhance performance by minimizing unnecessary API calls.
- Token Authentication: The package automates the management of access and refresh tokens, providing a robust solution for token handling.
- Authentication Expiry and Refresh Tokens: The
AuthTokenInterceptor
effectively handles token expiry, ensuring that users remain authenticated without requiring them to log in repeatedly. When an access token expires, the interceptor automatically attempts to refresh it using a stored refresh token, streamlining the user experience. Here's how it works:
class AuthTokenInterceptor extends Interceptor {
final Dio dio;
final String refreshTokenKey;
final String accessTokenKey;
final String refreshTokenUrl;
Completer<void>? _completer;
TokenStorage tokenStorage;
List<Map<dynamic, dynamic>> failedRequests = [];
/// Constructs a new instance of [AuthTokenInterceptor].
///
/// [dio]: The Dio client instance.
/// [refreshTokenKey]: The key to access the refresh token.
/// [refreshTokenUrl]: The URL for refreshing the access token.
AuthTokenInterceptor(this.dio, this.refreshTokenKey, this.accessTokenKey, this.refreshTokenUrl , this.tokenStorage);
@override
Future<void> onRequest(
RequestOptions options, RequestInterceptorHandler handler) async {
// Log.i(
// 'Network : TokenInterceptor REQUEST[${options.method}] => PATH: ${options.path}');
String? token = await tokenStorage.getUserToken();
if (token != null && token.isNotEmpty) {
Log.i('Network : TokenInterceptor token is $token');
options.headers['Authorization'] = 'Bearer $token';
}
return super.onRequest(options, handler);
}
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401 && refreshTokenUrl.isNotEmpty) {
Log.e("Network TokenInterceptor: token expired");
if (_completer == null) {
Log.e("ACCESS TOKEN EXPIRED, GETTING NEW TOKEN PAIR");
_completer = Completer<void>();
await refreshToken(err, handler);
_completer!.complete();
_completer = null;
} else {
///Refresh token in progess , add all incoming calls to request Queue
Log.e("ADDING ERROR REQUEST TO FAILED QUEUE");
failedRequests.add({'err': err, 'handler': handler});
}
} else {
/// some other error that isnt related to token expiry, pass this to next interceptor
return super.onError(err, handler);
}
}
/// Performs the token refresh operation.
/// [err]: The Dio error that triggered the token refresh.
/// [handler]: The error interceptor handler.
Future<void> refreshToken(
DioException err, ErrorInterceptorHandler handler) async {
//to avoid DeadLock using new DIO instanace https://github.com/cfug/dio/issues/1612
Dio tokenDio = Dio(dio.options);
tokenDio.interceptors.add(ErrorInterceptors(tokenDio));
tokenDio.interceptors.add(LogInterceptor());
String? userToken = await tokenStorage.getUserToken();
String? refreshToKen = await tokenStorage.getRefreshToken();
Log.i("CALLING REFRESH TOKEN API");
Response? refreshResponse = null;
try {
try {
refreshResponse = await tokenDio.post(refreshTokenUrl,
options: Options(headers: {'Authorization': 'Bearer $userToken'}),
data: jsonEncode({this.refreshTokenKey : refreshToKen}));
} on DioException catch (e) {
/// This will return a error handled from [ErrorInterceptors]
Log.e("REFRESH TOKEN FAILED FAILED received + $e");
/// calling site should logout the user
tokenStorage.clearTokens();
}
Log.i(
"REFRESH TOKEN RESPONSE STATUS ${refreshResponse?.statusCode}");
if (refreshResponse?.statusCode == 200 ||
refreshResponse?.statusCode == 201) {
///TODO : "access_token" this key names shouild be set up calling site
///
userToken = refreshResponse?.data[this.accessTokenKey];
refreshToKen = refreshResponse?.data[refreshTokenKey];
// parse data based on your JSON structure
await tokenStorage.setUserToken(userToken.orEmpty()); // save to local storage
await tokenStorage.setRefreshToken(
refreshToKen.orEmpty()); // save to local storage
Log.i(
"Network TokenInterceptor refreshed successfully ${refreshResponse
?.statusCode} \n new token $userToken");
/// Update the request header with the new access token
err.requestOptions.headers['Authorization'] = 'Bearer ${userToken.orEmpty()}';
} else if (refreshResponse?.statusCode == 401 ||
refreshResponse?.statusCode == 403) {
/// it means your refresh token no longer valid now, it may be revoked by the backend
tokenStorage.clearTokens();
Log.e(
"Network TokenInterceptor refresh token no longer valid now ${refreshResponse
?.statusCode}");
/// Complete the request with a error directly! Other error interceptor(s) will not be executed.
// return handler.next(err);
} else {
Log.i(
"Network refresh token else case with status code ${refreshResponse
?.statusCode}");
}
}
catch(error)
{
Log.e("Network caught some exception status $error");
}
/// Repeat the request with the updated header
/// Complete the request with Response object and other error interceptor(s) will not be executed.
/// This will be considered a successful request!
///
Log.i(
"Network refresh resolving now with ${err.requestOptions.data}");
Log.i("Network retrying status");
failedRequests.add({'err': err, 'handler': handler});
Log.i("RETRYING TOTAL OF ${failedRequests.length} FAILED REQUEST(s)");
return retryRequests(tokenDio,userToken.orEmpty())
.ignore();
}
/// Retries failed requests with the updated access token.
///
/// [retryDio]: The Dio client instance for retrying requests.
/// [token]: The updated access token.
Future<void> retryRequests(Dio retryDio, String token) async {
for (var i = 0; i < failedRequests.length; i++) {
Log.i(
'RETRYING[$i] => PATH: ${failedRequests[i]['err'].requestOptions.path}');
RequestOptions requestOptions =
failedRequests[i]['err'].requestOptions as RequestOptions;
requestOptions.headers = {
'Authorization': 'Bearer $token',
};
await retryDio.fetch(requestOptions).then(
failedRequests[i]['handler'].resolve,
onError: (error) =>
failedRequests[i]['handler'].reject(error as DioException),
);
}
failedRequests = [];
}
}
- Comprehensive Logging with cURL Generation: Another standout feature is the
LogInterceptor
, which provides detailed logs for network requests and responses. It generates cURL commands, enabling developers to easily replicate API calls in the terminal for testing and debugging. This feature saves significant time and effort when diagnosing API issues.
Getting Started
Integrating network_plus
into your Flutter project is simple. Just add the following dependency to your pubspec.yaml
:
dependencies:
network_plus: ^0.0.1
Example Setup
Here’s how you can set up your project with network_plus
:
// Setup Core DI
coreDILocator.registerLazySingleton<CoreConfiguration>(() => CoreConfiguration(
baseUrl: coreDILocator<AppEnvironment>().brandConfig.url.orEmpty(),
timeout: 120000,
connectTimeout: 120000,
cachePolicy: coreDILocator<AppEnvironment>().env == EnvironmentType.prod ||
coreDILocator<AppEnvironment>().env == EnvironmentType.dev
? CachePolicy.request
: CachePolicy.noCache,
refreshTokenUrl: authUrl,
refreshTokenKey: "refresh_token",
accessTokenKey: "access_token",
securityContext: _securityContext,
additionalHeaders: {
"locale": "en",
"unique-reference-code": "GUID",
},
storageProviderForToken: StorageProvider.sharedPref,
loggerConfig: const LoggerConfig(
shouldShowLogs: kDebugMode,
logLevel: LogsLevel.trace,
lineLength: 1000,
)
));
// Retrieve the CoreConfiguration instance
final core = coreDILocator<CoreConfiguration>();
core.setup();
Creating a Custom Repository
One of the package’s strengths is its flexibility. You can easily create custom repositories by extending the BaseRepository
class, tailored to your app's needs. Here's an example:
class MyCustomRepository extends BaseRepository<GlobalMasterConfigData> {
MyCustomRepository(NetworkExecutor networkExecutor) : super(networkExecutor);
Future<Result<MyUiModel>> fetchData() async {
final additionalHeaders = {
'Authorization': 'Bearer your_token',
};
return await execute<MyMapper, GlobalMasterConfigData, MyUiModel>(
urlPath: '/api/data',
method: METHOD_TYPE.GET,
params: EmptyRequest(),
mapper: MyMapper(),
responseType: GlobalMasterConfigData(),
headers: additionalHeaders,
cachePolicy: CachePolicy.cacheFirst,
retryPolicy: RetryPolicy(retrialCount: 3, retryDelay: Duration(seconds: 2)),
);
}
}
Conclusion
As developers, we face numerous challenges when building applications. The network_plus
package addresses these pain points by providing a robust, user-friendly solution for networking in Flutter. Whether you're dealing with complex API interactions, authentication expiry, logging, or local storage, network_plus
simplifies the process.
You can find the package on pub.dev and explore the source code on GitHub. I encourage you to try network_plus
in your next Flutter project and experience the difference it can make.
If you have any suggestions, issues, or feature requests, feel free to reach out on GitHub. Let’s enhance the Flutter networking experience together!