likes
comments
collection
share

如何把Flutter的网络请求代理到原生层进行

作者站长头像
站长
· 阅读数 59

前言

由于公司产品是原生+Flutter的混合开发模式,对于安卓来说网络请求分别走的Android的Okhttp和Flutter的Dio。但是这个做法会存在一些弊端:

1,网络请求的流程需要保证两套,原生一套流程,Flutter一套流程,我这里指的流程是指例如重试,缓存等机制,例如我想搞个token过期跳转登录页面,我也得写两份代码.

2, 使用Charles抓包工具有点麻烦,我们知道Flutter的Dio是忽略的手机代理设置的,想要走网络代理得在代码里面配置,但原生的网络组件不一样,使用Charles抓包就很简单.

出于以上两点考虑,我想把目前应用中现存的网络请求和新增网络请求统一委托到原生层走网络请求,Flutter只收网络请求结果解析展示ui即可.

在介绍大背景后,我们需要先了解Dio, Dio本身是提供自定义网络请求的方式的,这里所谓的自定义是指自己去实现网络请求的发送和接收。

关于Dio网络请求流程

首先我们创建一个很简单的get请求 如何把Flutter的网络请求代理到原生层进行

由于get内部的代码比较多,这里只挑选几个有代表性的方法说下

get里面最终会调用到dio_mixin.dart中的fetch方法,顾名思义,它就是发送网络请求的地方,方法内部有个_dispatchRequest.

如何把Flutter的网络请求代理到原生层进行

_dispatchRequest里面的内容理解对于我们这个需求非常重要,因为在这里完成了实际的网络请求的发送(就是包含socket相关的)我们想替换实际的网络请求到原生也要从这里做文章.

Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
   //关注点一
   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
      //关注点二
      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) {
        //关注点三
        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;
      }
}

上面标明了三个值得关注的地方,这是我认为比较值得关注的点。

关注点一

通过httpClientAdapter获得responseBody对象,这里httpClientAdapter我后面会详细讲到它的作用,这里可以先理解为,它内部会实现http网络请求的逻辑,执行完后就会返回一个responseBody

关注点二

创建一个response对象,实际上responseBody还不是最终返回的请求应答,因为它只代表网络连接建立的的response,它里面有个stream,同来读取socket连接后的服务端返回的body,我们需要获取stream的内容后关闭连接. 这里我们先创建response对象,并赋予一些已知的应答字段。

关注点三

之前说了这里实际上是读取stream的内容(通过一个transformer),作为我们实际网络应答使用的data内容. 这个data就是服务端返回的body了,我们通常请求结果用到的code , msg , json的协议内容这些就在这里了.

看到这里我们实际上会有一个完成该需求大致的思路,我们想要把实际的网络请求代理到原生,就需要关注点一去把网络请求的过程替换掉,让实际的网络请求发生在原生,并返回一个response.这里dio支持提供自己的httpClientAdapter和transformer,这下就好办了,我直接搞一套自己的httpClientAdapter和transformer连接到原生的不就行了.

不过在进行动手开发之前,我们必须要先搞清楚HttpClientAdapter和HttpClient

HttpClientAdapter和HttpClient

HttpClientAdapter是Dio和HttpClient的一个桥梁

Dio: 它用于实现标准和友好的API给开发者使用

HttpClient:它用于在Dio中实际发送和接收网络请求

我们可以通过HttpClientAdapter提供自己的HttpClient而不是默认的。

HttpClientAdapter实际上是一个抽象类,它不提供具体实现。

如何把Flutter的网络请求代理到原生层进行

这里默认工厂方法创建的是IOHttpClientAdapter.

如何把Flutter的网络请求代理到原生层进行

回头来看看HttpClientAdapter里面有一个fetch方法

如何把Flutter的网络请求代理到原生层进行

fetch方法按照注释来说,它就是用来发送真正的网络请求的地方,那么在IOHttpClientAdapter中又是如何实现的呢?这里我直接跳过细节部分,看下关键的代码,它的fetch方法会调用到一个_fetch

如何把Flutter的网络请求代理到原生层进行

_configHttpClient会用来创建一个HttpClient,它用于在Dio中实际发送和接收网络请求。

如何把Flutter的网络请求代理到原生层进行

看实现,这里分了两步

1,我如果有createHttpClient函数的赋值,我这里直接用createHttpClient方法创建HttpClient

2,如果没有,就是直接调用HttpClient的构造,那它就是创建的是_HttpClient

如何把Flutter的网络请求代理到原生层进行

_HttpClient实际上是默认Dart io提供的Http的实现方式. HttpClient用于与Http服务端进行数据交换,发送Http请求到Http服务端然后接收response,  还会维护一些状态,例如包含session cookies。

HttpClient包含发送HttpClientRequest到Http服务端然后接收HttpClientResponse。 至于HttpClient内部细节我就不展开分析了,它里面无非就实现了http协议这些,也不是这篇文章关注的内容.

大致方案

其实了解了Dio的流程后,思路也比较清晰了,Dio提供了自定义HttpClientAdapter的方法,我这边自定义了一个NativeClientAdapter重写了一下fetch的实现(发送网路请求的地方),在fetch中不走默认的httpclient, 直接走methodchannel把请求参数发给原生(methodchannel是官方提供原生和flutter通信的方式之一),原生拿到后发送网络请求,请求结果返回给flutter, 然后再给flutter上层做数据转换即可.

代码实现

class NativeNetDelegate {

  final dio = Dio();

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

这里我自定义一个Dio, 使用了自己的NativeClientAdapter和NativeTransformer。NativeTransformer用来转换原生返回的数据.

class NativeClientAdapter implements HttpClientAdapter {

  static const _tag = "NativeClientAdapter";

  /// 这里重写fetch方法,不走默认httpclient的实现逻辑,把请求参数直接发送给原生做处理.
  @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'];

    /// 协议 json data
    String data = result['data'];

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

  /// 由于这里走的代理,直接返回了response中的data部分,所以这个不存在使用stream读取data
  Stream<Uint8List> fakeStream() async* {

  }

  /// Dio关闭时候调用,由于是把网络请求转发给原生处理,这里不存在释放资源
  /// 详情参考 [Dio] close方法
  @override
  void close({bool force = false}) {

  }
}
class NativeTransformer implements Transformer {

  static const String _tag = "NativeTransformer";

  ///这里因为代理到原生,所以不做任何Stream的读取Request的内容
  @override
  Future<String> transformRequest(RequestOptions options) async {
    return "";
  }

  ///这里直接返回原生网络请求返回的协议json,就是response的body.
  @override
  Future<dynamic> transformResponse(RequestOptions options, ResponseBody responseBody) async {
    if (responseBody is NativeResponseBody) {
      if (responseBody.data != null) {
        /// 这里转换成json给上层使用
        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);
}

上面我们用到一个NativeRequestOption,这其实是一个协议内容类,我们通过转发这个对象到原生层,让原生层解析并发送网络请求即可.

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}';
  }
}

最后,原生层的代码我就不贴了,无非是收到NativeRequestOption后再发送网络请求,收到网络结果后再发送回flutter解析一个结构即可.

{
“httpCode“ : 1001
data” : 业务协议内容(就是code, data, msg)
}

结语

这篇文章最重要的还是提供一个思路,代码实现因人而异,因需求而异,可以根据自己的需求去自定义HttpClientAdapter。 当然Dio还提供了拦截器功能,我们在拦截器里面直接拦截请求往原生转发也是可行的,这里就不做这个方案的探讨了. 觉得有用的别忘了点赞,评论加收藏哦~.

转载自:https://juejin.cn/post/7261059812238573629
评论
请登录