1
头图

Flutter 中,我们经常会看到 with 关键字,比如:

class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
  // ...
}

查看源码发现,WidgetsBindingObserver 前面有个 mixin 关键字。

abstract mixin class WidgetsBindingObserver {}

那么 mixin 是干嘛的?with 关键字和 extends 又有什么区别?

什么是 Mixin

mixin is a way to reuse code by allowing classes to inherit behaviours and properties from multiple sources.

mixin 是一种重用代码的方法,它允许类从多个源继承行为和属性。

看到这个定义,大家肯定很困惑,它到底和继承有啥区别。在详细探讨 mixin 之前,我们先来说明两种关系 is-a 关系(继承)和 has-a 关系(组合)。

is-a & has-a

从上图可以很好理解这两种关系:

  • 苹果是手机
  • 苹果手机拥有蓝牙和相机

is-a 关系:两个类的直接关系,其中一个类(假设为A类)是另一个类(假设为B类)的子类,这就是所谓的继承

has-a 关系:两个类之间的关联,其中一个类(假设为A类)包含另一个类(假设为B类)的实例作为其成员之一,这种关联允许A类访问B类的功能和属性,因此成为组合。

我们用代码来说明。

// 父类
class Mobile {}

// 子类
// is-a 关系
class Apple extends Mobile {
  // has-a 关系 
  Bluetooth bluetooth = Bluetooth();
  Camera camera = Camera();
}

通过上面的例子,相信大家已经理解了这两种关系。

Mixin

我们再来看看 mixin 的定义。

mixin is a way to reuse code by allowing classes to inherit behaviours and properties from multiple sources, "without establishing a strict hierarchy of parent child relationship which is what inheritance (is-a relationship) does"

mixin 是一种重用代码的方法,它允许类从多个源继承行为和属性,“无需像继承(is-a 关系)那样建立严格的父子关系层次结构”。

因此,使用 mixin 可以继承所有的属性和方法,但不能称之为子类。

为什么呢?

因为 mixin 在类(使用方)和 mixin 提供的功能之间建立了一种类似于但不完全是 "has-a" 关系的关系。它可以访问 mixin 中定义的方法和属性。

然而,不同之处在于类本身并不保存 mixin 的实例;相反,它只是“继承”了它,不涉及严格的等级制度。 因为它不受严格的父子关系等级的约束。

这种灵活性允许类从 mixin 提供的功能中受益,而无需与它们形成直接的 "is-a" 关系。

mixin Camera {
  String getMessage() => 'Camera';
}

class Apple with  Camera {}

void main() {
  final apple = Apple();
  print(apple.runtimeType is Camera); // false
}

Mixin 的使用

我们来看看在 Dart/Flutter 中如何使用 mixin

基本用法

如前所述,mixin 的声明,直接以 mixin 关键字开头,紧接着该 mixin 的名称。

mixin LoggerMixin {
 void logMessage(String message) { 
   debugPrint("MESSAGE: $message");
  }
}

要在类中使用 mixin,需要使用 with 关键字,后面紧跟 mixin 的名称,这允许我们以非继承的方式使用 mixin 中定义的属性和方法。

class APIService with LoggerMixin {
  void getLists() { 
    try { 
      final response = dio.get('https://www.example.com/lists'); 
    } catch (Exception e) { 
      // Call mixin method 
      logMessage(e.toString()); 
    } 
  } 
}

深入使用

on 关键字

如果你想限制你的 mixin 只在特定类的子类使用,这是我们应该用 on 关键字声明 mixin 类。

请看下面例子。

void main() {
  final apple = Apple();
  print(apple.getMessage()); // Camera
}

mixin Camera on Mobile {
  String getMessage() => 'Camera';
}

class Mobile {}

class Apple extends Mobile with Camera {}

// 'Camera' can't be mixed onto 'Object' because 'Object' doesn't implement 'Mobile'.
class Tv with Camera {}

Camera 限制在 Mobile 类中,只能用于 Mobile 的子类中。因此,Apple 类可以正常使用 mixin 中的功能,而 Tv 类则抛出异常。

混入多个 mixins

用逗号进行分割,请看下面例子。

void main() {
  final apple = Apple();
  apple.useCamera(); // use camera
  apple.useBluetooth(); // use bluetooth
}

mixin Camera {
  void useCamera() {
    print('use camera');
  }
}

mixin Bluetooth {
  void useBluetooth() {
    print('use bluetooth');
  }
}

class Apple with Camera, Bluetooth {}

要是不同的 mixin 中有重名怎么办?后面的覆盖前面的。

void main() {
  final apple = Apple();
  print(apple.getMessage()); // Bluetooth
}

mixin Camera {
  String getMessage() => 'Camera';
}

mixin Bluetooth {
  String getMessage() => 'Bluetooth';
}

class Apple with Camera, Bluetooth {}

上述例子中,AppleCameraBluetooth 中都获取 getMessage 方法,但 Dart 必须决定使用哪一个,因为存在冲突。 它从最后使用的 mixin(即 Bluetooth)中选择 getMessage 方法。因此,上面输出的是 Bluetooth

其实,支持多重继承的编程语言对于这种类型的场景来说很复杂,这被称为钻石问题。但对于 Dart 来说,简单粗暴,直接后面的覆盖前面的。

