点击我的视频网校,学习编程课程,或者关注我的微信公众号“编程之路从0到1”,了解课程更新

Flutter 网络请求库之Gio

在Flutter开发中,常用网络请求库有diohttp两个,但它们有时候并不能完全满足我们的需求,因此我开发了Gio这个网络请求库。首先,我为什么不直接fork已有的库添加新的功能,而要自己从头开发一个这样的库呢?

一方面我觉得目前最热门的dio的代码写得并不直观,并不适合添加我设想的新特性,另一方面,别人的项目自己也无法掌控走向,索性自己撸一个。

Gio是一个功能强大的Dart HTTP请求库,它提供链式调用拦截器,通过它我们可以实现许多功能。例如,它提供了一个拦截器来模拟后端响应,这使得我们可以在不依赖后端程序员的进度的情况下快速构建和调试用户界面。

快速开始

添加以下依赖

dependencies:
  gio: ^0.0.4

接下来,看一个HTTP请求示例:

import 'dart:convert';

import 'package:gio/gio.dart' as gio;

void main() async {
  var resp =
      await gio.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
  print(resp.body);

  /// GET http://example.com?a=1&b=2
  resp = await gio.get("http://example.com", queryParameters: {"a": "1", "b": "2"});
  print(resp.request?.url);


  /// POST Form Data
  var data = {"username": "Bob", "passwd": "123456"};
  var header = {"content-type":"application/x-www-form-urlencoded"};
  resp = await gio.post("http://example.com", headers: header,body: data);
  print(resp.body);

  /// POST JSON Data
  /// 注意:如果传递的是JSON数据
  /// 那么body应该是一个字符串类型
  var data2 = {"name": "Bob", "age": "22"};
  var header2 = {"content-type":"application/json"};
  resp = await gio.post("http://example.com", headers: header,body: jsonEncode(data));
  print(resp.body);
}

你也可以像下面这样使用Gio,在这个连接被关闭之前,你可以重复使用这个连接:

import 'package:gio/gio.dart';

void main() async{
  Gio gio = Gio();
  try{
    var resp = await gio.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp.body);
  }finally{
    gio.close();
  }
}

全局配置

Gio允许我们使用GioOption设置全局的Base URL,以及启用全局日志以方便跟踪请求。例如:

import 'package:gio/gio.dart' as gio;

void main() async{
  gio.Gio.option = gio.GioOption(
      basePath: 'https://giomock.io',
      enableLog: true
  );

  // 相当于: https://giomock.io/greet
  var resp = await gio.get("/greet", queryParameters: {'name': 'Bob'});
  print(resp.body);
}

enableLog设置为true,以启用全局日志,方便追踪请求。

在此注意,如果你在GioOption中配置了basePath,但你在一些请求中没有应用这个basePath,那么你需要在请求中覆盖这个参数,如下所示:

import 'package:gio/gio.dart';

void main() async {
  Gio.option = GioOption(
      basePath: 'https://giomock.io',
      enableLog: true
  );

  // 将`Gio'的`baseUrl'设置为空字符串,覆盖全局配置
  Gio gio = Gio(baseUrl: '');
  try{
    var resp = await gio.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp.body);
  }finally{
    gio.close();
  }
}

拦截器

拦截器类型,就是一个符合以下签名的函数或闭包:

typedef Interceptor = Future<StreamedResponse> Function(Chain chain);

Gio拦截器分为三种类型

  • 全局拦截器
  • 本地拦截器
  • 默认拦截器

请注意,这三种拦截器中,本地拦截器被首先调用,依次是全局拦截器,最后是默认拦截器。

全局拦截器

全局拦截器全局有效,通过GioOption设置:

void main() async {
  // 声明一个拦截器
  checkHeader(gio.Chain chain) {
    var auth = chain.request.headers['Authorization'];
     // 当条件满足时,继续执行下一个拦截器,
     // 否则,中断请求
    if (auth != null && auth.isNotEmpty) {
      return chain.proceed(chain.request);
    }
    throw Exception('Invalid request, does not contain Authorization!');
  }

  // 参数是一个列表,可以设置多个拦截器
  gio.Gio.option = gio.GioOption(
      basePath: 'http://worldtimeapi.org', 
      globalInterceptors: [checkHeader]);

  try {
    var resp = await gio.get("/api/timezone/Asia/Shanghai");
    print(resp.body);
  } catch (e) {
    print(e);
  }
}
Exception: Invalid request, does not contain Authorization!

Process finished with exit code 0

本地拦截器

本地拦截器也可以添加多个:

import 'package:gio/gio.dart';

void main() async {
  Gio gio = Gio();

  // 拦截请求并修改请求头
  gio.addInterceptor((chain) {
    if (chain.request.method == "POST") {
      chain.request.headers["content-type"] = "application/json";
    }
    return chain.proceed(chain.request);
  });

  // 拦截response,在这里做一些业务处理
  gio.addInterceptor((chain) async {
    var res = await chain.proceed(chain.request);
    if (res.statusCode != 200) {
      throw Exception(
          "The request is unsuccessful, the status code is ${res.statusCode}");
    }
    return res;
  });

  try {
    var resp =
    await gio.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp.body);
  } catch (e) {
    print(e);
  } finally {
    gio.close();
  }
}

我们也可以把拦截器的逻辑放在一个类中,而不是一个闭包中,这样可以更好地组织我们的代码。

这里我们实现了一个取消请求的例子,当用户离开视图时,可能不再需要请求的结果了:

cancel_interceptor.dart

import 'package:gio/gio.dart';

class CancelInterceptor {
  bool _isCancel = false;

  void cancel() {
    _isCancel = true;
  }

