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