认识混入(Mixin)
Flutter作为目前最火的App跨平台解决方案,对Dart语言的新特性是必须要了解的。Dart中的继承(extends)与OC语言中的继承特性基本一致,Dart中的继承也是单继承。但是Dart有OC语言中没有的特性:混入(Mixin)。混入(Mixin)是Dart中的重要特性,在Dart官网中的定义是Mixins are a way of reusing code in multiple class hierarchies,翻译过来就是“Mixins是一种在多类层次结构中复用代码的一种方式”。允许子类在继承父类时混入其他类,相当于不必成为其子类就可以拥有混入类的功能。
1.1、使用混入
我们来看一下这张关于动物(Animal),哺乳动物(Mammal),鸟(Bird)和鱼(Fish)的继承关系图。
这里面的Animal是一个超类,它有三个子类:Mammal、Bird、Fish。最下面是具体的一些子类。
各种颜色的小方块代表了动物的某些行为:
- 黄色表示具有此行为的类的实例可以步行(walk)。
- 蓝色表示具有此行为的类的实例可以游泳(swim)。
- 灰色表示具有此行为的类的实例可以飞行(fly)。
- 紫色表示具有此行为的类的实例可以唱歌(sing)(就此图来说唱歌(sing)是鸟类(Bird)的专属行为)。
通过上图可以看出,有些动物有共同的行为,比如:猫(cat)和鸭子(Duck)都可以行走,但猫(cat)不能飞也不能游泳也不会唱歌;鸭子(Duck)和飞鱼(Flying Fish)都会游泳和飞,但飞鱼(Flying Fish)不会走和唱歌。
如果一个类可以有多个超类,那么就很容易办到了。但是就算是用继承的方式实现了此功能,这样的设计也会使代码冗余。
我们可以利用混入的方式(Mixin)来完成相应的设计
// 步行
mixin Walker {
void walk(String name){
print("$name is walking");
}
}
// 游泳
mixin Swimmer {
void swim(String name){
print("$name is swimming");
}
}
// 飞行
mixin Flyer {
void fly(String name){
print("$name is flying");
}
}
// 唱歌
mixin Singer {
void sing(String name){
print("$name is singing");
}
}
使用with关键字进行混入,后面可以有一个或多个混入的类名(多个的话使用“,”隔开)。
class Cat extends Mammal with Walker {}
class Duck extends Bird with Walker, Swimmer, Flyer, Singer{}
class FlyingFish extends Fish with Swimmer, Flyer{}
混入的方法就可以调用了
main(List<String> args) {
Cat cat = Cat();
Duck duck = Duck();
cat.walk(cat.runtimeType.toString());
duck.walk(duck.runtimeType.toString());
duck.swim(duck.runtimeType.toString());
duck.fly(duck.runtimeType.toString());
duck.sing(duck.runtimeType.toString());
}
打印结果:
Cat is walking
Duck is walking
Duck is swimming
Duck is flying
Duck is singing
1.2、混入(Mixin)的限制条件
在上面也提到了唱歌是鸟类(Bird)的专属行为,但目前并没有做任何限制,鱼类(Fish)以及子类混入(Minxin)Singer就可以拥有唱歌的能力了,这不是我们想要看到的,这时候就可以使用on关键字来进行限制了。
mixin Singer on Bird{
void sing(String name){
print("$name is singing");
}
}
限制后就只能由鸟类以及子类能够混入(Minxin),其他类混入就会报错。这样就不会被滥用了。
1.3、混入的线性关系
如果混入(Mixin)的类和继承类,或者混入类之间有相同的方法,在调用的时候会产生什么样的情况呢?我们来看一下下面的例子:
超类为类P,类P有一个“getMessage”方法返回值为“P”
混入类为类A和类B,也都有一个“getMessage”方法,返回值分别为“A”和“B”
类AB继承自类P混入(Mixin)类A、类B
类BA继承自类P混入(Mixin)类B、类A
然后打印类AB和类BA实例的“getMessage”方法,打印结果是什么呢?
class A {
String getString() => "A";
}
class B {
String getString() => "B";
}
class P {
String getString() => "P";
}
class AB extends P with A, B {}
class BA extends P with B, A {}
main(List<String> args) {
AB ab = AB();
BA ba = BA();
print(ab.getString());
print(ba.getString());
}
运行结果:BA
因为Dart中的混入(Mixin)是通过创建一个新类来实现,该类将Mixin的实现层叠在一个超类之上创建一个新类,它不是在“超类”中,而是在超类的“顶部”。
这段代码:
class AB extends P with A, B {}
class BA extends P with B, A {}
相当于:
class PA = P with A;
class PAB = PA with B;
class AB extends PAB {}
class PB = P with B;
class PBA = PB with A;
class BA extends PBA {}
最终的继承关系如下所示:
很显然,最后被继承的类重写了上面所有的getMessage方法,处于Mixin结尾的类将前面的getMessage方法都覆盖(override)了。
混入(Mixin)是呈线性的,所以混入(Mixin)的先后顺序非常重要。
2、利用混入解决一些实际问题
2.1、官网demo
在创建第一个Flutter项目的时候,官方会有一个计数器的小Demo,主要代码如下:
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
从上面的代码可以看到所有的逻辑都在_MyHomePageState里面,如果你的业务逻辑比较复杂的话这个State会越来越膨胀。代码的可读性下降,日后维护也会越来越难。这个就和iOS当中的Controller会越来越臃肿类似。那么该如何解决呢?当前的state相当于两个角色:一个是视图和控制器(View和Controller)一个是数据(Model)。解决办法就是对当前的View和Mode进行分层解耦,将不属于View和Controller的职责分离出去。
了解了混入(Mixin)特性后,下面我们利用混入(Mixin)来对官方的demo进行改造,首先声明一个minxin继承自State将与counter有关的逻辑放到这个mixin中。
mixin counter_state_mixin<T extends StatefulWidget> on State<T> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
}
然后将原来的state使用with混入这个mixin,并将counter相关的逻辑使用mixin实现。
class _MyHomePageState extends State<MyHomePage> with counter_state_mixin {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
这样就把View和Model分开了,View相关的逻辑都在State中,而Model相关的逻辑都在mixin中。调用的时候与原来的方式并没有什么差别。由于这个mixin是对State的扩展,所以与生命周期相关的函数如initState(),dispose()等都可以在mixin中重写。
2.2、网络请求
混入(Mixin)不光可以把View和Model进行解耦,也可以作为功能模块来使用,在需要的时候“混入”,从而不会使类的关系变得复杂。
举例来说像网络请求就是一个较为独立的功能,就可以使用混入的方式来完成。
新建一个http_request.dart文件,并mixin一个http_request。这里使用的是睡眠3秒的方式来模拟网络请求,共有两个方法一个get一个post
import 'dart:io';
mixin http_request {
Future http_get(String url, Map<String, dynamic> params) async {
sleep(Duration(seconds: 3));
return "get success";
}
Future http_post(String url, Map<String, dynamic> params) async {
sleep(Duration(seconds: 3));
return "post success";
}
}
在使用的地方混入http_request就可以调用网络请求的方法了:
http_get("htttp://xxxx.xxx", map).then((value) {
print("value is $value");
});
2.3、页面状态
一个独立和使用频次较高的UI组件也可以使用混入的方式来完成。
比如一个页面从请求开始到结束会有不同的状态存在,对于不同的状态会有不同的样式和逻辑。
新建一个http_state.dart,并mixin一个http_state,状态定义如下:
/// 网络请求状态类型
enum ViewState {
loading, // 加载中
success, // 请求成功有数据
empty, // 请求成功无数据
error, // 加载失败
}
http_request_viewstate如下:
mixin http_request_viewstate {
Widget stateView(ViewState viewState) {
switch (viewState) {
case ViewState.loading:
// loading
return Container(
child: Text("loading..."),
);
break;
case ViewState.empty:
// empty
return Container(
child: Text("empty"),
);
break;
case ViewState.error:
// error
return Container(
child: Text("error"),
);
break;
case ViewState.success:
// success
return Container();
break;
default:
return Container();
}
}
}
在使用的地方混入http_request_viewstate,结合之前的网络mixin来模拟网络请求:
Map<String, dynamic> map = Map<String, dynamic>();
http_get("http://xxxx.xxx", map).then((value) {
setState(() {
state = ViewState.empty;
});
});
展示的widget:
stateView(state)
3、总结
利用混入(Mixin)对代码进行了有效的复用,跨越类的层次结构重用代码,也避免了继承的一些困扰。Mixin也可以看作是带实现的Interface。这种设计模式实现了依赖反转功能。当然mixin的方式在实践中也会遇到一些限制:Mixin之间可能会互相依赖;Mixin之间可能存在冲突。了解了混入(Mixin)的特性,就可以在适合的时候使用混入了。
4、参考资料
https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3
Dart开发语言概览:
https://dart.cn/guides/language/language-tour#adding-features-to-a-class-mixins