Flutter中如何使用混入(Mixin)解耦

认识混入(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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容