DEV Community

huangli huang
huangli huang

Posted on

How to proxy Flutter's network requests to the native layer

Preface

Due to the company's product adopting a hybrid development model of Native + Flutter, for Android, network requests are handled through Android's OkHttp and Flutter's Dio respectively. However, this approach has some drawbacks:

    1. The process of network request needs to have two sets: one set for the native side and another set for Flutter. The processes I'm referring to here include mechanisms such as retry and caching. For example if i want to implement a function where the user is redirected to the login page when the token expires, I have to write two copies of the code.
    1. Using the Charles packet capture tool is a bit troublesome. We know that Flutter's Dio ignores the phone's proxy setting. To use a network proxy, it has to be configured in the code. However, it's different with native network components; using Charles for packet capture is very simple.

Based on the above two considerations, I want to entrust both the existing network requests and the newly added network requests in the current application to the native layer for handling network requests. Flutter only needs to parse the results of the network requests and display the UI.

After introducing the general background, we first need to understand Dio. Dio itself provides a way to customize network requests. The so-called customization here means implementing the sending and receiving of network requests by oneself.

Regarding the Dio network request process

First, we create a very simple get request.

Since there is a lot of code inside "get", only a few representative methods will be mentioned here.

The "get" will eventually call the fetch method in dio_mixin.dart. As the name suggests, this is where network requests are sent, and there is a _dispatchRequest inside the method.

Understanding the content within _dispatchRequest is crucial for our requirement because it is here that the actual sending of network requests(including those related to sockets) is completed. If we want to replace the actual network requests with native ones, we need to start from here.

Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
   //Focus 1
   final responseBody = await httpClientAdapter.fetch(
        reqOpt,
        stream,
        cancelToken?.whenCancel,
      )
   final headers = Headers.fromMap(responseBody.headers);
      // Make sure headers and responseBody.headers point to a same Map
      //Focus 2
      responseBody.headers = headers.map;
      final ret = Response<dynamic>(
        headers: headers,
        requestOptions: reqOpt,
        redirects: responseBody.redirects ?? [],
        isRedirect: responseBody.isRedirect,
        statusCode: responseBody.statusCode,
        statusMessage: responseBody.statusMessage,
        extra: responseBody.extra,
      );
  if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
        //Focus 3
        Object? data = await transformer.transformResponse(
          reqOpt,
          responseBody,
        );
        // Make the response as null before returned as JSON.
        if (data is String &&
            data.isEmpty &&
            T != dynamic &&
            T != String &&
            reqOpt.responseType == ResponseType.json) {
          data = null;
        }
        ret.data = data;
      }
}

Enter fullscreen mode Exit fullscreen mode

Three points worthy of attention are marked above, and these are the points i think are more worthy of attention.

  • Focus 1
    Obtain the responseBody object through httpClientAdapter. Here, i will elaborate on the role of httpClientAdapter later. For now, it can be understood that it internally implements the logic of HTTP network requests, and after execution, it will return a responseBody.

  • Focus 2
    Create a response object. In fact, responseBody is not yet the final request response to be returned, because it only represents the response when the network connection is established. It contains a stream, which is used to read the body returned by server after the socket connection is established. We need to obtain the content of the stream and then close the connection. Here, we first create the response object and assign some known response fields to it.

  • Focus 3
    As mentioned earlier, this is actually reading the content of the stream(through a transformer) and using it as the data content for our actual network response. This data is the body returned by the server, and the protocol contents such as code, msg, and json that we usually use in request results are all here.

At this point, we actually have a general idea of how to fulfill this requirement. If we want to proxy the actual network requests to the native side, we need to focus on replacing the network request process so that actual network requests occur on the native side and return a response. Here, dio supports providing custom httpClientAdapter and transformer to connect to the native side?

However, before starting the hands-on development, we must first clarify HttpClientAdapter and HttpClient.

HttpClientAdapter and HttpClient

HttpClientAdapter is a bridge between Dio and HttpClient.

Dio: it is used to implement standard and user-friendly API for developers.

HttpClient: it is used to actually send and receive network requests in Dio.

We can provide our own HttpClient through HttpClientAdapter instead of using the default one. HttpClientAdapter is actually an abstract class and does not provide specific implementations.

By default, the factory method here creates an IOHttpClientAdapter.

Looking back, there is a fetch method in HttpClientAdapter.

According to the comments, the fetch method is where the actual network request is sent. So how is it implemented in IOHttpClientAdapter? Here, I'll skip details and look at the key code. Its fetch method will call a _fetch.

_configHttpClient will be used to create an HttpClient, which is used to actually send and receive network requests in Dio.

Looking at the implementation, it is divided into two steps:
1, If i have the assignment of the createHttpClient, I directly use the createHttpClient method to create an HttpClient here.

2, If not, I directly call the constructor of HttpClient, and then what is created is _HttpClient.

_HttpClient is actually the default implementation of Http provided by Dart io. HttpClient is used to exchange data with the Http server, send Http requests to the Http server, receive responses, and also maintain some states, such as containing session cookies.

