In this section, we will encapsulate the network request interfaces used in our app based on the previously introduced Dio network library, while also applying a simple network request caching strategy. First, we will introduce the principles of network request caching, and then we will encapsulate the app's business request interfaces.
1 Network Interface Caching
Due to slow access to the GitHub server in China, we will implement some simple caching strategies: using the requested URL as the key, we will cache the response value for a specified duration and also set a maximum cache size. When the maximum cache size is exceeded, the oldest cache will be removed. However, we also need to provide a mechanism to determine whether caching should be enabled for specific interfaces or requests. This mechanism allows us to specify which interfaces or requests should not use caching; for example, the login interface should not be cached, and when a user performs a pull-to-refresh, caching should also not apply. Before implementing caching, we define a CacheObject
class to store cache information:
class CacheObject { CacheObject(this.response) : timeStamp = DateTime.now().millisecondsSinceEpoch; Response response; int timeStamp; // Cache creation time @override bool operator ==(other) { return response.hashCode == other.hashCode; } // Use the request URI as the cache key @override int get hashCode => response.realUri.hashCode; }
Next, we need to implement the specific caching strategy. Since we are using the Dio package, we can directly implement the caching strategy through an interceptor:
import 'dart:collection'; import 'package:dio/dio.dart'; import '../index.dart'; class CacheObject { CacheObject(this.response) : timeStamp = DateTime.now().millisecondsSinceEpoch; Response response; int timeStamp; @override bool operator ==(other) { return response.hashCode == other.hashCode; } @override int get hashCode => response.realUri.hashCode; } class NetCache extends Interceptor { // Use LinkedHashMap to ensure the order of insertion matches iteration order var cache = LinkedHashMap<String, CacheObject>(); @override onRequest(RequestOptions options, RequestInterceptorHandler handler) async { if (!Global.profile.cache!.enable) { return handler.next(options); } // refresh indicates if it is a "pull-to-refresh" bool refresh = options.extra["refresh"] == true; // If it's a pull-to-refresh, first remove related caches if (refresh) { if (options.extra["list"] == true) { // For lists, remove all caches containing the current path (simple implementation, not precise) cache.removeWhere((key, v) => key.contains(options.path)); } else { // If not a list, only remove caches with the same URI delete(options.uri.toString()); } return handler.next(options); } if (options.extra["noCache"] != true && options.method.toLowerCase() == 'get') { String key = options.extra["cacheKey"] ?? options.uri.toString(); var ob = cache[key]; if (ob != null) { // If the cache has not expired, return the cached content if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 < Global.profile.cache!.maxAge) { return handler.resolve(ob.response); } else { // If expired, delete the cache and continue requesting the server cache.remove(key); } } } handler.next(options); } @override onResponse(Response response, ResponseInterceptorHandler handler) async { // If caching is enabled, save the returned result to cache if (Global.profile.cache!.enable) { _saveCache(response); } handler.next(response); } _saveCache(Response object) { RequestOptions options = object.requestOptions; if (options.extra["noCache"] != true && options.method.toLowerCase() == "get") { // If the number of caches exceeds the maximum limit, remove the oldest record first if (cache.length == Global.profile.cache!.maxCount) { cache.remove(cache[cache.keys.first]); } String key = options.extra["cacheKey"] ?? options.uri.toString(); cache[key] = CacheObject(object); } } void delete(String key) { cache.remove(key); } }
The explanations of the code are in the comments. It is important to note that the options.extra
in the Dio package is specifically for extending request parameters. We defined two parameters, “refresh” and “noCache,” to implement the mechanism for deciding whether to enable caching for specific interfaces or requests. The meanings of these parameters are as follows:
Parameter Name | Type | Description |
---|---|---|
refresh | bool | If true, this request will not use the cache, but the new request result will still be cached. |
noCache | bool | This request disables caching; the request result will not be cached. |
2 Encapsulating Network Requests
A complete app may involve many network requests. To facilitate management and consolidate the request entry points, the best practice is to place all network requests in the same source file. Since our interfaces all call APIs provided by the GitHub development platform, we define a Git
class specifically for GitHub API calls. Additionally, during debugging, we often need tools to view network request and response packets. Using a network proxy tool to debug network data issues is the mainstream approach. Configuring the proxy requires specifying the address and port of the proxy server in the application, and since the GitHub API uses HTTPS, we should also disable certificate verification after configuring the proxy. These configurations are executed when initializing the Git
class (in the init()
method). Below is the source code for the Git
class:
import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:dio/adapter.dart'; import 'package:dio/dio.dart'; import '../index.dart'; export 'package:dio/dio.dart' show DioError; class Git { // During network requests, the current context information may be needed, such as opening a new route on request failure. Git([this.context]) { _options = Options(extra: {"context": context}); } BuildContext? context; late Options _options; static Dio dio = new Dio(BaseOptions( baseUrl: 'https://api.github.com/', headers: { HttpHeaders.acceptHeader: "application/vnd.github.squirrel-girl-preview," "application/vnd.github.symmetra-preview+json", }, )); static void init() { // Add cache plugin dio.interceptors.add(Global.netCache); // Set user token (may be null, representing not logged in) dio.options.headers[HttpHeaders.authorizationHeader] = Global.profile.token; // In debug mode, we need to capture and debug packets, so we use a proxy and disable HTTPS certificate verification if (!Global.isRelease) { (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { // client.findProxy = (uri) { // return 'PROXY 192.168.50.154:8888'; // }; // The proxy tool will provide a self-signed certificate for packet capture, which will fail certificate verification, so we disable it client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; }; } } // Login interface, returns user information upon successful login Future<User> login(String login, String pwd) async { String basic = 'Basic ' + base64.encode(utf8.encode('$login:$pwd')); var r = await dio.get( "/user", options: _options.copyWith(headers: { HttpHeaders.authorizationHeader: basic }, extra: { "noCache": true, // Disable caching for this interface }), ); // After a successful login, update the common header (authorization); all subsequent requests will carry user identity information dio.options.headers[HttpHeaders.authorizationHeader] = basic; // Clear all caches Global.netCache.cache.clear(); // Update the token information in the profile Global.profile.token = basic; return User.fromJson(r.data); } // Get user repositories Future<List<Repo>> getRepos({ Map<String, dynamic>? queryParameters, // Query parameters for pagination information refresh = false, }) async { if (refresh) { // List pull-to-refresh, need to delete cache (the interceptor will read this information) _options.extra!.addAll({"refresh": true, "list": true}); } var r = await dio.get<List>( "user/repos", queryParameters: queryParameters, options: _options, ); return r.data!.map((e) => Repo.fromJson(e)).toList(); } }
In the init()
method, we check whether the environment is in debug mode and perform some network configurations specific to the debug environment (such as setting a proxy and disabling certificate verification). The Git.init()
method is called during the application startup (it is invoked within the Global.init()
method).
Additionally, it is important to note that all our network requests are made through a single dio
instance (a static variable). When creating this dio
instance, we globally configured the base URL for the GitHub API and the headers supported by the API. As a result, all requests made through this dio
instance will use these configurations by default.
In this example, we only utilized the login interface and the interface for fetching user repositories. Therefore, the Git
class only defines the login(...)
and getRepos(...)
methods. If readers wish to expand the functionality based on this example, they can add other interface request methods to the Git
class, achieving centralized management and maintenance of network request interfaces at the code level.