Flutter中api實現(xiàn)自動生成(簡單記錄下)

Flutter中api實現(xiàn)自動生成

最近公司項目中電商模塊從H5遷移為Flutter,在此過程中難免要對之前的接口再實現(xiàn)一遍。
最初設(shè)計是定義一單例的ApiUtil,再實現(xiàn)一基于ApiUtil的擴展,在擴展類中添加各個接口
的實現(xiàn),部分代碼如下:

class ApiUtil {
  ApiUtil._();

  static final ApiUtil _instance = ApiUtil._();

  static ApiUtil get inst => _instance;

  static Dio _dio = getDio();

  static Dio getDefaultDio() {
    Dio result = Dio(BaseOptions(
      connectTimeout: 10000,
      receiveTimeout: 10000,
    ));

    final adapter = result.httpClientAdapter as DefaultHttpClientAdapter;
      adapter.onHttpClientCreate = (client) {
        client.findProxy = (uri) {
          return "PROXY ";
        };
        client.badCertificateCallback =
            (X509Certificate cert, String host, int port) {
          return true;
        };
      };

    return result;
  }

  static Dio getDio() {
    _dio = getDefaultDio();
    _dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      ...
    }, onError: (DioError error) {
      return error;
    }));
    return _dio;
  }

  Future<Response> get(
    String path, {
    data,
    Map<String, dynamic> queryParameters,
    CancelToken cancelToken,
    ProgressCallback onReceiveProgress,
    String contentType = Headers.jsonContentType,
  }) async {
    return _dio.get(path,
        options: Options(
          headers: {
            HttpHeaders.acceptHeader: "application/json,text/plain,*/*",
          },
          contentType: contentType,
        ),
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        onReceiveProgress: onReceiveProgress);
  }

  Future<Response> post(
    String path, {
    data,
    Map<String, dynamic> queryParameters,
    CancelToken cancelToken,
    ProgressCallback onSendProgress,
    ProgressCallback onReceiveProgress,
    String contentType = Headers.formUrlEncodedContentType,
  }) async {
    return _dio.post(path,
        data: data,
        options: Options(
          headers: {
            HttpHeaders.acceptHeader: "application/json,text/plain,*/*",
          },
          contentType: contentType,
        ),
        queryParameters: queryParameters,
        cancelToken: cancelToken,
        onSendProgress: onSendProgress,
        onReceiveProgress: onReceiveProgress);
  }
}

擴展類:

extension BaseApiUtil on ApiUtil {
  Future<GetAppPageRsp> getAppPage(int pageType, {String pageId}) {
    Map<String, dynamic> data = {
      "pageType": pageType,
      "modeType": 1,
      "companyId": AppConfig.COMPANY_ID,
      "lang": "zh_CN",
      "platformId": 4
    };
    if (!StringUtil.isEmpty(pageId)) {
      data.addAll({"pageId": pageId});
    }
    Future<GetAppPageRsp> result = new Future(() async {
      Response rsp = await post(
        "${ApiConfig.base}/cms/page/getAppPage",
        data: data,
      );
      return GetAppPageRsp.fromJson(rsp.data);
    });
    return result;
  }

  Future<RecommendMpListRsp> recommendMpListByMpIds(List mpIds) {
    Map<String, dynamic> data = {
      "sceneNo": 2,
      "pageNo": 1,
      "pageSize": 24,
      "platformId": AppConfig.PLATFORM_ID,
      "mpIds": mpIds.join(','),
      "sessionId": GlobalData.sessionId,
      "areaCode": MallData.getAreaCode(),
    };
    Future<RecommendMpListRsp> result = new Future(() async {
      Response rsp = await get(
        "${ApiConfig.base}/search/rest/recommendMpList",
        queryParameters: data,
      );
      return RecommendMpListRsp.fromJson(rsp.data);
    });

    return result;
  }
  ...
}

查看擴展類中的部分代碼,我們可以看到,其定義了基本的get/post方法,供其他接口實現(xiàn)時調(diào)用。相關(guān)模塊在調(diào)用時使用ApiUtil.inst獲取ApiUtil的實例,然后調(diào)用擴展中實現(xiàn)的方法。大致接口實現(xiàn)結(jié)構(gòu)如下:

Future<應(yīng)答類> 方法名(參數(shù)...) {
    Map<String, dynamic> data = {
      ...
    };
    Future<應(yīng)答類> result = new Future(() async {
      Response rsp = await 請求方式(get/post)(
        "接口地址",
        "請求數(shù)據(jù)(get->queryParameters, post->data)": data,
      );
      return 應(yīng)答類.fromJson(rsp.data);
    });

    return result;
  }

如果有新增接口實現(xiàn)的話,基本可以拷貝其中的接口實現(xiàn),修改方法返回類型、方法名、參數(shù)列表、data中的Map數(shù)據(jù)、請求方式、接口地址。盡著能少寫代碼就少寫代碼的原則, 考慮接口實現(xiàn)部分代碼能否自動生成呢?答案是肯定的,通過注解、source_gen和build_runner方式可以實現(xiàn)類似json_serializable那樣的代碼自動生成。我們在build.yaml中定義的builder,編譯時掃描builder下的相關(guān)文件,搜集其中的注解,通過generator生成具體代碼。以下是相關(guān)步驟:

  • 1 定義注解類
    我們通過注解來生成代碼,那么肯定需要在注解中定義要搜集的一些信息。我們的目標(biāo)是生成一個接口工具類,那么顯而易見,首先需要定義一個目標(biāo)類名,其次接口地址、請求方式、請求數(shù)據(jù)等也是必須的,具體定義如下:
class ApiGen {
  static const String GET = 'GET';
  static const String POST = 'POST';
  static const String PUT = 'PUT';
  static const String PATCH = 'PATCH';
  static const String DELETE = 'DELETE';

  final String target;// 目標(biāo)類名
  final String url;// 接口地址
  final String method;// 請求方式
  final dynamic data;// 請求數(shù)據(jù)
  final String contentType;// 請求數(shù)據(jù)contentType
  final Map<String, dynamic> header;// 請求header
  final String requestName;// 請求方式名

  const ApiGen(this.url, {
    this.method = POST,
    this.data,
    this.contentType,
    this.header,
    this.requestName,
    this.target
  });
}
  • 2 創(chuàng)建注解解析生成器
class ApiGenerator extends GeneratorForAnnotation<ApiGen> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    // 解析注解,生成目標(biāo)代碼
  }
}
  • 3 有了注解解析生成器,得能觸發(fā)解析才可以執(zhí)行,于是Builder就登場了。Builder有SharedPartBuilder、PartBuilder、LibraryBuilder,用于處理生成不同的文件。SharedPartBuilder生成.g.dart文件,在源文件中作為part引入,PartBuilder生成.自定義.dart文件,在源文件中作為part引入,LibraryBuilder生成單獨的dart文件。因為我們要生成單獨的文件,所以使用LibraryBuilder, 其中g(shù)eneratedExtension為生成文件的擴展名
Builder apiBuilder(BuilderOptions options) => LibraryBuilder(
  ApiGenerator(),
  generatedExtension: '.api.util.dart'
);
  • 4 build.yaml中配置builder,其中package_name為pubspec.yaml的name
    generate_for下的include為我們要掃描的文件
targets:
  $default:
    builders:
      package_name|api_builder:
        enabled: true
        generate_for:
          include: ['**.api_gen.dart']
builders:
  api_builder:
    import: 'package:package_name/annotation/api_builder.dart'
    builder_factories: ['apiBuilder']
    build_extensions: {'.api_gen.dart': ['.api.util.dart']}
    auto_apply: root_package
    build_to: source

當(dāng)我們執(zhí)行flutter packages pub run build_runner build --delete-conflicting-outputs執(zhí)行編譯時,build會讀取build.yaml中的配置信息,讀取到apiBuilder后觸發(fā)注解生成器ApiGenerator,在GeneratorForAnnotation中調(diào)用generate來處理生成代碼,可以看到generate中會調(diào)用generateForAnnotatedElement,故我們在generateForAnnotatedElement中實現(xiàn)注解相關(guān)解析處理即可。接口實現(xiàn)部分代碼使用mustache模塊來實現(xiàn),相關(guān)語法說明可參考mustache