什么时候使用 mixin

当我们想要在没有相同类层次结构的多个类之间共享行为时,或者当在超类中实现此类行为没有意义时,mixin 非常有用。在序列化(如 json_serializable)或者持久存储等是非常典型的场景,也可以使用 mixin 来提供一些实用函数(例如 Flutter 中的 RenderSliverHelpers)。

花点时间尝试使用这个功能,一定会发现新的用例。不要将自己限制在无状态 mixin,你绝对可以存储变量并使用它们。

下面我们来看个用 mixin 封装本地存储的例子。

例子

Flutter 应用中,我们肯定会用到本地持久化存储功能保存一些信息,如是否是初次安装打开、当前语言、或者其他业务数据。我们就基于 shared_preferencesmixin 来封装这一功能,在保存时加上节流的功能,名字就叫 ThrottledSaveLoadMixin

实现

mixin ThrottledSaveLoadMixin {}

mixin 中肯定需要读和写两个方法。

// 读数据
Future<void> load() async {}
  
// 写数据
Future<void> save() async {}

读写操作需要时,我们模拟从文件中读写数据。

late final _file;

Future<void> load() async {
  final results = await _file.load();
}

Future<void> save() async {
  await _file.save();
}

读的数据是由消费方来使用的,写的数据时也是消费方来确定,因此,还需要声明两个方法。

void copyFromJson(Map<String, dynamic> value);
Map<String, dynamic> toJson();

Future<void> load() async {
  final results = await _file.load();
  copyFromJson(results);
}
  
Future<void> save() async {
  await _file.save(toJson());
}

具体的读写操作我们另一类中来实现。

import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';

class JsonPrefsFile {
  JsonPrefsFile(this.name);
  final String name;

  Future<Map<String, dynamic>> load() async {
    final p = (await SharedPreferences.getInstance()).getString(name);
    return Map<String, dynamic>.from(jsonDecode(p ?? '{}'));
  }

  Future<void> save(Map<String, dynamic> data) async {
    await (await SharedPreferences.getInstance())
        .setString(name, jsonEncode(data));
  }
}

该类接收一个 name 参数作为读写操作的 key,并执行最终读写操作。

因此,我们需要在 mixin 实例化该类,fileName 由消费方确定即可。

late final _file = JsonPrefsFile(fileName);

String get fileName;

最后,我们在 mixin 中再加上节流写入的功能。

final _throttle = Throttler(const Duration(seconds: 2));

Future<void> scheduleSave() async => _throttle.call(save);

ThrottledSaveLoadMixin 完整代码如下。

import 'package:flutter/foundation.dart';

import 'throttler.dart';
import 'json_prefs_file.dart';

mixin ThrottledSaveLoadMixin {
  late final _file = JsonPrefsFile(fileName);
  
  final _throttle = Throttler(const Duration(seconds: 2));

  Future<void> load() async {
    final results = await _file.load();
    try {
      copyFromJson(results);
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

  Future<void> save() async {
    debugPrint('Saving...');
    try {
      await _file.save(toJson());
    } on Exception catch (e) {
      debugPrint(e.toString());
    }
  }

  Future<void> scheduleSave() async => _throttle.call(save);
  
  String get fileName;
  Map<String, dynamic> toJson();
  void copyFromJson(Map<String, dynamic> value);
}

节流类

import 'dart:async';
import 'package:flutter/material.dart';

class Throttler {
  Throttler(this.interval);
  final Duration interval;

  VoidCallback? _action;
  Timer? _timer;

  void call(VoidCallback action, {bool immediateCall = true}) {
    _action = action;
    if (_timer == null) {
      if (immediateCall) {
        _callAction();
      }
      _timer = Timer(interval, _callAction);
    }
  }

  void _callAction() {
    _action?.call();
    _action = null;
    _timer = null;
  }

  void reset() {
    _action = null;
    _timer = null;
  }
}

使用

这里直接上代码了。

import 'package:flutter/material.dart';
import 'save_load_mixin.dart';
// 设置模块
class SettingsController with ThrottledSaveLoadMixin {
  @override
  String get fileName => 'settings.dat';
  
  // 是否初次安装打开
  late final hasCompletedOnboarding = ValueNotifier<bool>(false)
    ..addListener(scheduleSave);
  // 当前语言
  late final currentLocale = ValueNotifier<String?>(null)
    ..addListener(scheduleSave);
    
  // ...

  Future<void> changeLocale(Locale value) async {
    currentLocale.value = value.languageCode;
    // ...
  }

  @override
  void copyFromJson(Map<String, dynamic> value) {
    hasCompletedOnboarding.value = value['hasCompletedOnboarding'] ?? false;
    currentLocale.value = value['currentLocale'];
    // ...
  }

  @override
  Map<String, dynamic> toJson() {
    return {
      'hasCompletedOnboarding': hasCompletedOnboarding.value,
      'currentLocale': currentLocale.value,
      // ...
    };
  }
}
// 车辆模块
class CarController with ThrottledSaveLoadMixin {
  @override
  String get fileName => 'car.dat';
  
  // ...
}

见贤思齐
66 声望8 粉丝

写代码的