在 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 {}
上述例子中,Apple
从 Camera
和 Bluetooth
中都获取 getMessage
方法,但 Dart
必须决定使用哪一个,因为存在冲突。 它从最后使用的 mixin
(即 Bluetooth
)中选择 getMessage
方法。因此,上面输出的是 Bluetooth
。
其实,支持多重继承的编程语言对于这种类型的场景来说很复杂,这被称为钻石问题。但对于 Dart
来说,简单粗暴,直接后面的覆盖前面的。
什么时候使用 mixin
当我们想要在没有相同类层次结构的多个类之间共享行为时,或者当在超类中实现此类行为没有意义时,mixin
非常有用。在序列化(如 json_serializable
)或者持久存储等是非常典型的场景,也可以使用 mixin
来提供一些实用函数(例如 Flutter
中的 RenderSliverHelpers
)。
花点时间尝试使用这个功能,一定会发现新的用例。不要将自己限制在无状态 mixin,你绝对可以存储变量并使用它们。
下面我们来看个用 mixin
封装本地存储的例子。
例子
在 Flutter
应用中,我们肯定会用到本地持久化存储功能保存一些信息,如是否是初次安装打开、当前语言、或者其他业务数据。我们就基于 shared_preferences
用 mixin
来封装这一功能,在保存时加上节流的功能,名字就叫 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';
// ...
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。