class ApiUtilTpl {
  static const String tpl = """
import 'package:dio/dio.dart';
{{#imports}}
import '{{{path}}}';
{{/imports}}

extension {{className}} on {{targetClassName}} {
  {{#functions}}
  {{{functionDefine}}} {
    {{#hasData}}
    {{{dataType}}} data = {{{dataValue}}};
    {{/hasData}}
    
    {{^withBodyWrapper}}
    {{#params}}
    if (null != {{paramName}}) {
      data["{{{paramName}}}"] = {{paramName}};
    }
    {{/params}}
    {{/withBodyWrapper}}
    
    {{{returnType}}} result = new Future(() async {
      Response rsp = await {{requestName}}(
          "{{{url}}}",
          {{#hasData}}{{#httpSendData}}data{{/httpSendData}}{{^httpSendData}}queryParameters{{/httpSendData}}: data,{{/hasData}}
          {{#hasContentType}}contentType: "{{{contentType}}}",{{/hasContentType}});
      {{#withBodyWrapper}}
      return {{{rspType}}}.fromJson(json.decode(rsp.data));
      {{/withBodyWrapper}}
      
      {{^withBodyWrapper}}
      return {{{rspType}}}.fromJson(rsp.data);
      {{/withBodyWrapper}}
    });

    return result;
  }
  {{/functions}}
}

""";
}

generateForAnnotatedElement中解析注解內(nèi)容,并用mustache模板渲染代碼

List<Map<String, dynamic>> functions = [];
List<Map<String, dynamic>> imports = [];
Map<String, bool> importMap = {};

class ApiGenerator extends GeneratorForAnnotation<ApiGen> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    String baseUrl = '';
    /// 注解修飾的是類,注解中可添加baseUrl及生成目標(biāo)類名
    if (element is ClassElement) {
      baseUrl = annotation.peek('url')?.stringValue ?? '';
      print('ClassElement baseUrl : ' + baseUrl);
      if (baseUrl.isEmpty) {
        print('please check annotation url of class : ' + element.name);
        return;
      }
    }

    addDocumentImport(element, buildStep);

    /// 遍歷含有注解的類的成員,本文只處理接口方法注解
    element.visitChildren(SimpleVisitor(buildStep, baseUrl));

    Template tpl = Template(ApiUtilTpl.tpl);
    String content = tpl.renderString({
      'imports': imports,
      'className': annotation.peek('target')?.stringValue,
      'targetClassName': 'ApiBase',
      'functions': functions,
    });
    imports.clear();
    functions.clear();
    importMap.clear();
    return content;
  }

  /// 添加import包信息
  static void addDocumentImport(Element element, BuildStep buildStep) {
    if (element.documentationComment != null) {
      List<String> comments = element.documentationComment.split('\n');
      for (String elem in comments) {
        if (elem?.isNotEmpty ?? false) {
          if (elem.contains('package:')) {
            ApiGenerator.addImport(
                buildStep, elem.substring(elem.indexOf('package')));
          } else if (elem.contains('dart:')) {
            ApiGenerator.addImport(
                buildStep, elem.substring(elem.indexOf('dart')));
          }
        }
      }
    }
  }

  static void addImport(BuildStep buildStep, String path) {
    String result = path;
    if (path.startsWith('/${buildStep.inputId.package}/lib/')) {
      result =
      "package:${buildStep.inputId.package}/${path.replaceFirst('/${buildStep.inputId.package}/lib/', '')}";
    }
    if (!importMap.containsKey(result)) {
      importMap[result] = true;
      print("addImport path[$path]");
      imports.add({"path": result});
    }
  }
}

class SimpleVisitor extends SimpleElementVisitor {
  String _baseUrl;
  BuildStep _buildStep;
  SimpleVisitor(this._buildStep, this._baseUrl);

