基于Dio+RxDart的网络框架

1,137 阅读4分钟

实现思想

自己在工作中长期使用Swift,网络库经常使用Moya+Alamofire框架,对于Moya的设计理念很喜欢,学了Flutter后一直没有找到比较喜欢的网络框架,因此就像写一个使用Moya思想的框架

仓库地址

https://github.com/manfengjun/flutter_spi

使用

# add this line to your dependencies
flutter_spi: ^0.1.1

引入

import 'package:flutter_spi/flutter_spi.dart';

目录

classdescription
pg_spiSpi
pg_spi_dioDio Response Convert
pg_spi_errorError
pg_spi_loggerLogger
pg_spi_managerNetWork Manage
pg_spi_responseResponse Convert
pg_spi_targetApi Enum

详解

Api Enum

在这里,我是用了Dart的枚举和抽象类(类似Swift的Protocol)

// 请求类型
enum HTTPMethod {
  get,
  post,
  put,
  patch,
  delete,
  trace,
  connect,
}
// value Extension
extension HTTPMethodEx on HTTPMethod {
  String get value =>
      ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT'][this.index];
}
// PGSpiTarget
abstract class PGSpiTarget {
  // 发出网络请求的基础地址字符串
  String get baseUrl;

  /// 网络请求的路径字符串
  String get path;

  /// 网络请求的方式,默认返回get
  HTTPMethod get method => HTTPMethod.post;

  /// 网络请求参数
  Map<String, dynamic> get parameters => null;

  /// 网络请求头
  Map<String, String> get headers => null;

  /// 日志输出
  bool get logEnable => true;
}

示例

enum Account { login }
// 由于Dart的枚举不支持关联参数,因此使用了扩展代替,基本实现了功能,使用相对繁琐一点
extension AccountAPI on Account {
  static login(Map<String, dynamic> params) =>
      AccountTarget(Account.login, params: params);
}

class AccountTarget with PGSpiTarget {
  final Account type;
  Map<String, dynamic> params = {};
  @override
  String get baseUrl => Constant.baseUrl;

  @override
  String get path {
    switch (type) {
      case Account.login:
        return Constant.api + '/toutiao/index';
        break;
      default:
        return '';
    }
  }

  @override
  HTTPMethod get method {
    switch (type) {
      case Account.login:
        return HTTPMethod.get;
        break;
      default:
        return HTTPMethod.post;
    }
  }

  @override
  Map<String, dynamic> get parameters => params;

  @override
  Map<String, String> get headers => {"version": "1.0"};

  AccountTarget(this.type, {this.params});
}

错误处理

enum Exception {
  // 网络异常
  networkException,
  // invalidURL
  invalidURL,
  // 服务器异常 500
  serverException,
  // 方法不存在 404
  notFound,
  // ContentType 不被接受
  unacceptableContentType,
  // 响应状态异常
  unacceptableStatusCode,
  // data缺失
  dataNotFound,
  // JSON序列化异常
  jsonSerializationFailed,
  // 对象转换失败
  objectFailed,
  // 执行结果状态吗不合理
  unlegal,
  // 执行结果异常,操作失败
  executeFail,
}

extension ExceptionEx on Exception {
  int get value => index + 10000;
}

class PGSpiError extends Error {
  Exception exception;
  int status;
  String message = '请求失败';

  PGSpiError._init(this.status, {this.message});
  factory PGSpiError.exception(Exception exception,
      {int status, String message}) {
    int code = status != null ? status : exception.value;
    String msg = message != null ? message : spiCode[exception.value].msg;
    return PGSpiError._init(code, message: msg);
  }
}

Map<int, Tuple> spiCode = {
  10000: Tuple(10000, '网络异常,请稍后重试!'),
  10001: Tuple(10001, '请求地址异常'),
  10002: Tuple(10002, '服务器异常,请稍后重试!'),
  10003: Tuple(10003, '接口未找到'),
  10004: Tuple(10004, 'Content-Type异常'),
  10005: Tuple(10005, '响应状态异常'),
  10006: Tuple(10006, '没有数据节点'),
  10007: Tuple(10007, 'JSON序列化异常'),
  10008: Tuple(10008, '对象序列化异常'),
  10009: Tuple(10009, '缺省状态节点'),
};

class Tuple<T> {
  final int status;
  final T msg;

  Tuple(this.status, this.msg);
}

日志

// 拦截器
class LogsInterceptors extends InterceptorsWrapper {
  @override
  Future onRequest(RequestOptions options) {
    print("✅ " + '${options.path}');
    print('✅ METHOD:${options.method}');
    if (options.method == 'GET') {
      print('✅ Body:${options.queryParameters}');
    } else {
      print('✅ Body:${options.data}');
    }
    return super.onRequest(options);
  }
  @override
  Future onResponse(Response response) {
    if (response != null) {
      var json = jsonDecode(response.data);
      if (json != null) {
        print('🇨🇳 Return Data:');
        print('🇨🇳 $json');
      } else {
        print('🇨🇳 JSON 解析异常');
      }
    } else {
      print('🇨🇳 response 不存在');
    }
    return super.onResponse(response);
  }

