今天来谈谈 Flutter 的 typedef
。
VoidCallback
之前看 setState
源码的时候,发现它的参数是 VoidCallback
:
void setState(VoidCallback fn) {}
VoidCallback
其实是一个自定义类型的无参数无返回值的匿名函数:
/// Signature of callbacks that have no arguments and return no data.
typedef VoidCallback = void Function();
不得不说 Flutter 官方的命名就是好,光看名字就知道它是做什么的:void 回调(狗头)。
看到它的第一眼我想到的是 OC 的 dispatch_block_t
:
源码:
typedef void (^dispatch_block_t)(void);
文档:
The type of blocks submitted to dispatch queues, which take no argument and have no return value.
也是无参数无返回值的回调。
不能说完全一样,只能说一模一样,毕竟思想都是相通的嘛。
想知道有什么用,先看看官方怎么用,再想想官方为什么这么用。
除了那个 setState
,Flutter 源码中 VoidCallback
的身影经常出现,要么做属性,要么做参数。
为什么要用 VoidCallback
?直接 void Function()
不行吗?
不是不行,VoidCallback
相对 void Function()
,更加简洁,可读性也更强。
因为这种无参数无返回值的回调会经常使用到,所以官方设计了这个自定义类型。
说白了就是代码封装,方便使用。
VoidCallback
相当于是对那种无参数无返回值的回调进行了一层封装,并且给它取了一个名字,下次使用,直接叫它的名字。
比如选择城市页面,选择了城市需要执行回调,将城市名传出去,回调可以这样写:
/// 选择了城市的回调
typedef ChosenCityCallback = void Function(String city);
使用:
class ChooseCityPage extends StatefulWidget {
final ChosenCityCallback chosenCityCallback;
ChooseCityPage({
Key? key,
required this.chosenCityCallback,
}) : super(key: key);
@override
_ChooseCityPageState createState() => _ChooseCityPageState();
}
回调:
ChooseCityPage(chosenCityCallback: (city) {
print(city);
});
注意:如果函数类型特别长或经常使用,那么还是有必要使用 typedef 进行定义。
typedef 定义泛型
typedef ItemView = Function(T t);
使用场景:回调
如下代码:
typedef OnSuccess = void Function(Object o);
typedef OnError = void Function(Exception e);
class HttpCallback {
OnSuccess onSuccess;
OnError onError;
HttpCallback ({OnSuccess this.onSuccess, OnError this.onError});
}
使用:
HttpClient.setCallBack(HttpCallback(
onSuccess: (Object o){
// 请求成功的处理
},
onError: (Exception e) {
// 请求失败的处理
},
));
延伸:
这里我们来围绕“函数作为一等公民,作为对象”这一概念来展开讨论和总结。
1.将一个函数赋值给一个变量.
add(int a, int b) => a + b;
void main() {
var sum = add;
print(sum(1, 2)); // 打印3
}
对于add函数,我们将其赋值给变量sum,然后照样可以通过sum来调用函数,执行两个int整数的求和运算。
2. 将一个函数作为另一个函数的参数,进行传递.
run(operate, a, b) {
return operate(a, b);
}
add(int a, int b) => a + b;
sub(int a, int b) => a - b;
void main() {
print(run(add, 1, 2)); // 打印3
print(run(sub, 1, 2)); // 打印-1
}
注意:run方法按照下面的写法也是可以的,只不过为了简介可以忽略掉前面的声明。
void main() {
print(run(add, 1, 2)); // 打印3
print(run(sub, 1, 2)); // 打印-1
}
run(Function(int, int) operate, a, b) {
return operate(a, b);
}
add(int a, int b) => a + b;
sub(int a, int b) => a - b;
在使用run函数时,我们分别将add函数、sub函数作为它的第一个参数进行传递。
3. 将函数作为另一个函数的返回值.
add(int a, int b) => a + b;
sub(int a, int b) => a - b;
void main() {
print(select(true)(1, 2)); // 打印3
print(select(false)(1, 2)); // 打印-1
}
select(bool isAdd) => isAdd ? add : sub;
直接来看select函数,我们根据isAdd的true或false,来决定是将add函数或sub函数返回
4. 将函数对象存储在数据结构里,比如数组.
add(int a, int b) => a + b;
sub(int a, int b) => a - b;
void main() {
var operations = [add, sub];
operations.forEach((op) {
print(op(1, 2));
});
}
输出结果为:
3
-1
5. 一个有趣的例子,作为闭包概念的引导:
void main() {
int a;
second() {
a = 12;
return a;
}
print(a);
print(second());
print(a);
}
先不看输出结果,我们来看整个代码结构:
我们在main()中定义了一个second(),说明Function是可以嵌套定义的
我们在second()中对外部的局部变量a重新赋值,在Java里,局部内部类里是不允许修改外部的局部变量的。而Dart以及Kotlin这种现代语言,这块是允许直接修改的。输出结果:
null
12
12
6. 闭包。闭包并不是Dart中独有的一个概念,在Js和py中都有,并且Dart中的闭包的概念和JS很像。闭包就是能够读取其他函数内部变量的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
Function makeAdder(num addBy) {
return (num i) => addBy + i;
}
void main() {
var add2 = makeAdder(2);
var add4 = makeAdder(4);
print(add2(3));
print(add4(3));
}
输出结果:
5
7
我们来分析这段程序:
makeAdder函数:接收一个num类型的参数,然后返回了一个匿名Function,这个匿名Function,会接收一个num参数,然后和makeAdder函数的入参进行求和运算。
var add2 = makeAdder(2): 这里add2它实际上是一个Function类型的对象,就是我们前面提到的那个接收一个num参数的匿名Function。所以我们执行add2(3) 自然会得到结果5(2 + 3)。
在makeAdder函数中的addBy参数,作为一个局部变量,只在当前函数可见,但是你会发现,我们通过闭包,在函数外部访问到了函数内部的变量。
我们最后再看一个例子,来加深对闭包的理解:
calculate(base) {
var count = 1;
return () => print('Value is ${base + count++}');
}
void main(){
var f = calculate(2);
f();
f();
}
这里的输出结果又是什么呢?
Value is 3
Value is 4
实际上我们可以这样去看待:一旦形成闭包,闭包函数中对于外部变量的引用,最终会导致闭包函数内,也就是这个Function对象会有一个与之对应的成员变量。结合上面的例子来看,也就是f对象中有两个成员变量:base、count,由于base的值保持不变,count在每次调用中都会自增,那么自然而然会有这样的输出结果。
延伸:详解Flutter中网络框架dio的二次封装
其实dio框架已经封装的很好了,但是在实战项目中,为了项目可以统一管理,还是需要对dio框架进行二次封装。
整体思路:一般情况下,后台返回的数据我们可以分为两部分
状态数据就是接口有没有正常返回数据相关的数据,这部分数据跟业务无关,我们可以封装起来统一管理,渲染数据就是我们渲染页面所需要的数据,这块的数据需要我们自己处理。
接下来我们就主要处理渲染数据这块的内容,我定义了两个函数,渲染数据可能为一个对象或者一个数组,我做了分别处理,定义两个函数来接受渲染数据。
// 定义两个函数
typedef Success = Function(T data); 请求数据data为obj对象
typedef SuccessList = Function(List data); // 请求数据data为[]数组
首先我们需要定义两种数据接口的bean对象,返回对象示例:
/// 响应数据头数据统一管理
class BaseRes {
int? code;// 状态码
String? message; // 状态码说明
T? data; // 渲染数据
BaseRes({this.code, this.message, this.data});
factory BaseRes.fromJson(json) {
// json 渲染json数据
try {
if (json["code"] != null) {
try {
code = json["code"];
} catch (e) {
code = -1;
}
} else {
return BaseRes(code: -1, message: "服务器开小差了~", data: null);
}
return BaseRes(
code: json["code"] ?? -1,
message: json["message"],
data: BeanFactory.generateOBJ(json["data"]));
} catch (e) {
return BaseRes(code: -1, message: "服务器开小差了~", data: null);
}
}
}
返回数组示例省略... 只把 T?data 改为 List
渲染实体类转化:
/// 实体bean转化工厂类
class BeanFactory {
static T? generateOBJ(json) {
//T.toString() 类名
try {
switch (T.toString()) {
case "int":
return json;
case "bool":
return json;
case "String":
return json;
default:
// 实体类序列化
return TestBean.formJson(json) as T;
}
} catch (e) {
return null;
}
}
}
实体类:
/// 测试bean
class TestBean {
String? msg;
bool? isSelector;
TestBean(this.msg,this.isSelector);
TestBean.fromJson(dynamic json) {
msg = json["msg"];
isSelector = json["isSelector"];
}
Map toJson() {
var map = {};
map["msg"] = msg;
map["isSelector"] = isSelector;
return map;
}
}
实际项目中用的JsonToDart插件一键生成即可。(这个插件唯一不好的地方,不能在已生成的文件里序列化,其他都很好)。
接下来就需要进行对dio请求进行二次封装,dio库核心请求方法是request方法,dio的get、post方法都是基于request方法,所以我们要自己再次对request方法进行封装成我们所需要的。
/// dio配置类
class DioManager {
static const baseUrl = "https://xxx"; // 正式环境
static DioManager instance = DioManager._internal();
Dio? _dio;
final Map _headers = {};
// 单例 私有化构造初始化dio
DioManager._internal() {
if (_dio == null) {
BaseOptions options = BaseOptions(
baseUrl: baseUrl,
contentType: Headers.jsonContentType,
responseType: ResponseType.json,
receiveDataWhenStatusError: false,
connectTimeout: _connectTimeout,
receiveTimeout: _receiveTimeout,
headers: _headers);
_dio = Dio(options);
/// 正式环境拦截日志打印
if (!const bool.fromEnvironment("dart.vm.product")) {
_dio?.interceptors
.add(LogInterceptor(requestBody: true, responseBody: true));
}
Future imageToBytes(String imageUrl) async {
var response = await _dio?.get(imageUrl,
options: Options(responseType: ResponseType.bytes));
return Uint8List.fromList(response?.data);
}
get header => _headers;
/// 更新header
updateHeader() {
_dio?.options.headers = _headers;
}
/// 请求,返回的渲染数据 T
/// method:请求方法,NWMethod.GET等
/// path:请求地址
/// params:请求参数
/// success:请求成功回调
/// error:请求失败回调
Future request(Method method, String path,
{String? baseUrl,
Map? params,
data,
ProgressCallback? onSendProgress, // 上传数据进度
ProgressCallback? onReceiveProgress, // 接受数据进度
Success? success,
Function(ErrorRes)? error}) async {
try {
var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
if (error != null) {
error(ErrorRes(code: -9, message: "网络异常~,请检查您的网络状态!"));
}
return;
}
_setBaseUrl(baseUrl); // 动态设置baseUrl
Response? response = await _dio?.request(
path,
queryParameters: params,
data: data,
onSendProgress: onSendProgress,
onReceiveProgress: onReceiveProgress,
options: Options(method: methodValues[method]),
);
if (response != null) {
BaseRes entity = BaseRes.fromJson(response.data);
// 对象数据结构
if (entity.code == 200 && entity.data != null) {
if (success != null) success(entity.data);
} else {
if (error != null) {
error(ErrorRes(code: entity.code, message: entity.message));
}
}
} else {
if (error != null) error(ErrorRes(code: -1, message: "未知错误"));
}
} on DioError catch (e) {
if (error != null) error(createErrorEntity(e));
}
}
Http最终调用类:
typedef Success = Function(T data); // 定义一个函数 请求成功回调
typedef Error = Function(ErrorRes errorRes); // 请求失败统一回调
typedef SuccessList = Function(List data); // 请求数据data为[]集合
/// 通用调用接口类
class Http {
// 私有构造
Http._internal();
static Http instance = Http._internal();
/// get请求
/// baseUrl 切换baseUrl
/// params 参数
/// success 请求对象成功回调
/// successList 请求列表成功回调
/// error 错误回调
/// [isList] 请求的数据是否为List列表 默认对象
/// [isList]=true 请求data数据为[]list列表
void get(String url,
{String? baseUrl,
Map? params,
Success? success,
SuccessList? successList,
Error? error,
bool isList = false}) {
if (isList) {
_requestList(Method.get, url,
baseUrl: baseUrl, params: params, success: successList, error: error);
} else {
_request(Method.get, url,
baseUrl: baseUrl, params: params, success: success, error: error);
}
}
/// post请求
/// baseUrl 切换baseUrl
/// params 参数
/// data 上传表单数据 formData
/// success 请求对象成功回调
/// successList 请求列表成功回调
/// error 错误回调
/// [isList] 请求的数据是否为List列表 默认对象
/// [isList]=true 请求data数据为[]list列表
void post(String url,
{String? baseUrl,
Map? params,
required data,
Success? success,
SuccessList? successList,
Error? error,
bool isList = false}) {
if (isList) {
_requestList(Method.post, url,
data: data,
baseUrl: baseUrl,
params: params,
success: successList,
error: error);
} else {
_request(Method.post, url,
data: data,
baseUrl: baseUrl,
params: params,
success: success,
error: error);
}
}
实战应用调用:
我这边对对业务模块加了一层model处理,页面只需要混入我们的model层调用具体的方法即可。
mixin LoginModel {
// get请求
loadCode(
String value, {
required Success success,
required Error error,
}) {
Http.instance.get(Api.loadxxx,
params: {"key": value}, success: success, error: error);
}
}
可以看到,我们在页面中只需要关注具体的业务请求就可以了。