  @override
  visitMethodElement(MethodElement element) {
    ConstantReader reader = ConstantReader(TypeChecker.fromRuntime(ApiGen).firstAnnotationOf(element));
    if (reader == null) {
      print('firstAnnotationOf ' + element.name + ' is null');
      return;
    }

    Map<String, dynamic> funcInfo = {};
    Map<String, dynamic> defaultParams = {};

    funcInfo['functionDefine'] = element.toString();

    if (element.returnType.isVoid) {
      print('please check return type of method : ' + element.name);
      return;
    }

    var url = reader.peek('url')?.stringValue ?? '';
    if (url.isEmpty) {
      print('please check annotation url of method : ' + element.name);
      return;
    }
    funcInfo['url'] = _baseUrl + url;

    funcInfo["withBodyWrapper"] = false;
    ApiGenerator.addDocumentImport(element, _buildStep);

    var requestName = reader.peek('requestName')?.stringValue ?? '';
    var method = reader.peek('method')?.stringValue ?? '';
    switch (method) {
      case ApiGen.POST:
        requestName = 'post';
        funcInfo['httpSendData'] = true;
        break;

      case ApiGen.GET:
        requestName = 'get';
        funcInfo['httpSendData'] = false;
        break;

      default:
        print('unsupportable method : ' + method);
        return;
    }
    funcInfo['requestName'] = requestName;

    var data = reader.peek('data');
    funcInfo["hasData"] = data != null && data.objectValue != null;
    if (funcInfo["hasData"]) {
      funcInfo["dataType"] = AnnotationUtil.getDataType(data.objectValue);
      funcInfo["dataValue"] = AnnotationUtil.getDataValue(data.objectValue);
    } else {
      if ((element.parameters?.length ?? 0) > 0) {
        funcInfo["hasData"] = true;
        funcInfo["dataValue"] = "{}";
        funcInfo["dataType"] = "Map<String, dynamic>";
      }
    }

    /// 函數(shù)參數(shù),收集有默認值的參數(shù)
    List<Map<String, String>> params = [];
    element.parameters?.forEach((parameterElement) {
      params.add({"paramName": parameterElement.displayName});
      if (parameterElement.defaultValueCode != null) {
        defaultParams[parameterElement.displayName] = parameterElement.defaultValueCode;
      }
    });
    funcInfo["params"] = params;

    /// 函數(shù)參數(shù)有默認值的情況,更新函數(shù)定義
    if (defaultParams.isNotEmpty) {
      Iterator<String> iterator = defaultParams.keys.iterator;
      String funcDef = element.toString();
      while (iterator.moveNext()) {
        String key = iterator.current;
        funcDef = funcDef.replaceFirst(key, key + ' = ' + defaultParams[key]);
      }
      funcInfo["functionDefine"] = funcDef;
    }

    /// 函數(shù)返回值
    DartType returnType = element.returnType;
    funcInfo["returnType"] = returnType.toString();

    /// 返回值為泛型
    if (AnnotationUtil.canHaveGenerics(returnType)) {
      List<DartType> types = AnnotationUtil.getGenericTypes(returnType);
      if (types.length > 1) {
        throw Exception("multiple generics not support!!!");
      }
      funcInfo["rspType"] = types.first.toString();
    }

    /// http contentType
    funcInfo['hasContentType'] = reader.peek('contentType')?.stringValue != null;
    if (funcInfo['hasContentType']) {
      funcInfo['contentType'] = reader.peek('contentType')?.stringValue;
    }

    /// 獲取此函數(shù)需要的引入的包
    /// 返回值的包
    ApiGenerator.addImport(_buildStep, returnType.element.librarySource.fullName);

    /// 返回值為泛型
    if (AnnotationUtil.canHaveGenerics(returnType)) {
      List<DartType> types = AnnotationUtil.getGenericTypes(returnType);
      for (DartType type in types) {
        ApiGenerator.addImport(_buildStep, type.element.librarySource.fullName);
      }
    }
    functions.add(funcInfo);
  }
}

其中類AnnotationUtil代碼如下:

class AnnotationUtil {
  static const String KEEP_NAME_PREFIX = "@C_";

