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 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 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 {
  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: [
            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 on State {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }
}

然后将原来的state使用with混入这个mixin,并将counter相关的逻辑使用mixin实现。

class _MyHomePageState extends State 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: [
            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 params) async {
    sleep(Duration(seconds: 3));
    return "get success";
  }

  Future http_post(String url, Map 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 map = Map();
                    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

你可能感兴趣的:(Flutter中如何使用混入(Mixin)解耦)