Flutter中api实现自动生成
最近公司项目中电商模块从H5迁移为Flutter,在此过程中难免要对之前的接口再实现一遍。
最初设计是定义一单例的ApiUtil,再实现一基于ApiUtil的扩展,在扩展类中添加各个接口
的实现,部分代码如下:
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方法,供其他接口实现时调用。相关模块在调用时使用ApiUtil.inst获取ApiUtil的实例,然后调用扩展中实现的方法。大致接口实现结构如下:
Future<应答类> 方法名(参数...) {
Map<String, dynamic> data = {
...
};
Future<应答类> result = new Future(() async {
Response rsp = await 请求方式(get/post)(
"接口地址",
"请求数据(get->queryParameters, post->data)": data,
);
return 应答类.fromJson(rsp.data);
});
return result;
}
如果有新增接口实现的话,基本可以拷贝其中的接口实现,修改方法返回类型、方法名、参数列表、data中的Map数据、请求方式、接口地址。尽着能少写代码就少写代码的原则, 考虑接口实现部分代码能否自动生成呢?答案是肯定的,通过注解、source_gen和build_runner方式可以实现类似json_serializable那样的代码自动生成。我们在build.yaml中定义的builder,编译时扫描builder下的相关文件,搜集其中的注解,通过generator生成具体代码。以下是相关步骤:
- 1 定义注解类
我们通过注解来生成代码,那么肯定需要在注解中定义要搜集的一些信息。我们的目标是生成一个接口工具类,那么显而易见,首先需要定义一个目标类名,其次接口地址、请求方式、请求数据等也是必须的,具体定义如下:
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;// 目标类名
final String url;// 接口地址
final String method;// 请求方式
final dynamic data;// 请求数据
final String contentType;// 请求数据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 创建注解解析生成器
class ApiGenerator extends GeneratorForAnnotation<ApiGen> {
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
// 解析注解,生成目标代码
}
}
- 3 有了注解解析生成器,得能触发解析才可以执行,于是Builder就登场了。Builder有SharedPartBuilder、PartBuilder、LibraryBuilder,用于处理生成不同的文件。SharedPartBuilder生成.g.dart文件,在源文件中作为part引入,PartBuilder生成.自定义.dart文件,在源文件中作为part引入,LibraryBuilder生成单独的dart文件。因为我们要生成单独的文件,所以使用LibraryBuilder, 其中generatedExtension为生成文件的扩展名
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
当我们执行flutter packages pub run build_runner build --delete-conflicting-outputs执行编译时,build会读取build.yaml中的配置信息,读取到apiBuilder后触发注解生成器ApiGenerator,在GeneratorForAnnotation中调用generate来处理生成代码,可以看到generate中会调用generateForAnnotatedElement,故我们在generateForAnnotatedElement中实现注解相关解析处理即可。接口实现部分代码使用mustache模块来实现,相关语法说明可参考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中解析注解内容,并用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及生成目标类名
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>";
}
}
/// 函数参数,收集有默认值的参数
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;
/// 函数参数有默认值的情况,更新函数定义
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;
}
/// 函数返回值
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;
}
/// 获取此函数需要的引入的包
/// 返回值的包
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 数据值字符串。代码格式
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 数据类型。代码格式
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()这种方法调用,为了能在生成的代码中保留原始展示,在注解处理时需要做下特殊处理,避免转换成为常量数值或者普通字符串。上面代码中的KEEP_NAME_PREFIX部分处理即是为了保留原始注解值做的处理。
- 注解用法示例test.api_gen.dart,其中类修饰注解中定义目标类名及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});
}
执行flutter packages pub run build_runner build --delete-conflicting-outputs后,会自动生成一文件test.api_gen.api.util.dart,其中内容如下:
// 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