  @override
  Future onError(DioError e) {
    if (e.error is PGSpiError) {
      print('❌ ${e.error.status} ---- ${e.error.message}');
    } else {
      print('❌ ${e.toString() ?? "无错误描述"})');
    }
    return super.onError(e);
  }
}

Response的解析

在Swift中一般请求结果都通过泛型来进行类型的传递,这样就可以直接将返回信息直接处理,就可以直接得到对象,在Dart中遇到了不少问题,Dart的泛型功能相对Swift弱了一点,无法通过泛型去对应解析关系,最后遭到了一种使用字符串对应解析关系的方法,目前的问题是使用时需要对应类和字符串的关系

// News 为类名,需要通过字符串找到对应关系
class EntityFactory {
  static T generateOBJ<T>(json) {
    if (1 == 0) {
      return null;
    } else if (T.toString() == "News") {
      return News.fromJson(json) as T;
    } else {
      return null;
    }
  }
}

Bean类解析

/// 解析基类
class BaseBeanEntity<T> {
  Map<String, dynamic> results;
  List<dynamic> resultsList = [];

  BaseBeanEntity({this.results, this.resultsList});

  /// 处理results为对象的情况
  BaseBeanEntity.fromJson(Map<String, dynamic> json) {
    results = json;
  }

  /// 处理results为数组的情况
  BaseBeanEntity.fromJsonList(List<Map<String, dynamic>> json) {
    resultsList = json;
  }

  /// 获取results对象
  T object<T>() {
    return EntityFactory.generateOBJ<T>(results); //使用EntityFactory解析对象
  }

  /// 获取results数组
  List<T> objects<T>() {
    var list = new List<T>();
    if (resultsList != null) {
      resultsList.forEach((v) {
        //拼装List
        list.add(EntityFactory.generateOBJ<T>(v)); //使用EntityFactory解析对象
      });
    }
    return list;
  }

  Map<String, dynamic> toJson() {
    Map<String, dynamic> data = new Map<String, dynamic>();
    if (this.results != null) {
      data = this.results;
    }
    return data;
  }
}

这里是一个对象数组的解析类,通过泛型传递对象类型

// 返回结果为 List
  Future<List<dynamic>> mapSpiJsons<T>(
    PGSpiTarget target, {
    String designatedPath,
    Options options,
    CancelToken cancelToken,
    ProgressCallback onSendProgress,
    onReceiveProgress,
  }) async {
    Response response = await request(
      target.baseUrl + target.path,
      data: target.method == HTTPMethod.get ? {} : target.parameters,
      options: checkOptions(target.method.value, options),
      queryParameters: target.method != HTTPMethod.get ? {} : target.parameters,
      cancelToken: cancelToken,
      onSendProgress: onSendProgress,
      onReceiveProgress: onReceiveProgress,
    );
    var json = response.data;
    var status = json[PGSpiManager.shared.key.status];
    if (status == PGSpiManager.shared.key.success) {
      if (json[PGSpiManager.shared.key.data] == null) {
        return [];
      }
      if (designatedPath == null || designatedPath.length <= 0) {
        return json[PGSpiManager.shared.key.data];
      }
      if (json[PGSpiManager.shared.key.data][designatedPath] == null) {
        throw DioError(
          error: PGSpiError.exception(Exception.objectFailed),
        );
      }
      return json[PGSpiManager.shared.key.data][designatedPath];
    } else {
      if (status != null && status is int) {
        // 请求正常,操作失败
        throw DioError(
          error: PGSpiError.exception(
            Exception.executeFail,
            status: status,
            message: json[PGSpiManager.shared.key.msg],
          ),
        );
      }
      // 请求结果状态码不合法
      throw DioError(
        error: PGSpiError.exception(Exception.unlegal),
      );
    }
  }

使用

使用时如果需要结合RxDart使用,需要增加一个Extension

extension PGSpiRx on PGSpi {
  // object stream
  Stream<T> mapSpiObject<T>({String path}) => this
      .responseSpiJson(path: path)
      .asStream()
      .map((value) => BaseBeanEntity.fromJson(value).object<T>());
  // objects stream
  Stream<List<T>> mapSpiObjects<T>({String path}) => this
      .responseSpiJsons(path: path)
      .asStream()
      .map((value) => BaseBeanEntity.fromJsonList(value).objects<T>());
}

实际使用示例

PGSpi(AccountAPI.login(
            {"type": "top", "key": "8093f06289133b469be6ff7ab6af1aa9"}))
        .mapSpiObjects<News>(path: "data")
        .listen(
      (value) => print(value[0].authorName),
      onError: (e) {
        //获取失败
        print('错误:' + (e.error as PGSpiError).message);
      },
    );

思考

目前功能基本实现了,使用上也还不错,主要问题是解析时实体类和泛型的结合有一定问题,通过字符串的方式后续维护可能和麻烦。不过目前没有找到一个不错的解决方案