  /// 獲取 DartObject 數(shù)據(jù)值字符串。代碼格式
  static String getDataValue(DartObject dartObject) {
    String result = "";
    if (dartObject.type.isDartCoreMap) {
      Map<DartObject, DartObject> map = dartObject.toMapValue();
      result = "{";
      map.forEach((key, value) {
        result += "\n${getDataValue(key)} : ${getDataValue(value)},";
      });
      result += "\n}";
    } else if (dartObject.type.isDartCoreString) {
      if (dartObject.toStringValue().startsWith(KEEP_NAME_PREFIX)) {
        return dartObject.toStringValue().substring(KEEP_NAME_PREFIX.length, dartObject.toStringValue().length);
      }
      return "\"${dartObject.toStringValue()}\"";
    } else if (dartObject.type.isDartCoreList) {
      List<DartObject> list = dartObject.toListValue();
      result = "[";
      list.forEach((element) {
        result += "\n${getDataValue(element)},";
      });
      result += "\n]";
    } else if (dartObject.type.isDartCoreInt) {
      result = "${dartObject.toIntValue()}";
    } else if (dartObject.type.isDartCoreDouble) {
      result = "${dartObject.toDoubleValue()}";
    } else if (dartObject.type.isDartCoreBool) {
      result = "${dartObject.toBoolValue()}";
    } else if (dartObject.type.isDynamic) {
      result = "${dartObject.toString()}";
    } else {
      throw Exception("data value [${dartObject.type}] not support!!!");
    }
    return result;
  }

  /// 獲取 DartObject 數(shù)據(jù)類型。代碼格式
  static String getDataType(DartObject value) {
    if (value.type.isDartCoreMap) {
      return "Map<String, dynamic>";
    } else if (value.type.isDartCoreString) {
      return "String";
    } else if (value.type.isDartCoreList) {
      return "List";
    } else if (value.type.isDartCoreInt) {
      return "int";
    } else if (value.type.isDartCoreDouble) {
      return "double";
    } else if (value.type.isDartCoreBool) {
      return "bool";
    } else if (value.type.isDynamic) {
      return "dynamic";
    } else {
      throw Exception("data type not support!!!");
    }
  }

  static List<DartType> getGenericTypes(DartType type) {
    return type is ParameterizedType ? type.typeArguments : const [];
  }

  static bool canHaveGenerics(DartType type) {
    final element = type.element;
    if (element is ClassElement) {
      return element.typeParameters.isNotEmpty;
    }
    return false;
  }
}

因為注解中只能使用常量字符串,像AppConfig.COMPANY_ID(1)這種常量,MallData.getAreaCode()這種方法調(diào)用,為了能在生成的代碼中保留原始展示,在注解處理時需要做下特殊處理,避免轉(zhuǎn)換成為常量數(shù)值或者普通字符串。上面代碼中的KEEP_NAME_PREFIX部分處理即是為了保留原始注解值做的處理。

  • 注解用法示例test.api_gen.dart,其中類修飾注解中定義目標(biāo)類名及baseUrl
/// package:package_name/api/api_base.dart
/// package:package_name/api/api_config.dart
@ApiGen('\${ApiConfig.base}', target: 'TestApi')
abstract class ApiInterface {
  /// package:package_name/api/base/constants.dart
  @ApiGen('/cms/page/getAppPage', data: {
    'platformId' : '@C_Constants.PLATFORM_ID'
  })
  Future<GetPageRsp> getAppPage(int pageType, {String pageId});
}

執(zhí)行flutter packages pub run build_runner build --delete-conflicting-outputs后,會自動生成一文件test.api_gen.api.util.dart,其中內(nèi)容如下:

// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// ApiGenerator
// **************************************************************************

import 'package:dio/dio.dart';
import 'package:package_name/api/api_base.dart';
import 'package:package_name/api/api_config.dart';
import 'package:package_name/api/base/constants.dart';
import 'dart:async';
import 'package:package_name/api/get_app_page.dart';

extension TestApi on ApiBase {
  Future<GetPageRsp> getAppPage(int pageType, {String pageId}) {
    Map<String, dynamic> data = {
      "platformId": Constants.PLATFORM_ID,
    };

    if (null != pageType) {
      data["pageType"] = pageType;
    }
    if (null != pageId) {
      data["pageId"] = pageId;
    }

    Future<GetPageRsp> result = new Future(() async {
      Response rsp = await post(
        "${ApiConfig.base}/cms/page/getAppPage",
        data: data,
      );

      return GetPageRsp.fromJson(rsp.data);
    });

    return result;
  }
}

這種生成代碼的方式有一個缺點是,因為代碼需要自動生成,所以編譯時間會稍微變長。示例完整代碼詳見flutter_api_gen

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容