  Future<StreamedResponse> call(Chain chain) async {
    if (_isCancel) {
      throw CancelError("User initiated cancellation.");
    }
    var res = await chain.proceed(chain.request);
    if (_isCancel) {
      throw CancelError("User initiated cancellation.");
    }
    return res;
  }
}

main.dart

import 'package:gio/gio.dart';

void main(){
  var cancelInterceptor = CancelInterceptor();
  testCancel(cancelInterceptor);
  cancelInterceptor.cancel();
}

Future<void> testCancel(CancelInterceptor cancel) async {
  Gio g = Gio();
  g.addInterceptor(cancel);
  try {
    var res = await g.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(res.body);
  } on CancelError {
    print("request has been canceled");
  } finally {
    g.close();
  }
}

分组拦截器

拦截器允许我们使用统一的方式来处理网络请求,但有时我们可能需要根据模块来定制网络请求。分组拦截器正是用在这样的场景中。

import 'package:gio/gio.dart' as gio;

void main() async {
  // 设置分组拦截器
  gio.group("module1").addInterceptor(CancelInterceptor());
  gio.group("module2").addInterceptor(ModifyHeaderInterceptor());

  var module1 = gio.GioGroup("module1");
  try{
    var resp = module1.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp);
  }catch(e){
    print(e);
  }finally{
    module1.close();
  }

  var module2 = gio.GioGroup("module2");
  try{
    var resp = module2.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp);
  }catch(e){
    print(e);
  }finally{
    module2.close();
  }
}

默认拦截器

默认拦截器是指GioOption中包含的几个拦截器,它们是:

  • GioLogInterceptor :用于打印请求与响应的日志
  • GioConnectInterceptor :用于处理当前网络是否连通
  • GioMockInterceptor:用于模拟响应

我们可以自定义这些拦截器来替换默认的拦截器:

import 'package:gio/gio.dart';

void main() async {
  Gio.option = GioOption(
    connectInterceptor: MyConnectInterceptor(),
    logInterceptor: MyLogInterceptor()
  );

  Gio gio = Gio();
  try{
    var resp = await gio.get("http://worldtimeapi.org/api/timezone/Asia/Shanghai");
    print(resp.body);
  }finally{
    gio.close();
  }
}

class MyConnectInterceptor extends GioConnectInterceptor{

  @override
  Future<bool> checkConnectivity() {
    // TODO: 在此检查当前网络是否已连接
      throw ConnectiveError(101,"Mobile Network Data disabled !");
      // or
      // throw ConnectiveError(102,"Wifi disabled !");
  }
}

class MyLogInterceptor extends GioLogInterceptor{
  @override
  Future<StreamedResponse> call(Chain chain) async {
    final request = chain.request;
    // _logRequest(request);
    try {
      final response = await chain.proceed(request);
      // _logResponse(response);
      return response;
    } catch (e) {
      // _logUnknownError(e, request);
      rethrow;
    }
  }
}

注意,默认拦截器也是全局可用的。

在这里,我们可以在拦截器中实现日志跟踪,请求耗时的监控。你可以参考GioLogInterceptor的源代码来实现一个你自己的日志拦截器。

模拟响应

当你想先行开发UI或在不依赖服务后台的情况下测试和调试UI时,模拟响应是一个非常有用的功能。要使用此功能,请额外添加gio_mock依赖:

dependencies:
  gio: latest
  gio_mock: latest

要使用模拟后端响应的功能,你需要设置mockInterceptor参数,如下所示:

void main() async{
  gio.Gio.option = gio.GioOption(
      basePath: 'https://giomock.io',
      mockInterceptor: GioMockServer(MyMockChannel()));

  var resp = await gio.get("/greet", queryParameters: {'name': 'Bob'});
  print(resp.body);

  var data = {"username": "Bob", "passwd": "123456"};
  var header = {"content-type":"application/x-www-form-urlencoded"};
  resp = await gio.post("/login", headers: header,body: data);
  print(resp.body);

  var data2 = {
    "array":[
      {
        "name":"Go",
        "url":"https://go.dev/",
      },
      {
        "name":"Dart",
        "url":"https://dart.dev/",
      },
    ]
  };
  header = {"content-type":"application/json"};
  resp = await gio.post("/list", headers: header,body: jsonEncode(data2));
  print(resp.body);
}

接下来你需要创建 MyMockChannel

import 'package:gio_mock/gio_mock.dart';
import 'package:gio_mock/src/http/response_x.dart';

class MyMockChannel extends MockChannel{

  @override
  void entryPoint() {
    get("/greet",(MockRequest request){
      return ResponseX.ok("hello,${request.query['name']}");
    });

    post("/login",(MockRequest request){
      return ResponseX.ok(request.bodyFields);
    });

    post("/list",(MockRequest request){
      return ResponseX.ok(request.body);
    });
  }
}

我们需要在entryPoint方法中注册请求路由,你可以使用get/post等方法来为HTTP请求注册路由。

这些方法的第二个参数是路由响应的处理函数,你可以在这里处理请求参数并返回响应。

TODO

  • 支持在单独的Isolate发起请求
  • 支持解析Swagger配置,自动生成RESTful 接口的调用代码

这是一些尚未实现的设想。因为目前Dart的元编程尚不稳定,且Dart 3规划中的宏编程语法尚未发布。我希望借助成熟的元编程来实现这些功能。

在Flutter开发中,有时候会需要编写大量类似的RESTful 接口的调用代码,这大大增加了程序员的重复劳动。如果能自动生成调用后端RESTful 接口的调用代码,能大大提升开发效率。


关注公众号:

# Flutter   Gio   HTTP   request  

评论

公众号:编程之路从0到1

公众号

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×