目录
背景
首先看到标题,相信大家已经血压飙升了,既然都用了Flutter
,怎么还用原生的网络库呢?这不是多此一举么。emmmmm,当我看到这个需求的时候一样恼火。但因为我们项目中使用的Dio库不支持查询DNS、SSL等所需时间(至少我没找到),而Okhttp
却可以。因此,不得以只能借助原生网络库去实现此功能。
实现方案
既然必须要去实现,那摆在面前的问题就是如何去实现了。最简单粗暴的一个方式,就是通过建立一个Flutter Channel将网络请求的所有信息传到原生,但脑补一波后,发现实现起来太过麻烦,一堆header,body,data等信息都需要自己组装,并且致命的是需要改动现有项目的请求方式。因此为了避免项目大改,最后看中了Dio
提供的interceptors(拦截器)功能,在拦截器中,我们可以对网络请求进行拦截,拦截后再通过FlutterChannel发给原生进行网络请求。这样既避免了项目改动,也省去了数据组装,很容易的达到了目标。
实现步骤
- 首先定义一个拦截器(NativeNetInterceptor),用来转发网络请求,在onRequest中,我们调用了NativeNet.sendRequestToNative方法,用来接收接口中的参数(url、header、body、data、等等等),方便下一步转发。catch模块主要用来捕获异常,如果是DioError,直接
reject
,如果是其他异常,可以调用handler.next(options)继续走Dio请求,当作兜底方案。这里我注释掉了。
class NativeNetInterceptor extends Interceptor { final Dio dio; final NativeNetOptions options; NativeNetInterceptor({ required this.dio, NativeNetOptions? options, }) : options = options ?? NativeNetOptions(); @override void onRequest( RequestOptions options, RequestInterceptorHandler handler) async { try { Response response = await NativeNet.sendRequestToNative(options); return handler.resolve(response, true); } catch (e) { if (e.runtimeType == DioError) { return handler.reject(e as DioError); } else { // TODO:如果担心原生有问题,可打开下方注释。 // handler.next(options); rethrow; } } } }
- NativeNet的实现,这里主要是将Dio
options
中所带的信息发送给原生,并将原生返回信息进行组装。
class NativeNet { static const MethodChannel _channel = MethodChannel('nativeNet'); static Future<Response> sendRequestToNative(RequestOptions options) async { final String url = options.baseUrl + options.path; Map channelParams = _getChannelParams( url, options.method, queryParameters: options.queryParameters, data: options.data, header: options.headers, timeoutInterval: options.connectTimeout, ); return await _sendRequestToNative(options, channelParams); } static Future<Response> _sendRequestToNative( RequestOptions options, Map params) async { final Map nativeResponse = await _channel.invokeMapMethod('net', params) ?? {}; final Response response = Response(requestOptions: options); if (nativeResponse.containsKey('headers')) { final Map nativeResponseHaders = nativeResponse['headers']; nativeResponseHaders.forEach((key, value) { response.headers.set(key, value); }); } response.statusCode = nativeResponse['statusCode'] ?? -1; response.statusMessage = nativeResponse['statusMessage'] ?? '无message'; if (Platform.isAndroid) { if (nativeResponse.containsKey('data')) { String jsonData = nativeResponse['data']; try { Map<String, dynamic> data = convert.jsonDecode(jsonData); response.data = data; } on FormatException catch (e) { ///转换异常后手动构造一下 debugPrint("Http FormatException"); Map map = {}; map["data"] = jsonData; response.data = map; } } else { response.data = {}; } } else { Map<String, dynamic> data = Map<String, dynamic>.from(nativeResponse['data'] ?? {}); response.data = data; } if (nativeResponse.containsKey('error')) { //网络请求失败 Map? errorData = nativeResponse['error']; throw DioError( requestOptions: options, response: response, error: errorData); } return response; } static Map<String, dynamic> _getChannelParams( String url, String method, { Map<String, dynamic>? queryParameters, Map<String, dynamic>? data, Map<String, dynamic>? header, num? timeoutInterval, num? retryCount, }) { Map<String, dynamic> channelParams = { 'url': url, 'method': method, }; if (queryParameters != null) { channelParams['queryParameters'] = queryParameters; } if (data != null) { channelParams['data'] = data; } if (header != null) { channelParams['header'] = header; } if (retryCount != null) { channelParams['retryCount'] = retryCount; } if (timeoutInterval != null) { channelParams['timeoutInterval'] = timeoutInterval; } return channelParams; } }
- 原生实现,这里贴出安卓代码,请求完之后,无论成功失败,都调用result.success将处理好的数据返回给flutter。具体的
ANDROID/IOS
网络请求这里就略过了,相信大家都用的很成熟了。
class NativeNetPlugin : FlutterPlugin, MethodCallHandler { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel: MethodChannel private var gson: Gson = Gson() private val mTag = "Android NativeNetPlugin" override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "nativeNet") channel.setMethodCallHandler(this) } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "net") { val channelParams = call.arguments as Map<String, Any> var method = "" if (channelParams.containsKey("method")) { method = channelParams["method"] as String } var url = "" if (channelParams.containsKey("url")) { url = channelParams["url"] as String } var header = HashMap<String, String>() if (channelParams.containsKey("header")) { header = channelParams["header"] as HashMap<String, String> } val headers = HttpHeaders() headers.headersMap = getMapValueForLinkedHashMap(header) // params var queryParameters = HashMap<String, String>() if (channelParams.containsKey("queryParameters")) { queryParameters = channelParams["queryParameters"] as HashMap<String, String> } //post body var data = HashMap<String, Any>() if (channelParams.containsKey("data")) { data = channelParams["data"] as HashMap<String, Any> } //超时时间 var timeoutInterval: Int = 1000 * 15 if (channelParams.containsKey("timeoutInterval")) { timeoutInterval = channelParams["timeoutInterval"] as Int } val mTimeOut = Timeout(timeoutInterval, timeoutInterval, timeoutInterval, TimeUnit.MILLISECONDS) //重试次数 var retryCount = 0 if (channelParams.containsKey("retryCount")) { retryCount = channelParams["retryCount"] as Int } when (method) { "POST" -> { ... //请求成功/失败后调用此方法, result.success(dealResponse(response)) } "GET" -> { ... } "DELETE" -> { ... } "PUT" -> { ... } "HEAD" -> { ... } else -> { result.notImplemented() } } } else { result.notImplemented() } } private fun dealResponse( response: Response<String> ): Map<String, Any?> { val map = HashMap<String, Any?>() if (BuildConfig.DEBUG) { Log.e(mTag, "dealResponse isSuccessful: ${response.code()}") Log.e(mTag, "dealResponse code: ${response.code()}") Log.e(mTag, "dealResponse message: ${response.message()}") Log.e(mTag, "dealResponse body: ${response.body()}") Log.e(mTag, "dealResponse headers: ${response.headers()}") } map["statusCode"] = response.code() response.message()?.let { map["statusMessage"] = it } ?: let { map["statusMessage"] = "" } response.body()?.let { map["data"] = it } ?: let { map["data"] = "" } response.headers()?.let { map["headers"] = it.toMap() } ?: let { map["headers"] = HashMap<String, Any>() } if (response.code() != 200) { //失败 val errorMap = HashMap<String, Any?>() response.exception?.let { errorMap["code"] = response.code() errorMap["domain"] = it.toString() errorMap["description"] = it.message } ?: let { errorMap["code"] = response.code() errorMap["domain"] = map["statusMessage"] errorMap["description"] = "HttpException" } map["error"] = errorMap } return map } //map 转 LinkedHashMap private fun getMapValueForLinkedHashMap(dataMap: Map<String, String>): LinkedHashMap<String, String> { val returnMap: LinkedHashMap<String, String> = LinkedHashMap() if (dataMap.isNullOrEmpty()) { return returnMap } val iterator = dataMap.keys.iterator() while (iterator.hasNext()) { val objKey = iterator.next() val objValue = dataMap[objKey] returnMap[objKey] = objValue.toString() } return returnMap } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } }
4.最后调用dio.interceptors.add(NativeNetInterceptor(dio: dio));
将我们写好的NativeNetInterceptor加入Dio拦截器。
交互协议
Request
(Flutter To Native)
{ "url": "https://www.baidu.com", // 必传 请求地址 "method": "GET", //必传 请求方式 GET POST HEAD PUT DELETE "queryParameters":{}, //可选 query参数 "data":{}, //可选 body参数 "header":{}, //可选 请求头 "timeoutInterval":15000, //可选 (毫秒) 链接超时时常 "retryCount": 0 //可选 重试次数,默认0 }
Response
(Native To Flutter)
// native返回 成功 { "data":{}, //接口原始返回内容 "headers":{}, //返回头 "statusCode":200, //http状态码 "statusMessage":"请求成功" //http状态码对应文案 } // native返回 失败 { "data":{}, //接口原始返回内容 "headers":{}, //返回头 "statusCode":404, //http状态码 "statusMessage":"找不到对象", //http状态码对应文案 "error":{ "code":-3001, //错误码 "domain":"URLDmain", // 错误大分类 "description":"错误详情" // 错误详情 } }
注意点
添加NativeNetInterceptor,如果有多个拦截器,例如LogInterceptors等等,需要将NativeNetInterceptor放到最后。