HttpClient involves sending HttpClientRequest to the Http server and then receiving HttpClientResponse. As for the internal details of HttpClient, I will not elaborate on them. It is nothing more than implementing the http protocol, which is not the focus of this article.

General Solution

In fact, understanding the flow of Dio, the idea becomes quite clear. Dio provides a method to customize the HttpClientAdapter. I have customized a NativeClientAdapter here, which overrides the implementation of fetch (the part where network request are sent). In fetch, instead of using the default http client, it directly sends the request parameters to the native side via method channel(method channel is one of the official was to communicate between native and Flutter). After the native side receives them, it sends the network request, return the response to Flutter, and then performs data conversion for the upper layer of Flutter.

Code implementation

class NativeNetDelegate {

  final dio = Dio();

  NativeNetDelegate(String gateway) {
    dio.httpClientAdapter = NativeClientAdapter();
    dio.transformer = NativeTransformer();
    dio.options.baseUrl = Base.baseUrl + gateway;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, I customized a Dio, using my own NativeClientAdapter and NativeTransformer. NativeTransformer is used to convert the data returned natively.

class NativeClientAdapter implements HttpClientAdapter {

  static const _tag = "NativeClientAdapter";

  /// Here, the fetch method is rewritten. It does not follow the implementation logic of the default http client, and directly sends the request parameters to the native for processing.
  @override
  Future<ResponseBody> fetch(
      RequestOptions options,
      Stream<Uint8List>? requestStream,
      Future<void>? cancelFuture,
      ) async {
    NativeRequestOption nativeRequestOption =
        NativeRequestOption().compose(options);
    vlog.d("$_tag fetch request $nativeRequestOption");
    dynamic result =
        await FlutterMethodHelper.instance.sendHttpRequest(nativeRequestOption.toJson());
    vlog.d("$_tag fetch response result $result");
    /// http code
    int httpCode = result['httpCode'];

    /// protocol json data
    String data = result['data'];

    return NativeResponseBody(
      fakeStream(),
      httpCode,
    )..data = data;
  }

  /// Because a proxy is used here, the data part of the response is returned directly, so there is no need to read data using a stream.
  Stream<Uint8List> fakeStream() async* {

  }

  /// Called when Dio is closed. Since network requests are forwarded to the native for processing, there is no need to release resources here.
  /// For details, refer to the [Dio] close method
  @override
  void close({bool force = false}) {

  }
}
Enter fullscreen mode Exit fullscreen mode
class NativeTransformer implements Transformer {

  static const String _tag = "NativeTransformer";

  ///Here, because it is proxied to the native, no Stream is used to read the content of the Request.
  @override
  Future<String> transformRequest(RequestOptions options) async {
    return "";
  }

  ///Here, directly return the protocol json returned by the original network request, which is the body of the response.
  @override
  Future<dynamic> transformResponse(RequestOptions options, ResponseBody responseBody) async {
    if (responseBody is NativeResponseBody) {
      if (responseBody.data != null) {
        /// convert this to json for upper-level use
        Map<String, dynamic> data = jsonDecode(responseBody.data!);
        vlog.d("$_tag $data");
        return data;
      } else {
        return "";
      }
    } else {
      throw DioException(
          requestOptions: options,
          message:
              "no support responseBody type, only support NativeResponseBody in NativeTransformer");
    }
  }
}

class NativeResponseBody extends ResponseBody {
  String? data;
  NativeResponseBody(super.stream, super.statusCode);
}
Enter fullscreen mode Exit fullscreen mode

In the previous code, we used a NativeRequestOption, which is actually a protocol content class. We can forward this object to the native layer to parse and send network requests.

class NativeRequestOption {

  String? baseUrl;
  String? path;
  String? method;
  String? data;
  Map<String, dynamic>? queryParameters;

  NativeRequestOption();

  factory NativeRequestOption.fromJson(Map<String, dynamic> json) => _$NativeRequestOptionFromJson(json);

  Map<String, dynamic> toJson() => _$NativeRequestOptionToJson(this);

  NativeRequestOption compose(RequestOptions options) {
    baseUrl = options.baseUrl;
    path = options.path;
    method = options.method;
    data = options.data?.toString();
    queryParameters = options.queryParameters;
    return this;
  }

  @override
  String toString() {
    return 'NativeRequestOption{baseUrl: $baseUrl, path: $path, method: $method, data: $data, queryParameters: $queryParameters}';
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, I won't paste the code of the native layer. It's nothing more than sending a network request after receiving the NativeRequestOption, and then sending the network result back to Flutter for parsing into a structure.

{
“httpCode“ : 1001
“data” : Content of business agreement(i.e., code, data, msg)
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The most important thing about this article is to provide an idea. The code implementation varies form person to person and from requirement to requirement. You can customize the HttpClientAdapter according to your own needs. Of course, Dio also provides an interceptor function. It is also feasible for us to intercept requests directly in the interceptor and forward them to the native side, but we will not discuss this scheme here. if you find it useful, don't forget to thumb up, comment and collect it.

Top comments (0)