在每个Flutter开发者的职业生涯中,都会遇到这样的时刻:继承模型开始出现问题。
你为某个需要播放动画的界面创建了一个`StatefulWidget`,并仔细地在其中编写了动画逻辑,同时使用了`SingleTickerProviderStateMixin`这个混合类。几周后,你需要为另一个完全不同的界面也添加动画效果。你考虑过扩展之前的那个组件,但这样做并不合适,因为这两个界面的需求截然不同。于是,你选择了最直接的方法:复制代码。
接着,第三个界面出现了,你又再次复制了相关的代码。现在,你的代码库中出现了三份相同的动画逻辑代码。当你需要修复其中某个错误时,你在其中一个地方进行了修改,却忘记了另外两份代码;当你发布了更新版本后,有用户反馈在另一个界面上出现了崩溃问题。你花了一个小时才弄清楚为什么`vsync`在第二个界面上的表现不同,最终才发现你根本没有更新那两份代码。
这就是“复制粘贴”的陷阱,也是Flutter应用程序中常见的一些隐蔽性错误的根源。出现这种问题的原因并不是开发者粗心大意,而是因为该语言的继承模型并没有提供其他更合理的解决方案。
`StatefulWidget`已经继承自`Widget`,它不能再继承`AnimationController`或其他任何类。与大多数现代编程语言一样,Dart也不支持多重继承——你只能选择一个父类。
但是,如果能够定义一组方法、字段和生命周期钩子,让它们可以被添加到任何需要这些功能的类中,而无需成为该类的父类,那该多好呢?如果动画逻辑、日志记录功能、表单验证规则以及错误报告机制都可以被独立地组织起来,让一个类可以根据自己的需求选择使用其中的一部分,而不必继承整个类,那该多理想啊!
而这正是混合类的作用所在。
混合类是Dart中最强大但也是最容易被忽视的功能之一。Flutter自身的框架中就广泛使用了混合类:`TickerProviderStateMixin`、`AutomaticKeepAliveClientMixin`、`WidgetsBindingObserver`等等,都属于这一类别。每当你在一个组件中写入`with SingleTickerProviderStateMixin`这样的代码时,实际上就是在使用一个混合类。
然而,大多数开发者只是将其当作一种简单的指令来使用,并没有真正理解其背后的原理。因此,在编写自己的代码时,他们很少会想到使用混合类。
这本手册就是为了改变这种状况而编写的。它是一本从基础原理出发、深入讲解如何使用混合类的指南,能帮助你在Flutter应用程序中自信地运用这些工具。通过阅读这本书,你将了解混合类最初被设计出来的目的,明白它们在Dart语言层面上是如何工作的,理解为什么Flutter的框架会采用这样的设计方式,以及如何为自己的实际开发代码设计出清晰、可复用的基于混合类的抽象结构。
通过学习,你不仅会掌握如何使用Flutter提供的混合函数,还会了解如何自己编写混合函数、在什么情况下使用它们,以及在什么情况下应该选择其他方式。同时,你也会学会如何构建代码库,让混合函数为代码的清晰性而非混乱性做出贡献。
目录
-
Dart基础知识:你需要理解类、构造函数、方法、字段以及继承的概念。了解`extends`的作用机制以及Dart类型系统的运作方式是非常重要的。如果你之前曾经定义过自己的Dart类,并且明白`super`的含义,那么你就已经具备了开始学习的条件。
-
Flutter组件基础知识:你需要区分`StatelessWidget`和`StatefulWidget`,并且要理解`State`是一个具有生命周期的类:`initState`、`build`、`dispose`等等。掌握这一生命周期的概念非常重要,因为许多关键的Flutter混合函数都是直接与它相关联的。
-
面向对象编程概念:熟悉继承、接口和多态性等概念,将有助于你理解为什么混合函数在这些工具中占据着独特而重要的地位。你不需要成为面向对象编程的理论专家,但了解`extends`和`implements`在Dart中的作用,会让你更容易理解它们与`with`之间的区别。
-
Flutter SDK 3.x或更高版本
-
Dart SDK 3.x或更高版本(随Flutter一同提供)
-
像VS Code或Android Studio这样的代码编辑器,并且安装了Flutter插件
-
能够在终端中使用`flutter`和`dart`命令行工具
-
DartPad(https://dartpad.dev)对于在不创建完整项目的情况下测试纯Dart混合函数示例来说非常有用
先决条件
在开始学习混合函数之前,你应当已经掌握了某些基础知识。本指南并不要求你在这些领域都是专家,但整个学习过程都会基于这些概念进行。
此外,你还需确保自己的开发环境具备以下条件:
使用混合函数并不需要额外的包,因为它们是Dart语言内置的功能。虽然本指南中的一些示例会用到`flutter_test`这样的标准Flutter包来演示可测试性,但核心功能本身并不需要任何额外的组件。
什么是混合函数?
想象一下各种专业认证:护士可以拥有急救处理、药物给药和伤口护理方面的认证;医生也可以具备急救处理和药物给药的能力;而急救人员则可能专注于急救处理和患者转运工作。
这些专业人士属于完全不同的职业类别,他们的基本职责也各不相同,但他们可以共同拥有某些特定且明确的技能。
这些认证本身并不是人,你不能雇佣一个“认证”;但是你可以将某个认证授予某个人,从那一刻起,这个人就具备了该认证所代表的所有能力。
这种认证机制是自包含的:它明确规定了一组具体的技能要求,而且任何职责与之相匹配的人都可以通过这种认证。
这就是“mixin”的作用。mixin并不是一个可以实例化的类,而是一组功能、字段和方法的集合,你可以将其应用到某个类中。一旦应用了mixin,该类就会获得mixin所包含的所有功能,就像这些功能是直接写在该类内部一样。多个不同的类可以独立地使用同一个mixin,而一个类也可以同时使用多个mixin,这些mixin之间并不需要存在父子关系。
在Dart中,mixin是通过mixin关键字来定义的。它描述了一组字段和方法,你可以使用with关键字将这些内容“混合”到某个类中。使用mixin的类就被认为“融入了”该mixin的功能,从这一刻起,这个类就可以访问mixin所定义的所有内容。
下面是一个最简单的mixin示例:
mixin Greetable {
String get name;
String greet() {
return 'Hello, my name is $name.';
}
}
class Person with Greetable {
@override
final String name;
Person(this.name);
}
void main() {
final person = Person('Ade');
print(person.greet()); // Hello, my name is Ade.
}
具体来说:mixin Greetable声明了一个名为Greetable的mixin。它包含一个getter方法name和一个方法greet。需要注意的是,name在mixin中被定义了,但并没有被实现。
mixin所包含的功能需要依赖使用它的类来提供具体的实现。在class Person with Greetable这个例子中,Person类应用了Greetable mixin,而Person类通过提供一个具体的name字段来实现了name方法。当你调用person.greet()时,Dart会从Greetable mixin中找到greet方法的实现,并使用Person类的name字段来执行这个方法。
这与继承机制有着本质的不同。Person并没有继承Greetable,它们之间也没有父子关系。mixin的功能是在编译时被“编织”到Person类的定义中的。Person仍然只有一个超类,默认情况下这个超类是Object。
为什么Dart需要mixin
Dart的设计采用了单一继承机制,Java、C#、Swift和Kotlin等语言也选择了同样的设计方式。这种设计可以避免多重继承带来的各种问题,尤其是“菱形问题”——当两个父类定义了相同的方法时,子类就无法确定应该使用哪一个方法。
然而,单一继承本身也会带来其他问题:如果你想让不相关的类之间共享代码,就必须强制它们建立人为的父子关系结构。
Dart的mixin正是为了解决这个问题而存在的。它们能够实现多重继承带来的代码共享优势,同时避免了多重继承带来的歧义性问题,因为Dart对于如何解决mixin之间的冲突有严格的规则(我们后面会详细讨论这一点)。
混合模式解决的问题:理解继承的局限性
class Animal { final String name; Animal(this.name); void breathe() { print('$name is breathing.'); } } class Dog extends Animal { Dog(super.name); void bark() { print('$name says: Woof!'); } }
Dog继承了Animal中的breathe方法,同时自己也添加了bark方法。这种设计简洁直观,当各种类型自然地构成层次结构时,这种机制能够很好地发挥作用。
但问题出现在那些类型本身并不构成层次结构,却仍然需要共享某些行为的情况下。
选项一:将所有功能都放在一个基类中”>选项一:将所有功能都放入一个基类中
你可以创建一个继承自State的BaseScreen类,并在其中实现所有需要被多个屏幕共享的功能。然后,所有的屏幕组件都继承这个BaseScreen类。
这种方案起初看起来可行,但问题在于:BaseScreen很快就会变成一个包含600多行代码的“巨型类”,它不仅要负责记录分析数据,还要监控网络连接状态、管理动画生命周期、处理错误报告以及验证表单输入。任何对BaseScreen的修改都可能影响到所有依赖它的屏幕组件。即使只有三个屏幕需要使用某个功能,你也不得不将其添加到这个被所有屏幕共享的基类中。
选项三:直接复制代码”>选项三:直接复制代码
正如引言中提到的,这种做法会导致相同的逻辑被重复编写,久而久之就会产生各种不一致之处和错误。
这些选项没有一个能满足需求。你真正需要的是一种方式,用来说明:“这个功能具有数据分析跟踪功能,那个具有连接状态监控功能,而这个同时具备这两种功能,但它们都没有一个共同的父类来强制规定这种结构。”

混入模式所避免的“菱形问题”
多重继承——即一个类能够同时继承两个父类——看似是显而易见的解决方案。但实际上,它会导致“菱形问题”的出现。

不同的语言采用不同的方式来解决这个问题,但由此带来的困惑程度也各不相同。Dart通过不支持多重继承来完全避免这一问题,同时提供了混入模式作为更为清晰、定义明确的替代方案。
接口与继承之间的差距
Dart确实允许使用implements来实现多个接口。但是,接口只定义了契约规范,并不提供具体的实现代码。如果你实现了某个接口,你就必须自己为该接口中的每一个方法编写实现代码,即使所有使用这个接口的类其实现内容都是相同的。这样一来,虽然可以获得类型安全,但却无法实现代码的重用。
混入模式弥补了接口与继承之间的这一差距。它们既定义了契约规范(即哪些方法或字段存在),也提供了具体的实现代码。使用混入模式的类可以免费获得这些实现细节,而不仅仅是接口的结构框架。
核心混入模式概念:深入探讨
定义一个基本的混入模块
使用mixin关键字就可以定义一个混入模块。在混入模块内部,你可以像在普通类中一样来编写字段、方法和获取器:
mixin Logger {
// 这是由混入模块定义的一个字段。
// 所有使用这个混入模块的类都会拥有自己的>tag字段。
String get tag => runtimeType.toString();
void log(String message) {
print('[\(tag] \)message');
}
void logError(String message, [Object? error]) {
print('[\(tag] ERROR: \)message');
if (error != null) print '[\(tag> Cause: \)error');
}
}
这个名为Logger的混入模块是一段可重复使用的代码片段,你可以将其添加到任何类中,从而为该类添加日志记录功能。它会自动使用类的名称作为标签,并提供了两个方法:log用于打印普通消息,logError用于打印错误信息(以及可选的错误原因)。
现在,任何类都可以通过使用这个混入模块来获得日志记录功能。
class UserRepository with Logger {
Future
UserRepository和AuthService都使用了log和logError方法,但它们并没有继承任何共同的父类。tag获取器使用了runtimeType.toString()这个方法,因此UserRepository使用的日志标签是[UserRepository],而AuthService使用的日志标签是[AuthService],尽管它们都是通过同一个mixin实现的。
`on`关键字:限制mixin的使用范围
有时,某个mixin只适用于特定类型的类。使用`on`关键字,你可以指定这个mixin只能应用于继承或实现了某种特定类型的类。这样,mixin就可以访问这些类型所拥有的成员,而无需重新声明它们。
// 这个mixin仅适用于State对象,因为其中包含了setState、 initState和dispose方法,
//而这些方法只存在于State类中。
mixin ConnectivityMixin
bool _isConnected = true;
// 因为使用了“on State
// 并且可以覆盖initState()和dispose()方法,而不会出现任何错误。
// 这些方法在使用这个mixin的类中肯定是存在的。
@override
void initState() {
super.initState(); // 在覆盖生命周期方法时必须调用super
_startConnectivityListener();
}
@override
void dispose() {
_stopConnectivityListener();
super.dispose();
}
void _startConnectivityListener() {
// 在实际应用中,可以在这里订阅连接状态的变化。
log('开始检测连接状态');
_isConnected = true;
}
void _stopConnectivityListener() {
log('停止检测连接状态');
}
void onConnectivityChanged(bool isConnected) {
setState(() {
_isConnected = isConnected;
});
}
bool get.isConnected => _isConnected;
}
“on StateConnectivityMixin只能被应用到继承自State的类中,这一限制是在编译时就生效的;其次,它让mixin能够完全访问State所提供的所有成员和方法,包括setState、widget、context、mounted,以及initState和dispose等生命周期方法。
Flutter自带的SingleTickerProviderStateMixin就是按照这种方式工作的。它利用“on State”来确保自己只能被应用于State的子类,并且通过覆盖initState和dispose方法来自动管理
具有抽象成员的混合组件
混合组件可以声明一些要求使用它的类去实现的成员。这样就能形成一种强大的契约:混合组件提供了特定的行为,但这种行为取决于该类自身所提供的值或逻辑。
mixin Validatable {
// 这个混合组件声明了这些成员,但并不实现它们。
// 任何使用这个混合组件的类都必须提供相应的实现。
Map get validators;
// 这个混合组件通过上述抽象获取器来实现这一功能。
bool validate(Map formData) {
for (final entry in validators.entries) {
final fieldName = entry.key;
final validatorFn = entry.value;
final fieldValue = formData[fieldName];
final error = validatorFn(fieldValue);
if (error != null) {
onValidationError(fieldName, error);
return false;
}
}
return true;
}
// 另一个抽象成员——类可以自行决定如何处理错误。
void onValidationError(String fieldName, String error);
}
这个Validatable混合组件定义了一个可复用的验证系统。任何类都可以通过提供自己的validators映射和onValidationError方法来使用它。而混合组件本身会负责遍历formData>中的每个字段,应用相应的验证规则,并在遇到第一个错误时停止执行过程,调用onValidationError方法;如果验证失败,则返回false,否则返回true。
现在,任何表单页面都可以使用这个混合组件:
class _LoginScreenState extends State with Validatable {
// 满足混合组件的要求。
@override
Map get validators => {
'email': (value) {
if (value == null || value.isEmpty) return '必须填写电子邮件地址';
if (!value.contains('@')) return '请输入有效的电子邮件地址';
return null;
},
'password': (value) {
if (value == null || value.isEmpty) return '必须填写密码';
if (value.length < 8) return '密码长度至少为8个字符';
return null;
},
};
// 满足混合组件的其他要求。
@override
void onValidationError(String fieldName, String error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('\(fieldName: \)error')),
);
}
void _onSubmit() {
final isValid = validate({
'email': _emailController.text,
'password': _passwordController.text,
});
if (isValid) {
// 继续进行登录操作
}
}
}
这种模式确实非常强大。Validatable混合组件提供了所有的验证逻辑,但具体的规则以及错误处理方式则由使用它的类来决定。这个混合组件可以在任何表单页面中重复使用,而类可以通过实现其中的抽象成员来自定义其行为。
同时使用多个混合组件
一个类可以通过在`with`后面列出这些混合组件,并用逗号分隔,从而同时使用多个混合组件:
mixin Analytics {
void trackEvent(String name, [Map? properties]) {
print('Analytics: \(name \){properties ?? {}}');
}
void trackScreenView(String screenName) {
trackEvent('screen_view', {'screen': screenName});
}
}
mixin ErrorReporter {
void reportError(Object error, StackTrace stackTrace) {
print('报告了错误:$error');
print(stackTrace);
}
}
mixin Logger {
String get tag => runtimeType.toString();
void log(String message) => print('[\(tag] \)message');
}
// 这个类同时使用了这三个混合组件。
class _HomeScreenState extends State
with Logger, Analytics, ErrorReporter {
@override
void initState() {
super.initState();
log('HomeScreen已初始化');
trackScreenView('HomeScreen');
}
Future _loadData() async {
try {
log('正在加载数据...
// ...加载数据的代码...
} catch (error, stackTrace) {
reportError(error, stackTrace);
}
}
}
_HomeScreenState从Logger中获得了`log`方法,从Analytics中获得了`trackEvent`和`trackScreenView`方法,而从ErrorReporter中获得了`reportError`方法——所有这些功能都是通过一个简洁的声明来实现的。使用这些混合组件并不需要重复编写代码,也不需要强行创建复杂的层次结构。
混合组件的线性化排序规则
当多个混合组件被应用到同一个类中时,Dart会通过一种称为“线性化”的机制来解决方法冲突以及调用父类的问题。这种机制能够有效避免“菱形问题”的发生。理解这一机制有助于避免一些隐藏的错误,尤其是当你使用的混合组件覆盖了诸如`initState`或`dispose`这样的生命周期方法时。
Dart会从右到左依次构建一个线性链来决定方法的调用顺序。如果你的类声明如下:
class MyState extends State
with MixinA, MixinB, MixinC { ... }
Dart会按照以下顺序来确定方法的调用顺序:
State -> MixinA -> MixinB -> MixinC -> MyState
优先级规则:具体性越高,优先级越高。
因此:MyState的覆盖方法 -> MixinC的覆盖方法 -> MixinB的覆盖方法 -> MixinA的覆盖方法 -> 父类State的方法
当MyState调用`super.initState()`时,实际上是在调用MixinC中的`initState`方法;而当MixinC再次调用`super initState()`时,则是在调用MixinB中的该方法。依此类推,最终会调用到State中的方法。
正因为如此,任何覆盖了生命周期方法的混合组件都必须在实现中正确地调用`super`方法——这样做不仅仅是为了调用父类的方法,更是为了确保后续的其他混合组件能够按照预期的顺序被执行。
// 这两个混合组件都重写了 initState方法,因此它们都必须调用super.initState()。
mixin MixinA on State {
@override
void initState() {
super.initState(); // 调用State的initState方法
print('MixinA已初始化');
}
}
mixin MixinB on State {
@override
void initState() {
super.initState(); // 由于继承关系,这里会调用MixinA的initState方法
print('MixinB已初始化');
}
}
class MyState extends State with MixinA, MixinB {
@override
void initState() {
super:initState(); // 调用MixinB的initState方法
print('MyState已初始化');
}
}
// 当MyState被初始化时,输出顺序如下:
// MixinA已初始化 (位于继承链的最底层,因此先执行)
// MixinB已初始化
// MyState已初始化 (具体性最高,因此最后执行)
这个例子说明了Dart中的混合组件是如何通过继承链被应用的。在每个混合组件中,initState方法都会调用super.initState():,因此这些方法的执行顺序是从最底层的混合组件开始,逐层向上进行,直到到达最终的类。也就是说,MixinA会首先被执行,然后是MixinB,最后才是MyState;每一层都会通过super.initState()将控制权传递给下一层。

这种确定性的、线性的执行顺序正是Dart混合组件系统安全性的保障。对于哪些方法会在何时被执行,从来都不会存在任何歧义;执行顺序总是由混合组件的排列顺序决定的,而且这个顺序是按照具体性的高低从右向左来确定的。
mixin class的声明方式
Dart 3引入了mixin class这种类型。它既可以作为普通的类来使用(通过new关键字创建实例),也可以作为混合组件来使用(通过with关键字与其他类结合使用)。当你需要一个既能扮演普通类角色,又能充当混合组件的类型时,这种设计方式就非常有用。
// 可以这样使用:`class MyClass extends Serializable` 或者
// `class MyClass with Serializable`
mixin class Serializable {
Map toJson() {
// 这是默认实现方式——子类或混合组件可以对其进行重写
return {};
}
StringtoJsonString() {
return toJson().toString();
}
}
// 作为混合组件使用
class User with Serializable {
final String id;
final String name;
User({required this.id, required this.name});
@override
Map toJson() => {'id': id, 'name': name};
}
// 作为基类使用
class Document extends Serializable {
final String title;
Document({required this.title});
@override
MaptoJson() => {'title': title};
}
与普通的mixin相比,mixin class>的使用频率较低,但在设计库API时,如果希望为使用者提供最大的灵活性,那么mixin class》就显得非常有用了。
抽象混入组件
你也可以直接在混入组件中使用abstract关键字来定义抽象方法,或者简单地声明一些没有实现逻辑的方法。那么使用该混入组件的类就必须自行实现这些方法:
mixin Cacheable {
// 该混入组件要求使用方类提供一个键。
String get cacheKey;
// 该混入组件要求提供缓存失效时间。
Duration get cacheTTL;
// 在抽象要求的基础上实现具体的功能。
bool isCacheExpired(DateTime cachedAt) {
return DateTime.now().difference(cachedAt) > cacheTTL;
}
String buildVersionedKey(int version) {
return '\({cacheKey}_v\)version';
}
}
class UserProfileCache with Cacheable {
@override
String get cacheKey => 'user_profile';
@override
Duration get cacheTTL => const Duration(minutes: 5);
}
这种模式对于在自己的应用程序中构建框架风格的代码来说非常有用。你可以通过定义混入组件来强制规定某些规则(必须实现cacheKey和cacheTTL方法),同时还能免费获得一些可重复使用的逻辑代码(实现了isCacheExpired和buildVersionedKey方法)。
Flutter自带的混入组件
在编写自己的混入组件之前,了解Flutter已经提供的那些混入组件是非常重要的。你很可能已经使用过它们了,但只有理解了为什么这些组件被设计成混入组件的形式,以及它们在State中具体起到了什么作用,才能真正让它们从一些难以理解的代码片段变成实用的工具。
TickerProviderStateMixin与SingleTickerProviderStateMixin
在Flutter中,最常用的混入组件是SingleTickerProviderStateMixin。Flutter中的每一个动画都是由一个Ticker对象来驱动的,这个对象会每帧调用一次回调函数。AnimationController需要一个TickerProvider(即vsync参数),这样才能知道从哪里获取计时信号。
SingleTickerProviderStateMixin使得你的State类本身就变成了一个TickerProvider。它会管理一个与你的组件生命周期紧密相关的Ticker对象:当状态被初始化时,这个计时器会被创建;而当状态被销毁时,这个计时器也会被释放。由于它使用了on State机制,因此你只需要在with语句中添加它即可,无需编写任何额外的代码。
class _AnimatedCardState extends State<AnimatedCard>>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double>> _scaleAnimation;
@override
void initState() {
super.initState();
// `this`被用作vsync参数,因为这个混入组件使得这个State对象实现了
// TickerProvider接口。
_controller = AnimationController(
vsync: this, // -- 这里使用了混入组件的功能
duration: const Duration(milliseconds: 300),
);
_scaleAnimation = Tween<double>>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.elasticOut),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose(); // 你负责释放控制器,而混入组件会处理计时器的销毁工作
superdispose();
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: widget.child,
);
}
}
如果在同一个State中需要使用多个AnimationController,那么应该使用TickerProviderStateMixin(而不是SingleTickerProviderStateMixin),因为后者能够提供无限数量的动画控制器:
class _MultiAnimationState extends State
with TickerProviderStateMixin {
late AnimationController _entranceController;
late AnimationController _pulseController;
@override
void initState() {
super.initState();
_entranceController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_pulseController = AnimationController(
vsync: this,
duration: const Duration(seconds: 1),
)..repeat(reverse: true);
}
@override
void dispose() {
_entranceController.dispose();
_pulseControllerdispose();
super.dispose();
}
}
这两者之间的区别非常重要。SingleTickerProviderStateMixin的效率略高,因为它的内部实现更为简单。当您只需要使用一个动画控制器时,就应该选择它;而当需要使用多个动画控制器时,则应该使用TickerProviderStateMixin。
AutomaticKeepAliveClientMixin
当您滚动ListView或PageView时,Flutter会自动销毁那些已经滚出屏幕的部件,以此来节省内存。这种行为是默认设置,通常也是大家所希望看到的。
但有时,您会遇到某些页面或标签页,其状态需要在用户在不同页面之间切换后仍然保持不变,例如用户正在填写的表单内容或者他们已经到达的滚动位置。
AutomaticKeepAliveClientMixin可以让Flutter的“保持组件存活”机制知道:即使这些部件滚出了屏幕,也不应该被销毁。
class _UserFormState extends State
with AutomaticKeepAliveClientMixin {
// 这个getter方法是该混合类的契约要求。如果希望该组件保持存活,请返回true。
// 如果您需要根据特定条件来决定是否保留组件的存活状态,也可以将这个方法设置为动态的。
@override
bool get wantKeepAlive => true;
final _nameController = TextEditingController();
final _emailController = TextEditingController();
@override
Widget build(BuildContext context) {
// 注意:在使用这个混合类时,必须调用super.build(context)。
// 混合类的super.build方法会将该组件注册到Flutter的“保持存活”系统中。
// 如果不调用这个方法,该混合类将没有任何效果。
super.build(context);
return Column(
children: [
TextField(controller: _nameController, decoration: const InputDecoration(labelText: '姓名')),
TextField控制器: _emailController, decoration: const InputDecoration(labelText: '电子邮件')),
],
);
}
@override
void dispose() {
_nameController.dispose();
_emailControllerdispose();
super.dispose();
}
}
使用这个混合类的两个关键要求是:必须始终实现wantKeepAlive方法,并且必须始终调用super.build(context)方法。如果忽略了其中任何一个要求,该组件保持存活的功能就会失效,而这种问题往往很难诊断出来。
WidgetsBindingObserver
WidgetsBindingObserver从技术上讲是一个抽象类,通常以混合组件的形式被使用(你需要通过传统的混合组件机制来实现它),但在实际使用时,它的功能与普通的混合组件完全相同。这个类能够让你的State访问应用程序的生命周期事件:比如当应用程序进入后台、重新回到前台、设备的文本缩放比例发生变化,或者某个路由被添加或移除时,WidgetsBindingObserver都能接收到这些事件。
class _HomeScreenState extends State
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
// 将这个观察者注册到全局的WidgetsBinding系统中。
// 这样就能将我们的State与Flutter框架的事件系统连接起来。
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
// 在State被销毁之前,一定要先取消注册,这样才能避免在State已经被销毁之后还收到回调事件,
// 因为这可能会导致错误。
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// 当应用程序的生命周期状态发生变化时,会调用这个方法。
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// 应用程序从后台返回到了前台。如果需要的话,可以更新数据。
_refreshData();
break;
case AppLifecycleStatepaused:
// 应用程序即将进入后台。需要保存当前的草稿状态,并停止所有的定时器。
_saveDraft();
break;
case AppLifecycleState.detached:
// 应用程序正在被终止。此时需要进行最后的清理操作。
break;
default:
break;
}
}
// 当用户在系统设置中更改字体大小时,会调用这个方法。
@override
void didChangeTextScaleFactor() {
// 如果需要的话,可以响应文本尺寸的变化。
setState(() {});
}
void _refreshData() {}
void _saveDraft() {}
}
RestorationMixin
RestorationMixin是一个更为高级的Flutter混合组件,它能够实现状态恢复功能:也就是说,当应用程序被操作系统终止并重新启动后,RestorationMixin可以帮助应用程序恢复其之前的UI状态。无论是iOS还是Android,都会在后台终止正在运行的应用程序以释放内存,而RestorationMixin则能确保用户能够回到他们之前所处的界面状态。
class _CounterScreenState extends State
with RestorationMixin {
// RestorableInt是一个特殊的包装类,它知道如何将自己的值序列化到恢复数据包中。
final RestorableInt _counter = RestorableInt(0);
// 根据RestorationMixin的要求,需要为这个状态提供一个唯一的标识符。
@override
String get restorationId => 'counter_screen';
// 同样根据RestorationMixin的要求,需要在这里注册所有可以被恢复的属性。
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_counter, 'counter_value');
}
@override
void dispose() {
_counter.dispose();
superdispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('计数器:${_counter.value}'),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _counter.value++),
child: const IconIcons.add),
),
);
}
}
Flutter混合组件的设计模式
Flutter所有内置的混合组件都遵循相同的架构模式,你在设计自己的混合组件时也应该参照这一模式:
这些混合组件会使用on State(或类似的约束机制)来确保自己仅被应用于合适的类中。它们会重写生命周期方法(initState、dispose、build),从而自动管理资源的分配与释放,这样使用这些混合组件的类就无需手动调用辅助函数了。它们提供的API简洁明了:通常只包含一两个供使用类调用的getter方法或函数。同时,这些混合组件要求使用它们的类实现一些抽象成员,以便根据具体的使用场景来定制它们的行为。
这就是一个设计良好的混合组件的标准:自动处理生命周期相关操作,通过抽象成员进行自定义,同时提供简洁的接口。
架构:混合组件在Flutter应用中的定位
每个混合组件都只负责处理一项定义明确的功能。与状态相关的类中的build方法、业务逻辑调用以及特定组件的行为代码,都不会被日志记录功能或数据分析代码所干扰。这些功能都是由相应的混合组件层在幕后默默处理的。
// 这个混合组件负责处理数据分析功能——这是一种跨层通用功能。
// 它与具体的业务逻辑无关。
mixin ScreenAnalytics on State {
String get screenName;
@override
void initState() {
super.initState();
_trackScreenOpened();
}
@override
void dispose() {
_trackScreenClosed();
super.dispose();
}
void _trackScreenOpened() {
AnalyticsService.instance.track('screen_opened', {
'screen': screenName,
'timestamp': DateTime.now().toIso8601String(),
});
}
void _trackScreenClosed() {
AnalyticsService.instance.track('screen_closed', {
'screen': screenName,
});
}
void trackUserAction(String action, [Map? data]) {
AnalyticsService.instance_track(action, {
'screen': screenName,
...?data,
});
}
}
// Bloc负责处理业务逻辑。
// ScreenAnalytics负责处理数据分析功能。
// State类则将这两者有机地结合在一起。
class _ProductScreenState extends State
with ScreenAnalytics {
@override
String get screenName => 'ProductScreen';
late final ProductBloc _bloc;
@override
void initState() {
super.initState();
// 由于代码执行的顺序是线性的,所以混合组件的initeState方法会先被执行,
// 这样就可以记录屏幕是否被打开;之后这段代码才会被执行。
_bloc = ProductBloc()..add(LoadProduct(widgetproductId));
}
void _onAddToCart(Product product) {
_bloc.add(AddToCart(product));
// 使用混合组件中的方法来记录这个操作。
trackUserAction('add_to_cart', {'product_id': product.id});
}
}
这种分离方式清晰明了,也便于进行测试。你可以独立于任何分析代码或混合代码来测试ProductBloc;同样地,你也可以通过创建一个使用ScreenAnalytics混合代码的简单测试类来单独测试它。这两种测试过程互不干扰,也不会互相影响。
编写自己的混合代码:实用技巧
生命周期混合代码模式
在Flutter中,最有价值的混合代码就是那些与应用程序的生命周期相关的混合代码。它们会监听initState和dispose>方法,从而自动释放资源或初始化所需资源。这种设计有效地避免了 Flutter中最常见的错误来源——忘记释放控制器、数据流订阅或者定时器对象。
下面是一个可用于管理TextEditingController对象的通用混合代码示例:
mixin TextControllerMixin on State {
// 使用该混合代码的类需要指定所需控制的文本控制器数量,
// 这使得混合代码具有灵活性,无需硬编码具体行为。
List get textControllers;
@override
void dispose() {
// 自动释放所有由该类创建的文本控制器对象。
// 使用该混合代码的类完全不需要手动调用dispose()方法。
for (final controller in textControllers) {
controller.dispose();
}
super.dispose();
}
}
// 使用示例:状态类只需声明所需的文本控制器对象,然后将其与TextControllerMixin混合使用即可。
// 资源释放会自动完成,无需人工干预。
class _RegistrationFormState extends State with TextControllerMixin {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
List get textControllers => [
_nameController,
_emailController,
_passwordController,
];
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _nameController),
TextField(controller: _emailController),
TextField控制器: _passwordController),
],
);
}
}
这种设计的好处在于:_RegistrationFormState类绝对不会忘记释放它所创建的文本控制器对象。因为混合代码会自动完成这一操作,因此完全不用担心会出现遗漏。
防抖混合代码模式
在许多场景中,我们都需要实现“防抖”功能:即要延迟执行某个操作,直到用户停止输入信息之后再触发该操作,而不是在用户每次按键时都立即执行它。由于这种逻辑在任何使用它的界面中都是相同的,因此它非常适合作为混合代码来使用:
mixin DebounceMixin on State {
Timer? _debounceTimer;
// 当在`delay`时间间隔内没有再次触发该操作时,才会执行`action`方法。
// 每次调用此方法都会重新启动计时器。
void debounce(VoidCallback action, {Duration delay = const Duration(milliseconds: 500)}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(delay, action);
}
@override
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
}
// 任何需要实现防抖功能的界面都可以免费使用这个混合代码。
class _SearchScreenState extends State with DebounceMixin {
void _onSearchChanged(String query) {
// 这个方法会在用户停止输入500毫秒后才会被执行,而不是在每次按键时都触发。
debounce(() {
context.read().add(SearchQueryChanged(query));
});
}
@override
Widget build(BuildContext context) {
return TextField(
onChanged: _onSearchChanged,
decoration: const InputDecoration(hintText: '搜索...'),
);
}
}
加载状态混合模式
许多屏幕都具有相同的结构:它们可能处于加载状态、错误状态或数据已获取的状态。如果在每个屏幕上都手动管理这三种状态,就会导致代码重复。而混合模式可以帮助实现标准化处理:
mixin LoadingStateMixin on State {
bool _isLoading = false;
Object? _error;
bool get isLoading => _isLoading;
bool get hasError => _error != null;
Object? get error => _error;
// 该混合模式能够自动管理加载状态,适用于任何异步操作。
// 使用该混合模式的类只需调用这个方法,而无需手动处理相关逻辑。
Future runWithLoading(Future Function() operation) async {
if (_isLoading) return null; // 防止重复调用
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await operation();
if (mounted) {
setState(() => _isLoading = false);
}
return result;
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = e;
});
}
return null;
}
}
void clearError() {
setState(() => _error = null);
}
}
// 任何需要获取数据的屏幕都可以免费使用这个混合模式。
class _ProfileScreenState extends State {
with LoadingStateMixin {
User? _user;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final user = await runWithLoading(
() => UserRepository().getUser(widget.userId),
);
if (user != null && mounted) {
setState(() => _user = user);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('错误:$error'),
ElevatedButton(
onPressed: () {
clearError();
_fetchUser();
},
child: const Text('重试'),
],
],
),
);
}
if (_user == null) {
return const Center(child: Text('未找到用户'));
}
return ProfileView(user: _user!);
}
}
}
这个名为LoadingStateMixin的混合模式为任何State类提供了一种内置的方法,使得这些类能够轻松处理加载状态、错误信息以及异步操作,而无需重复编写繁琐的代码。它通过提供isLoading、hasError和error这些获取器,以及runWithLoading方法来实现这一目标——该方法能够自动切换加载状态,并安全地处理操作成功或失败的情况。因此,像_ProfileScreenState这样的屏幕在获取数据时,只需调用runWithLoading即可,然后利用返回的状态值在用户界面中显示加载提示、错误信息或实际内容。
表单验证混合模式
在各类应用程序中,表单验证逻辑几乎是通用的。所有的注册页面、登录页面以及设置页面在提交数据之前都会对用户输入的内容进行验证。
下面是一个可供实际使用的验证混合模块:
mixin FormValidationMixin on State {
final _formKey = GlobalKey();
final Map _fieldErrors = {};
全球经济键 get formKey => _formKey;
Map get fieldErrors => Map.unmodifiable(_fieldErrors);
bool validateForm() {
// 清除所有之前的错误信息
setState(() => _fieldErrors.clear());
final isFormValid = _formKey currentState?.validate() ?? false;
if (!isFormValid) {
onValidationFailed();
}
return isFormValid;
}
void setFieldError(String field, String? error) {
setState(() => _fieldErrors[field] = error);
}
String? getFieldError(String field) => _fieldErrors[field];
bool get hasAnyError => _fieldErrors.values.any((e) => e != null);
// 当表单验证失败时会被调用。这个方法可以被重写,用于显示提示信息、滚动到第一个错误位置,或播放震动动画。
void onValidationFailed() {}
}
这个FormValidationMixin为任何State类提供了一种内置的表单验证管理方式:它通过formKey>来控制表单的状态,存储并显示各字段的错误信息,通过validateForm>方法执行验证操作,并允许该类通过onValidationFailed方法对验证失败的情况作出响应。此外,它还支持手动设置错误信息以及检查是否存在错误,这样用户界面就能保持整洁,同时验证逻辑也能得到集中管理,而不会在各个地方重复出现。
高级概念
混合模式、抽象类与扩展方法的区别
了解何时应该使用混合模式,而不是其他Dart工具,这一点与掌握如何编写混合模式同样重要。每种工具都有其特定的用途。
抽象类用于定义契约并可以提供部分实现,但它们会占用你的一个超级类位置。
当你需要建模“属于……”的关系时,可以使用抽象类:例如Dog属于Animal,PaymentCard属于PaymentMethod。另外,在类型识别非常重要且你希望编写if (payment is PaymentMethod)这样的代码时,也可以使用抽象类。
混合模式则用于定义可复用的行为组件,而不会占用超级类的位置。
当你需要建模“具有……”或“能够执行……”的关系时,应该使用混合模式:例如某个页面“具有数据分析功能”,某个仓库“能够记录日志”,某个表单“具有验证机制”。混合模式适用于那些不涉及类基本定义的通用功能模块。
扩展方法可以在不修改现有类型且无需进行子类化的情况下,为这些类型添加新的方法。
当您希望为某个不属于自己的类型添加实用方法时,就可以使用扩展功能:例如为DateTime类型添加toFormatted()方法,或为String类型添加capitalize()方法。不过,扩展方法不能用于添加字段或覆盖现有的方法。
// 抽象类:用于定义类型的特性
abstract class Shape {
double get area; // 规范要求
double get perimeter; // 规范要求
String describe() => '一个面积为 \({area.toStringAsFixed(2)}\)的 \({runtimeType}';;
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override double get area => 3.14159 * radius * radius;
@override double get perimeter => 2 * 3.14159 * radius;
}
// 混合组件:在不改变类型特性的前提下,为该类型添加新的功能
mixin Drawable {
void draw(Canvas canvas) {
// 默认的绘制逻辑
}
}
// 扩展方法:为现有类型添加实用功能
extension DateTimeFormatting on DateTime {
String get relativeLabel {
final diff = DateTime.now().difference(this);
if (diff.inDays > 0) return '${diff.inDays}天前';
if (diff.inHours > 0) return '${diff.inHours}小时前';
return '${diff.inMinutes}分钟前';
}
}
这段代码展示了在Dart中扩展或定义类型行为的三种不同方式:
-
抽象类Shape定义了一组所有形状都必须遵循的规范,并提供了一个通用的describe方法。
-
像Circle这样的类会实现这些规范,并根据自己的逻辑来计算area和perimeter的值。
-
混合组件Drawable提供了可复用的功能,比如draw方法,这种组件可以添加到任何类中,而不会改变该类的原有特性。
-
扩展方法DateTimeFormatting为DateTime类型添加了relativeLabel辅助方法,这样就可以轻松地得到“2小时前”这样的易读时间标识,而无需修改原始类代码。
混合组件与接口的结合使用
混合组件与implements>关键字可以协同工作,发挥出强大的作用。例如,您可以创建一个混合组件来为某个接口提供默认实现,同时让使用该组件的类仍然能够被多态地使用:
abstract interface Disposable {
void dispose();
}
// 这个混合组件为Disposable接口提供了具体的实现
mixin AutoDispose implements Disposable {
final List〈StreamSubscription〉 _subscriptions = [];
final List〈Timer〉 _timers = [];
void addSubscription(StreamSubscription subscription) {
_subscriptions.add(subscription);
}
void addTimer(Timer timer) {
_timers.add(timer);
}
@override
void dispose() {
for (final sub in _subscriptions) {
sub.cancel();
}
for (final timer in _timers) {
timer.cancel();
}
_subscriptions.clear();
_timers.clear();
}
}
class DataService with AutoDispose {
DataService() {
// 注册资源,这些资源会在dispose()被调用时被清理
addSubscription(
someStream.listen((data) => handleData(data)),
);
addTimer(
Timer(periodic(const Duration(minutes: 1), (_) => refresh()),
);
}
}
// 因为AutoDispose实现了Disposable接口,所以这段代码可以正常工作
void cleanUp(Disposable resource) {
resourcedispose();
}
这段代码定义了一个Disposable接口,该接口要求实现一个dispose方法,同时提供了一个AutoDispose混合类,该混合类通过跟踪订阅关系和定时器来自动清理这些资源,从而实现了这一接口。
因此,任何使用这个混合类的类(比如DataService),都可以通过addSubscription和addTimer方法来注册相关资源,当调用dispose方法时,所有这些资源都会被安全地清理掉;同时,在任何需要使用Disposable接口的地方,这类类仍然可以正常使用。
在隔离环境中测试混合类
混合类的一个非常重要的架构优势在于它们可以被独立地进行测试。你不需要创建一个完整的Flutter组件来测试某个混合类的行为,只需创建一个使用该混合类的最小化测试类,然后直接对其进行测试即可:
// test/mixinsloading_state_mixin_test.dart
import 'packageflutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
// 这是一个使用该混合类的简单模拟状态类——不需要真正的组件。
class TestLoadingState extends State
with LoadingStateMixin {
@override
Widget build(BuildContext context) => const SizedBox();
}
void main() {
group('LoadingStateMixin', () {
testWidgets('开始时处于非加载状态', (tester) async {
final state = TestLoadingState();
expect(state isLoading, false);
expect(state.hasError, false);
expect.state.error, null);
});
testWidgets('在操作过程中将加载状态设置为“正在加载”’, (tester) async {
await tester.pumpWidget(
MaterialApp(home: StatefulBuilder(
builder: (context, setState) {
return const SizedBox();
},
),
);
// 通过组件测试框架来验证该混合类的行为
// ...
});
test('debounce混合类会取消之前的定时器’, () async {
// 纯粹的Dart测试——不需要组件测试框架
int callCount = 0;
// 测试debounce机制的行为
// ...
});
});
}
这个测试文件展示了如何使用Flutter的测试工具来验证LoadingStateMixin。具体方法是创建一个使用该混合类的简单模拟State类,然后检查它在开始时是否没有处于加载状态或出现错误,并且在操作过程中是否能正常工作。此外,它还说明了有些行为可以通过完整的组件测试来验证,而有些行为(比如debounce逻辑)则可以通过纯粹的Dart测试来验证。
对于那些不依赖于State的纯Dart混合类来说,测试过程会更加简单,因为完全不需要使用Flutter的组件测试框架:
// 一个不依赖Flutter的纯Dart混合类
mixin Serializable {
Map toJson();
StringtoJsonString() => toJson().toString();
bool isEquivalentTo(Serializable other) {
return.toJson().toString() == other.toJson().toString();
}
}
// 使用纯粹的Dart测试来验证它
class TestModel with Serializable {
final String name;
TestModel(this.name);
@override
Map toJson() => {'name': name};
}
void main() {
test('Serializable.isEquivalentTo方法的比较结果是正确的', () {
final a = TestModel('Ade');
final b = TestModel('Ade');
final c = TestModel('Chioma');
expect(a.isEquivalentTo(b), true);
expect(a.isEquivalentTo(c), false);
});
}
这段代码定义了一个名为Serializable的Dart混合组件,任何使用它的类都必须实现toJson方法。该混合组件还提供了辅助方法,用于将数据转换为字符串,并通过对象的JSON表示形式来比较两个对象。这样,就可以简单地判断两个对象是否相等。
TestModel类通过实现toJson方法展示了这一机制的实际效果。测试验证了:具有相同数据的对象被视为相等,而数据不同的对象则不会被认定为相等。
性能考量
与直接在类中编写相同的代码相比,使用混合组件并不会增加运行时的开销。Dart会在编译时完成混合组件的合并处理,而非在运行时进行。最终生成的类,就如同你直接将混合组件中的所有方法和字段写在了该类中一样。不存在动态调度、代理层或虚拟方法表带来的额外开销,其性能表现与使用等效的类层次结构时相同。
唯一可能影响性能的情况是,当你在热点代码路径中使用了过于复杂的混合组件链(即一个类上应用了十个或多个混合组件)时。此时,问题并不在于混合组件本身,而在于每次调用这些组件时会执行大量代码。良好的混合组件设计应确保每个组件只承担一项特定的职责,这样就能避免这种性能问题。
一个混合组件,一个职责
这种分离方式清晰明了,也便于进行测试。你可以独立于任何分析代码或混合代码来测试ProductBloc;同样地,你也可以通过创建一个使用ScreenAnalytics混合代码的简单测试类来单独测试它。这两种测试过程互不干扰,也不会互相影响。
编写自己的混合代码:实用技巧
生命周期混合代码模式
在Flutter中,最有价值的混合代码就是那些与应用程序的生命周期相关的混合代码。它们会监听initState和dispose>方法,从而自动释放资源或初始化所需资源。这种设计有效地避免了 Flutter中最常见的错误来源——忘记释放控制器、数据流订阅或者定时器对象。
下面是一个可用于管理TextEditingController对象的通用混合代码示例:
mixin TextControllerMixin on State {
// 使用该混合代码的类需要指定所需控制的文本控制器数量,
// 这使得混合代码具有灵活性,无需硬编码具体行为。
List get textControllers;
@override
void dispose() {
// 自动释放所有由该类创建的文本控制器对象。
// 使用该混合代码的类完全不需要手动调用dispose()方法。
for (final controller in textControllers) {
controller.dispose();
}
super.dispose();
}
}
// 使用示例:状态类只需声明所需的文本控制器对象,然后将其与TextControllerMixin混合使用即可。
// 资源释放会自动完成,无需人工干预。
class _RegistrationFormState extends State with TextControllerMixin {
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
List get textControllers => [
_nameController,
_emailController,
_passwordController,
];
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(controller: _nameController),
TextField(controller: _emailController),
TextField控制器: _passwordController),
],
);
}
}
这种设计的好处在于:_RegistrationFormState类绝对不会忘记释放它所创建的文本控制器对象。因为混合代码会自动完成这一操作,因此完全不用担心会出现遗漏。
防抖混合代码模式
在许多场景中,我们都需要实现“防抖”功能:即要延迟执行某个操作,直到用户停止输入信息之后再触发该操作,而不是在用户每次按键时都立即执行它。由于这种逻辑在任何使用它的界面中都是相同的,因此它非常适合作为混合代码来使用:
mixin DebounceMixin on State {
Timer? _debounceTimer;
// 当在`delay`时间间隔内没有再次触发该操作时,才会执行`action`方法。
// 每次调用此方法都会重新启动计时器。
void debounce(VoidCallback action, {Duration delay = const Duration(milliseconds: 500)}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(delay, action);
}
@override
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
}
// 任何需要实现防抖功能的界面都可以免费使用这个混合代码。
class _SearchScreenState extends State with DebounceMixin {
void _onSearchChanged(String query) {
// 这个方法会在用户停止输入500毫秒后才会被执行,而不是在每次按键时都触发。
debounce(() {
context.read().add(SearchQueryChanged(query));
});
}
@override
Widget build(BuildContext context) {
return TextField(
onChanged: _onSearchChanged,
decoration: const InputDecoration(hintText: '搜索...'),
);
}
}
加载状态混合模式
许多屏幕都具有相同的结构:它们可能处于加载状态、错误状态或数据已获取的状态。如果在每个屏幕上都手动管理这三种状态,就会导致代码重复。而混合模式可以帮助实现标准化处理:
mixin LoadingStateMixin on State {
bool _isLoading = false;
Object? _error;
bool get isLoading => _isLoading;
bool get hasError => _error != null;
Object? get error => _error;
// 该混合模式能够自动管理加载状态,适用于任何异步操作。
// 使用该混合模式的类只需调用这个方法,而无需手动处理相关逻辑。
Future runWithLoading(Future Function() operation) async {
if (_isLoading) return null; // 防止重复调用
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await operation();
if (mounted) {
setState(() => _isLoading = false);
}
return result;
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = e;
});
}
return null;
}
}
void clearError() {
setState(() => _error = null);
}
}
// 任何需要获取数据的屏幕都可以免费使用这个混合模式。
class _ProfileScreenState extends State {
with LoadingStateMixin {
User? _user;
@override
void initState() {
super.initState();
_fetchUser();
}
Future _fetchUser() async {
final user = await runWithLoading(
() => UserRepository().getUser(widget.userId),
);
if (user != null && mounted) {
setState(() => _user = user);
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('错误:$error'),
ElevatedButton(
onPressed: () {
clearError();
_fetchUser();
},
child: const Text('重试'),
],
],
),
);
}
if (_user == null) {
return const Center(child: Text('未找到用户'));
}
return ProfileView(user: _user!);
}
}
}
这个名为LoadingStateMixin的混合模式为任何State类提供了一种内置的方法,使得这些类能够轻松处理加载状态、错误信息以及异步操作,而无需重复编写繁琐的代码。它通过提供isLoading、hasError和error这些获取器,以及runWithLoading方法来实现这一目标——该方法能够自动切换加载状态,并安全地处理操作成功或失败的情况。因此,像_ProfileScreenState这样的屏幕在获取数据时,只需调用runWithLoading即可,然后利用返回的状态值在用户界面中显示加载提示、错误信息或实际内容。
表单验证混合模式
在各类应用程序中,表单验证逻辑几乎是通用的。所有的注册页面、登录页面以及设置页面在提交数据之前都会对用户输入的内容进行验证。
下面是一个可供实际使用的验证混合模块:
mixin FormValidationMixin on State {
final _formKey = GlobalKey();
final Map _fieldErrors = {};
全球经济键 get formKey => _formKey;
Map get fieldErrors => Map.unmodifiable(_fieldErrors);
bool validateForm() {
// 清除所有之前的错误信息
setState(() => _fieldErrors.clear());
final isFormValid = _formKey currentState?.validate() ?? false;
if (!isFormValid) {
onValidationFailed();
}
return isFormValid;
}
void setFieldError(String field, String? error) {
setState(() => _fieldErrors[field] = error);
}
String? getFieldError(String field) => _fieldErrors[field];
bool get hasAnyError => _fieldErrors.values.any((e) => e != null);
// 当表单验证失败时会被调用。这个方法可以被重写,用于显示提示信息、滚动到第一个错误位置,或播放震动动画。
void onValidationFailed() {}
}
这个FormValidationMixin为任何State类提供了一种内置的表单验证管理方式:它通过formKey>来控制表单的状态,存储并显示各字段的错误信息,通过validateForm>方法执行验证操作,并允许该类通过onValidationFailed方法对验证失败的情况作出响应。此外,它还支持手动设置错误信息以及检查是否存在错误,这样用户界面就能保持整洁,同时验证逻辑也能得到集中管理,而不会在各个地方重复出现。
高级概念
混合模式、抽象类与扩展方法的区别
了解何时应该使用混合模式,而不是其他Dart工具,这一点与掌握如何编写混合模式同样重要。每种工具都有其特定的用途。
抽象类用于定义契约并可以提供部分实现,但它们会占用你的一个超级类位置。
当你需要建模“属于……”的关系时,可以使用抽象类:例如Dog属于Animal,PaymentCard属于PaymentMethod。另外,在类型识别非常重要且你希望编写if (payment is PaymentMethod)这样的代码时,也可以使用抽象类。
混合模式则用于定义可复用的行为组件,而不会占用超级类的位置。
当你需要建模“具有……”或“能够执行……”的关系时,应该使用混合模式:例如某个页面“具有数据分析功能”,某个仓库“能够记录日志”,某个表单“具有验证机制”。混合模式适用于那些不涉及类基本定义的通用功能模块。
扩展方法可以在不修改现有类型且无需进行子类化的情况下,为这些类型添加新的方法。
当您希望为某个不属于自己的类型添加实用方法时,就可以使用扩展功能:例如为DateTime类型添加toFormatted()方法,或为String类型添加capitalize()方法。不过,扩展方法不能用于添加字段或覆盖现有的方法。
// 抽象类:用于定义类型的特性
abstract class Shape {
double get area; // 规范要求
double get perimeter; // 规范要求
String describe() => '一个面积为 \({area.toStringAsFixed(2)}\)的 \({runtimeType}';;
}
class Circle extends Shape {
final double radius;
Circle(this.radius);
@override double get area => 3.14159 * radius * radius;
@override double get perimeter => 2 * 3.14159 * radius;
}
// 混合组件:在不改变类型特性的前提下,为该类型添加新的功能
mixin Drawable {
void draw(Canvas canvas) {
// 默认的绘制逻辑
}
}
// 扩展方法:为现有类型添加实用功能
extension DateTimeFormatting on DateTime {
String get relativeLabel {
final diff = DateTime.now().difference(this);
if (diff.inDays > 0) return '${diff.inDays}天前';
if (diff.inHours > 0) return '${diff.inHours}小时前';
return '${diff.inMinutes}分钟前';
}
}
这段代码展示了在Dart中扩展或定义类型行为的三种不同方式:
-
抽象类
Shape定义了一组所有形状都必须遵循的规范,并提供了一个通用的describe方法。 -
像
Circle这样的类会实现这些规范,并根据自己的逻辑来计算area和perimeter的值。 -
混合组件
Drawable提供了可复用的功能,比如draw方法,这种组件可以添加到任何类中,而不会改变该类的原有特性。 -
扩展方法
DateTimeFormatting为DateTime类型添加了relativeLabel辅助方法,这样就可以轻松地得到“2小时前”这样的易读时间标识,而无需修改原始类代码。
混合组件与接口的结合使用
混合组件与implements>关键字可以协同工作,发挥出强大的作用。例如,您可以创建一个混合组件来为某个接口提供默认实现,同时让使用该组件的类仍然能够被多态地使用:
abstract interface Disposable {
void dispose();
}
// 这个混合组件为Disposable接口提供了具体的实现
mixin AutoDispose implements Disposable {
final List〈StreamSubscription〉 _subscriptions = [];
final List〈Timer〉 _timers = [];
void addSubscription(StreamSubscription subscription) {
_subscriptions.add(subscription);
}
void addTimer(Timer timer) {
_timers.add(timer);
}
@override
void dispose() {
for (final sub in _subscriptions) {
sub.cancel();
}
for (final timer in _timers) {
timer.cancel();
}
_subscriptions.clear();
_timers.clear();
}
}
class DataService with AutoDispose {
DataService() {
// 注册资源,这些资源会在dispose()被调用时被清理
addSubscription(
someStream.listen((data) => handleData(data)),
);
addTimer(
Timer(periodic(const Duration(minutes: 1), (_) => refresh()),
);
}
}
// 因为AutoDispose实现了Disposable接口,所以这段代码可以正常工作
void cleanUp(Disposable resource) {
resourcedispose();
}
这段代码定义了一个Disposable接口,该接口要求实现一个dispose方法,同时提供了一个AutoDispose混合类,该混合类通过跟踪订阅关系和定时器来自动清理这些资源,从而实现了这一接口。
因此,任何使用这个混合类的类(比如DataService),都可以通过addSubscription和addTimer方法来注册相关资源,当调用dispose方法时,所有这些资源都会被安全地清理掉;同时,在任何需要使用Disposable接口的地方,这类类仍然可以正常使用。
在隔离环境中测试混合类
混合类的一个非常重要的架构优势在于它们可以被独立地进行测试。你不需要创建一个完整的Flutter组件来测试某个混合类的行为,只需创建一个使用该混合类的最小化测试类,然后直接对其进行测试即可:
// test/mixinsloading_state_mixin_test.dart
import 'packageflutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
// 这是一个使用该混合类的简单模拟状态类——不需要真正的组件。
class TestLoadingState extends State
with LoadingStateMixin {
@override
Widget build(BuildContext context) => const SizedBox();
}
void main() {
group('LoadingStateMixin', () {
testWidgets('开始时处于非加载状态', (tester) async {
final state = TestLoadingState();
expect(state isLoading, false);
expect(state.hasError, false);
expect.state.error, null);
});
testWidgets('在操作过程中将加载状态设置为“正在加载”’, (tester) async {
await tester.pumpWidget(
MaterialApp(home: StatefulBuilder(
builder: (context, setState) {
return const SizedBox();
},
),
);
// 通过组件测试框架来验证该混合类的行为
// ...
});
test('debounce混合类会取消之前的定时器’, () async {
// 纯粹的Dart测试——不需要组件测试框架
int callCount = 0;
// 测试debounce机制的行为
// ...
});
});
}
这个测试文件展示了如何使用Flutter的测试工具来验证LoadingStateMixin。具体方法是创建一个使用该混合类的简单模拟State类,然后检查它在开始时是否没有处于加载状态或出现错误,并且在操作过程中是否能正常工作。此外,它还说明了有些行为可以通过完整的组件测试来验证,而有些行为(比如debounce逻辑)则可以通过纯粹的Dart测试来验证。
对于那些不依赖于State的纯Dart混合类来说,测试过程会更加简单,因为完全不需要使用Flutter的组件测试框架:
// 一个不依赖Flutter的纯Dart混合类
mixin Serializable {
Map toJson();
StringtoJsonString() => toJson().toString();
bool isEquivalentTo(Serializable other) {
return.toJson().toString() == other.toJson().toString();
}
}
// 使用纯粹的Dart测试来验证它
class TestModel with Serializable {
final String name;
TestModel(this.name);
@override
Map toJson() => {'name': name};
}
void main() {
test('Serializable.isEquivalentTo方法的比较结果是正确的', () {
final a = TestModel('Ade');
final b = TestModel('Ade');
final c = TestModel('Chioma');
expect(a.isEquivalentTo(b), true);
expect(a.isEquivalentTo(c), false);
});
}
这段代码定义了一个名为Serializable的Dart混合组件,任何使用它的类都必须实现toJson方法。该混合组件还提供了辅助方法,用于将数据转换为字符串,并通过对象的JSON表示形式来比较两个对象。这样,就可以简单地判断两个对象是否相等。
TestModel类通过实现toJson方法展示了这一机制的实际效果。测试验证了:具有相同数据的对象被视为相等,而数据不同的对象则不会被认定为相等。
性能考量
与直接在类中编写相同的代码相比,使用混合组件并不会增加运行时的开销。Dart会在编译时完成混合组件的合并处理,而非在运行时进行。最终生成的类,就如同你直接将混合组件中的所有方法和字段写在了该类中一样。不存在动态调度、代理层或虚拟方法表带来的额外开销,其性能表现与使用等效的类层次结构时相同。
唯一可能影响性能的情况是,当你在热点代码路径中使用了过于复杂的混合组件链(即一个类上应用了十个或多个混合组件)时。此时,问题并不在于混合组件本身,而在于每次调用这些组件时会执行大量代码。良好的混合组件设计应确保每个组件只承担一项特定的职责,这样就能避免这种性能问题。
一个混合组件,一个职责
混合组件设计中最重要的一条规则是:每个混合组件都应只承担一项职责。如果一个名为ScreenBehavior的混合组件同时负责数据分析、网络连接处理、日志记录和数据验证等功能,那么它就不是一个真正的混合组件——实际上它就是一个“功能过于复杂的类”。
当你发现自己在现有的混合组件中添加了不相关的功能时,就应该考虑将其拆分开来。
// 错误的做法:一个混合组件承担过多职责
mixin ScreenBehavior on State {
void trackEvent(String name) { /* ... */ } // 数据分析
bool get isConnected { /* ... */ } // 网络连接处理
void log(String msg) { /* ... */ } // 日志记录
bool validateEmail(String e) { /* ... */ } // 数据验证
void showSnackBar(String msg) { /* ... */ } // 用户界面交互
}
// 正确的做法:将每个职责分别放入单独的混合组件中
mixin ScreenAnalytics on State {
void trackEvent(String name) { /* ... */ }
}
mixin ConnectivityAware on State {
bool get isConnected { /* ... */ }
}
mixin Logger {
void log(String msg) { /* ... */ }
}
这个例子说明了,第一个混合组件ScreenBehavior承担了太多不相关的职责,导致它既难以维护,也不便于复用。正确的做法是将这些职责分别分配到不同的混合组件中,比如ScreenAnalytics、ConnectivityAware和Logger,这样每个组件都能专注于自己的功能,并且只有在真正需要的地方才会被组合使用。
在生命周期方法中务必调用super
当某个混合组件覆盖了某个生命周期方法时,调用super是必不可少的:这是混合组件组合机制能够正常运行的基础。如果没有super,整个执行流程就会中断,链式结构中的其他混合组件也无法执行它们的生命周期代码。
mixin SomeMixin on State {
@override
void initState() {
super.initState(); // 必须首先调用super,而且必须在自己的代码之前调用它
// 在这里编写初始化代码
}
@override
void dispose() {
// 在这里编写清理代码
super.dispose(); // 在dispose方法中,应在清理操作之后最后调用super
}
}
在Flutter中,有一个约定:在initState>方法中要先调用super,而在dispose>方法中则要在最后调用super。这样的设计与State>本身的工作方式是一致的,能够确保资源在使用之前被正确配置,在父组件被销毁之前被及时清理。
混合组件的项目结构
在正式的生产代码库中,将混合组件放在专门的位置存放,这样它们就更容易被找到,也便于人们理解它们的用途:
lib/
mixins/
analytics_mixin.dart -- 屏幕分析功能
connectivity_mixin.dart -- 网络状态监控
debounce_mixin.dart -- 输入延迟处理
form_validation_mixin.dart -- 表单验证逻辑
loading_state_mixin.dart -- 加载/错误/数据状态管理
logger_mixin.dart -- 结构化日志记录
lifecycle_logger_mixin.dart -- 记录initState和dispose方法的调用情况
screens/
home/
home_screen.dart -- 使用分析功能、网络连接功能和日志记录功能
search/
search_screen.dart -- 使用输入延迟处理功能和加载状态管理功能
settings/
settingscreen.dart -- 使用表单验证功能和加载状态管理功能
将混合组件与具体的屏幕文件分开存放,可以让人们更容易找到它们,也便于进行测试,并且可以在整个项目中统一使用这些混合组件,而无需在各个屏幕文件的目录中寻找它们。
按照功能来命名混合组件,而不是根据使用它们的组件来命名
混合组件的名称应该反映其所实现的功能或提供的行为,而不是具体使用它的组件。因此,应该按照功能来为它们命名:
// 错误的命名方式:以特定组件来命名混合组件
mixin HomeScreenAnalytics { }
mixin LoginFormValidation { }
mixin DashboardConnectivity { }
// 正确的命名方式:根据功能来命名混合组件
mixin ScreenAnalytics { }
mixin FormValidation { }
mixin ConnectivityAware { }
如果按照功能来命名混合组件,当开发者搜索“是否有某个混合组件提供分析跟踪功能?”时,就能很容易地找到相应的组件;而如果按照使用它的组件来命名,就很难通过这种方式找到所需的混合组件。
明确说明混合组件的使用规范
那些使用了抽象成员或对使用它们的类提出了特定要求的混合组件,都应该清楚地说明这些要求。使用这些混合组件的开发者应该清楚自己需要实现哪些功能。
/// 一种能够自动跟踪屏幕分析数据的混合组件。
///
/// 使用方法:
/// ```dart
/// class _MyScreenState extends State
/// with ScreenAnalyticsMixin {
/// @override
/// String get screenName => 'MyScreen';
/// }
/// ```
///
/// 所需参数:
/// - [screenName]:用于标识当前屏幕的稳定且唯一的名称。
/// 该名称会在所有分析数据调用中作为事件属性被使用。
///
/// 提供的功能:
/// - 在 ` initState` 方法执行时自动触发 `screen_opened` 事件。
/// - 在 `dispose` 方法执行时自动触发 `screen_closed` 事件。
/// - [trackAction]:允许手动跟踪用户操作相关事件。
mixin ScreenAnalyticsMixin on State {
String get screenName;
@override
void initState() {
super.initState();
_track('screen_opened');
}
@override
void dispose() {
_track('screen_closed');
super.dispose();
}
void trackAction(String action, [Map? data]) {
_track(action, data);
}
void _track(String event, [Map? data]) {
AnalyticsService.instance.track(event, {
'screen': screenName,
...?data,
});
}
}
何时使用混合组件,何时不应使用它们
混合组件的适用场景
当某些功能具有跨类应用的特性时,使用混合组件是最佳选择。也就是说,这些功能并不定义需要使用它们的类的本质属性,但必须在多个无关的类中共同使用。
在 Flutter 应用中,这类跨类需求主要包括生命周期相关的操作,例如数据分析、日志记录、网络连接状态监控以及状态恢复等。这些功能是许多屏幕都需要的,而且在所有屏幕上几乎都是相同的,它们与各个屏幕之间的差异并无关联。
此外,当你希望通过默认实现来强制某些规则得到遵守时,混合组件也是个不错的选择。混合组件中的抽象成员模式能够确保“使用该混合组件的每个屏幕都必须提供屏幕名称,而混合组件会自动处理所有的跟踪工作”。这种通过实现来配置代码的模式可以使代码结构更加清晰、易于理解。
可重用的资源管理也是混合组件的一个典型应用场景。任何需要在 ` initState` 中创建、在 `dispose` 中销毁的资源,都适合使用混合组件来管理——比如动画控制器、流订阅对象、定时器、焦点控制元素以及滚动控制机制等。这些功能其实都可以通过编写混合组件来实现。
混合组件的不适用场景
混合组件并不能替代适当的抽象设计。如果你发现自己编写的混合组件中包含了大量的业务逻辑,那么这说明这种逻辑应该被放在 Bloc、仓库、服务类或普通的 Dart 类中,而不是混合组件里。混合组件的作用应该是规定屏幕应该如何表现,而不是决定屏幕具体要执行什么操作或处理哪些数据。
当你所需的行为真正属于对象层面时,使用混入模式也是错误的选择。在这种情况下,你应该创建该行为的实例并将其传递给其他地方。如果你想编写类似final handler = SomeHandler();这样的代码,并将其作为依赖项注入到其他类中,那么这应该是一个类,而不是一个混入模块——因为混入模块本身是无法被实例化的。
此外,当某种行为需要复杂的构造函数参数或依赖注入机制时,也应避免使用混入模式。传统意义上的混入模块并没有构造函数。如果你想要复用的某个行为在创建时需要传递配置对象,那么应该将其设计为一个类并通过依赖注入来使用它。
常见错误
在生命周期覆盖逻辑中忘记使用super
这是最常见的混入模块相关错误之一,而且这种错误的危害性往往并不明显,因为它并不一定会导致程序立即崩溃。实际上,它只是会悄悄地破坏整个混入模块的使用链。
// 错误示例:在混入模块中忘记了调用super.initState()
mixin BrokenMixin on State {
@override
void initState() {
// 这里忘记调用了super.initState(),因此后续的混入模块和State对象的 initState()方法都不会被执行。
_setupSomething();
}
}
// 正确示例:必须调用super.initState()
mixin CorrectMixin on State {
@override
void initState() {
super.initState(); // 这样就可以保证后续的混入模块和State对象的initeState()方法能够被正确执行。
_setupSomething();
}
}
有一条规则是绝对不能违反的:如果你的混入模块覆盖了某个生命周期方法,那么它就必须调用super。没有任何例外。
在没有指定on State约束的情况下使用混入模块
有些混入模块是专门为State对象设计的,它们会使用setState、mounted、context等生命周期方法。如果将这样的混入模块应用到非State类型的类上,就会导致编译错误。
但更隐蔽的问题在于,有些混入模块会使用setState方法,但却没有指定on State约束。如果没有这个约束,Dart语言就无法保证目标类中确实存在setState方法,从而导致编译失败,并且错误信息可能会让人难以理解。
// 错误示例:没有指定on State约束就使用了setState方法
mixin BrokenLoadingMixin {
bool _isLoading = false;
void startLoading() {
setState(() => _ isLoading = true); // 这里会报错,因为没有指定on State约束。
}
}
// 正确示例:必须指定on State约束
mixin LoadingMixin on State {
bool _isLoading = false;
void startLoading() {
setState(() => _isLoading = true); // 这样就可以正常编译通过了,因为Dart会确保目标类中存在setState方法。
}
}
在AutomaticKeepAliveClientMixin中忘记调用super.build
AutomaticKeepAliveClientMixin在Flutter的混合组件中较为特殊,因为它要求你在build方法中调用super.build(context)。如果忘记了这一操作,保持活动状态的机制将永远不会被激活,你的部件也会正常被销毁,这样一来,这个混合组件的存在意义就完全失去了。
// 错误做法:忘记调用super.build,导致保持活动状态的功能无法生效
class _BrokenState extends State
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
// 缺少了这行代码:super.build(context)
return const Placeholder();
}
}
// 正确做法:使用这个混合组件时,一定要调用super.build
class _CorrectState extends State
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 这行代码会将该部件注册到保持活动状态系统中
return const Placeholder();
}
}
将混合组件当作“神对象”来使用
如果一个混合组件的功能过于杂乱、缺乏条理,那么它就会变成类似“神对象”那样的存在——这种设计会导致其他组件与其之间产生紧密的耦合关系。
// 错误做法:一个混合组件承担了太多无关的功能
mixin AppBehaviorMixin on State {
// 分析功能
void trackEvent(String name) { }
// 连接性相关功能
bool get isConnected { return true; }
// 日志记录功能
void log(String message) { }
// 表单验证功能
bool validateEmail(String email) { return true; }
// 显示成功/错误提示框的功能
void showSuccessSnackBar(String message) { }
void showErrorSnackBar(String message) { }
// 加载状态相关功能
bool get isLoading { return false; }
// 导航功能
void navigateToHome() { }
}
// 正确做法:将相关的功能分离到不同的混合组件中
mixin ScreenAnalytics on State { /* ... */ }
mixin ConnectivityAware on State { /* ... */ }
mixin Logger { /* ... */ }
mixin SnackBarHelper on State { /* ... */ }
mixin LoadingStateMixin on State { /* ... */ }
没有文档说明的混合组件依赖顺序问题
混合组件的线性化调用顺序是确定的,但如果两个混合组件同时修改同一个资源或调用同一个方法,就可能会出现一些意想不到的问题。因此,当混合组件的行为依赖于其调用顺序时,必须对其进行明确的文档说明:
// 这两个混合类都重写了 initState方法。
// 在`with`语句中它们的顺序决定了哪个会先被执行。
// 需要明确说明这一点,以避免未来的开发者不小心将它们的顺序弄反。
/// 重要提示:在`with`语句中,LoggerMixin必须放在AnalyticsMixin之前。
// 因为LoggerMixin负责搭建日志记录系统,而AnalyticsMixin依赖于这个系统来工作。
///
/// 正确的写法是:with LoggerMixin, AnalyticsMixin
/// 错误的写法是:with AnalyticsMixin, LoggerMixin
mixin AnalyticsMixin on State {
@override
void initState() {
super.initState();
// 当这段代码被执行时,LoggerMixin已经先被执行过了,
// 因此log()方法已经可以正常使用了。
log('Analytics模块已为${runtimeType}初始化完毕');
_trackScreenOpen();
}
}
小型端到端示例
让我们构建一个功能完备的Flutter界面,通过这个例子来演示所有核心混合组件的概念。我们将创建一个SearchScreen,其中会使用三个自定义混合组件:一个是用于日志记录的,另一个是用于处理延迟输入事件的,还有一个是用于管理加载状态的。同时,我们还会利用Flutter内置的AutomaticKeepAliveClientMixin来确保在切换标签页时状态能够得到保留。
混合组件
// lib/mixins/logger_mixin.dart
/// 提供结构化的日志记录功能,并自动添加类名作为标签。
/// 这个混合组件不依赖于Flutter框架,可以应用于任何类。
mixin LoggerMixin {
String get tag => runtimeType.toString();
void log(String message) {
// 在生产环境中,请替换为你的日志记录框架(例如logger包)。
debugPrint('[\(tag] \)message');
}
void logError(String message, [Object? error, StackTrace? stackTrace]) {
debugPrint('[\(tag] ERROR: \)message');
if (error != null) debugPrint '[\(tag> 错误原因: \)error');
if (stackTrace != null) debugPrint(stackTrace.toString());
}
}
// lib/mixins/debounce_mixin.dart
import 'dart:async';
import 'package:flutter/material.dart';
/// 为State类提供延迟执行的回调功能。
/// 在组件被销毁时会自动取消待定的定时器。
///
/// 使用要求:必须应用于State类型的对象上。
///
/// 提供以下功能:
/// - [debounce]: 在输入停止后[delay]时间段内才执行相应操作。
mixin DebounceMixin on State {
Timer? _debounceTimer;
/// 延迟[action]指定的时间再执行该操作。每次调用都会重置延迟时间。
/// 这对于处理文本框的变化非常有用,可以避免在用户每次按键时都触发相应的操作。
void debounce(
VoidCallback action, {
Duration delay = const Duration(milliseconds: 500),
}) {
_debounceTimer?.cancel();
_debounceTimer = Timer(delay, action);
}
@override
void dispose() {
// 自动取消所有待定的延迟定时器。
// 使用该混合组件的类无需手动管理这些定时器。
_debounceTimer?.cancel();
super.dispose();
}
}
// lib/mixins LoadingState_mixin.dart
import 'packageflutter/material.dart';
/// 管理异步操作的加载状态、错误状态以及空闲状态。
///
/// 使用要求:必须应用于State类型的对象上。
///
/// 提供以下功能:
/// - [isLoading]: 当操作正在进行时,返回true。
/// - [hasError]: 如果上次操作失败,则返回true。
/// - [error]: 上次操作失败时产生的错误对象。
/// - [runWithLoading]: 为任何异步操作提供自动的状态管理功能。
/// - [clearError]: 重置错误状态,使UI恢复到空闲状态。
mixin LoadingStateMixin on State {
bool _isLoading = false;
Object? _error;
bool get isLoading => _isLoading;
bool get hasError => _error != null;
Object? get error => _error;
/// 运行[operation]指定的操作,在开始前会自动设置加载状态,
// 操作完成后无论是否成功都会重置加载状态。
/// 返回[operation]的操作结果,如果操作失败则返回null。
Future runWithLoading<R>>(Future〈R〉 Function() operation) async {
if (_isLoading) return null;
setState(() {
_isLoading = true;
_error = null;
});
try {
final result = await operation();
if (mounted) setState(() => _isLoading = false);
return result;
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_error = e;
});
}
return null;
}
}
/// 重置当前的错误状态,使UI恢复到空闲状态。
void clearError() {
setState(() => _error = null);
}
}
数据模型与虚拟服务
// lib/models/search_result.dart
class SearchResult {
final String id;
final String title;
final String subtitle;
final String category;
const SearchResult({
required this.id,
required this.title,
required this.subtitle,
required this.category,
});
}
// lib/services/search_service.dart
import '../models/search_result.dart';
class SearchService {
static const _fakeResults = [
SearchResult(id: '1', title: 'Flutter基础', subtitle: '开始学习Flutter', category: '教程'),
SearchResult(id: '2', title: 'Dart混合式编程', subtitle: '深入了解Dart混合式系统', category: '文章'),
SearchResult(id: '3', title: '状态管理', subtitle: 'Bloc、Riverpod与Provider的比较', category: '指南'),
SearchResult(id: '4', title: 'Flutter动画', subtitle: '动画控制器与计时器', category: '教程'),
SearchResult(id: '5', title: 'GraphQL与Flutter', subtitle: '在生产环境中使用graphql_flutter', category: '指南'),
SearchResult(id: '6', title: '测试Flutter应用程序', subtitle: '单元测试、组件测试与集成测试', category: '文章'),
];
Future> search(String query) async {
// 模拟网络延迟
await Future.delayed(const Duration(milliseconds: 600));
if (query.trim().isEmpty) return [];
return _fakeResults
.where((r) =>
r.title.toLowerCase().contains(query.toLowerCase()) ||
r.subtitle.toLowerCase().contains(query.toLowerCase())
.ToList();
}
}
搜索页面
// lib/screens/search_screen.dart
import 'package:flutter/material.dart';
import '../mixins/logger_mixin.dart';
import '../mixins/debounce_mixin.dart';
import '../mixinsloading_state_mixin.dart';
import '../models/search_result.dart';
import '../services/search_service.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({super.key});
@override
State createState() => _SearchScreenState();
}
class _SearchScreenState extends State
// AutomaticKeepAliveClientMixin:当用户切换到其他标签页后再返回时,会保持当前标签页的状态。搜索查询和结果不会被重新获取。
with
AutomaticKeepAliveClientMixin,
// LoggerMixin:在整个状态类中提供log()和logError()方法。由于这是一个纯Dart混合式组件,因此不需要遵守“on State”规则。
LoggerMixin,
// DebounceMixin:提供debounce()方法,并在组件被销毁时自动取消定时器。
DebounceMixin,
// LoadingStateMixin:提供runWithLoading()、isLoading、hasError和error等方法。
LoadingStateMixin {
// AutomaticKeepAliveClientMixin需要这个getter方法。
// 如果返回true,那么即使该组件滚出屏幕或用户离开TabView/PageView,它也会保持活跃状态。
@override
bool get wantKeepAlive => true;
final _searchController = TextEditingController();
final _searchService = SearchService();
List _results = [];
String _lastQuery =('');
@override
void initState() {
// 混合式组件的调用顺序在这里很重要。
// super.initState()会依次调用以下混合式组件的dispose方法:
// LoadingStateMixin -> DebounceMixin -> AutomaticKeepAliveClientMixin -> State
super.initState();
log('SearchScreen已初始化');
}
@override
void dispose() {
// DebounceMixin.dispose()会通过super.dispose()自动被调用。
// 我们只需要释放那些由我们自己管理的资源即可。
_searchControllerdispose();
// super.dispose()会依次调用所有混合式组件的dispose方法。
superdispose();
log('SearchScreen已销毁');
}
// 每当搜索文本字段发生变化时,都会调用此方法。
void _onSearchChanged(String query) {
// DebounceMixin.debounce()会将实际的搜索操作延迟500毫秒。
// 如果用户在500毫秒内输入了其他字符,定时器将会重置。
// 这样就可以避免每次按键时都触发网络请求。
debounce(() => _performSearch(query));
}
Future _performSearch(String query) async {
if (query == _lastQuery) return; // 避免重复搜索
_lastQuery = query;
log('正在搜索:" + query);
if (query.trim().isEmpty) {
setState(() => _results = []);
return;
}
// LoadingStateMixin.runWithLoading()会处理所有的状态转换:
// 在调用搜索函数之前,会将isLoading设置为true;
// 搜索完成后,会将isLoading设置为false;
// 如果搜索过程中出现错误,会将错误信息存储在error属性中。
final results = await runWithLoading(
() => _searchService.search(query),
);
if (results != null && mounted) {
setState(() => _results = results);
log('搜索结果:找到了\({results.length}条与"\)" + query + "相关的结果');
}
}
@override
Widget build(BuildContext context) {
// AutomaticKeepAliveClientMixin要求必须调用super.build(context)。
// 如果不调用这个方法,保持页面活跃的机制将无法生效。
super.build(context);
return Scaffold(
appBar: AppBar(
title: const Text('搜索'),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: TextField(
controller: _searchController,
onChanged: _onSearchChanged,
decoration: InputDecoration(
hintText: '搜索文章或教程...', // 修改了提示语
prefixIcon: const Icon'iconsWithArrow》, // 更改了前缀图标
suffixIcon: _searchController.text.isNotEmpty
? IconButton(
icon: const Icon'iconClear'), // 修改了后缀图标
onPressed: () {
_searchController.clear();
_onSearchChanged ''); // 重新调用_onSearchChanged方法
},
)
: null,
filled: true,
fillColor: Theme.of(context).colorScheme.surfaceVariant,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
),
),
),
),
body: _buildBody(),
);
}
Widget _buildBody() {
// 由于使用了混合式组件,因此在这里可以访问isLoading和hasError属性。
if (isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon'iconsWithErroroutline, size: 48, color: Colors.red),
const SizedBox(height: 12),
Text(
error?.toString() ?? '发生了错误',
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
clearError(); // 调用LoadingStateMixin.clearError()方法
_performSearch(_lastQuery); // 重新开始搜索
},
child: const Text('重试'),
),
],
),
);
}
if (_searchController.text.isEmpty) {
return const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon'icon_search, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'开始输入关键词进行搜索',
style: TextStyle(color: Colorsgrey, fontSize: 16),
),
],
),
);
}
if (_results.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon'icon_search_off, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
'没有找到与"\_" + _searchController.text + "相关的结果',
style: const TextStyle(color: Colorsgrey, fontSize: 16),
),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _results.length,
separatorBuilder: (_, __) => const SizedBox(height: 8),
itemBuilder: (context, index) {
final result = _results[index];
return SearchResultCard(result: result);
},
);
}
}
class SearchResultCard extends StatelessWidget {
final SearchResult result;
const SearchResultCard({super.key, required this.result});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: _categoryColor(result.category),
child: Text(
result.category[0],
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
result.title,
style: constTextStyle.fontWeight: FontWeight.w600),
),
subtitle: Text(result.subtitle),
trailing: Chip(
label: Text(result.category),
style: const TextStyle(fontSize: 11),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
),
);
}
Color _categoryColor(String category) {
switch (category) {
case 'Tutorial':
return Colors.blue;
case 'Article':
return Colors.green;
case 'Guide':
return Colors.orange;
default:
return Colors.purple;
}
}
}
这个SearchScreen示例展示了如何将多个混合类结合到一个State类中,从而清晰地分离不同的功能。其中,AutomaticKeepAliveClientMixin用于在切换标签页时保持屏幕状态;LoggerMixin负责日志记录功能;DebounceMixin通过延迟处理用户输入来避免频繁发起搜索请求;而LoadingStateMixin则用于管理加载过程及错误状态。这样的设计使得用户界面与逻辑结构更加清晰有序,同时能够有效响应用户的操作——通过延迟查询、内置的加载/错误处理机制,以及高效地更新搜索结果。
入口点
// lib/main.dart
import 'packageflutter/material.dart';
import 'screens/search/screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '混合类演示',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab'icon: Icon(Icons.search), text: '搜索'),
Tab(icon: IconIcons.home), text: '主页'),
],
),
),
body: const TabBarView(
children: [
SearchScreen(), // 使用了四个混合类
Center(child: Text('主页标签页')),
],
),
),
),
);
}
}
这个完整的、可运行的示例清楚地展示了所有主要的混合类应用场景。
_SearchScreenState同时使用了四个混合类:
AutomaticKeepAliveClientMixin用于保持标签页状态;LoggerMixin实现结构化的日志记录功能,且无需进行任何额外配置;DebounceMixin自动延迟搜索请求的发送,并在组件被销毁时清理相关定时器;LoadingStateMixin用于高效管理异步操作的状态。
这些混合类的使用顺序是经过精心设计的,相关代码中也进行了注释。在initState和dispose>方法中,都严格遵守了继承链的规则。每个混合类都只负责一项具体功能,而使用它们的State>类也只需专注于自身的逻辑实现——例如将用户界面与搜索服务关联起来,而不需要处理其他复杂任务。
结论
对于框架开发者来说,混合类并非什么小众的语言特性。它们其实是任何希望编写简洁、易于维护、可重复使用的Flutter代码的开发者的实用工具。
一旦你不再在各个页面中重复编写相同的initState>代码,而是开始使用专门设计、经过测试的混合类,你的代码质量就会显著提升:因忘记调用dispose>方法而导致的错误会减少,维护工作也会变得更为简单,而且代码本身会更加清晰易懂——它的功能是通过组件的组合来实现的,而非通过注释来说明的。
要真正理解混音机制的精髓,就必须弄清楚“属于关系”与“具备某种功能”之间的区别。继承用于描述对象的身份属性:例如,Dog是一种Animal。而混音机制则用于描述对象的功能特性:比如某个屏幕能够记录分析数据,某个存储库能够进行日志记录,某个表单能够验证用户输入的信息。一旦你掌握了这种区别,你就会发现自己能在现有的代码中轻松找到使用混音机制的机会。
Flutter自身的框架就是混音机制设计的典范。每当你使用with SingleTickerProviderStateMixin这个混音组件时,其实就是在使用一个能够隐式管理Ticker生命周期的组件:它只会在合适的类上被激活,仅提供一项功能(vsync),并且在组件被销毁后就会完全消失。这才是理想的混音机制应有的状态——实现最大的功能价值,最小的代码规模,同时确保不会出现内存泄漏。
Dart的混音机制之所以如此可靠,正是因为它采用了线性化设计模式。多重继承往往会引发歧义,而线性化设计则能确保所有混音组件的执行顺序都是可预测的,每个super调用都会按照预定的流程进行。理解这一执行逻辑,并在生命周期覆盖过程中严格遵守super调用的规则,才是安全使用混音机制的关键。
要编写高质量的混音组件,就需要遵循与编写优秀函数相同的规范:每个混音组件都应该只负责一项功能,拥有清晰的名称、明确的接口定义,并且能够在独立的环境中接受测试。
设计精良的混音组件在使用时是几乎察觉不到其存在的。使用它的开发者只需编写更少的代码,犯更少的错误,而只需要专注于屏幕的具体逻辑实现即可,其余的工作都由混音组件来完成。
开始时可以从简单的地方入手。找出那些你经常在两个不同的界面中重复使用的代码片段,思考一下这些代码是否适合被提取出来放入一个混音组件中。几乎在所有情况下,这样的代码都是适合被整合的,将其提取出来后,这两个界面的代码结构都会变得清晰许多。
逐步构建你的混音组件库,在添加每个新组件时都要对其进行测试。随着时间的推移,你会逐渐积累出一套可重复使用的功能模块,这些模块会让你开发的每一个新界面都比之前的版本更加高效、更加可靠。
参考资料
Dart语言官方文档
-
Dart混音机制文档:Dart官方提供的关于混音机制的指南,涵盖了语法、
on子句以及混音组件的组合方式。https://dart.dev/language/mixins -
Dart类与对象:关于Dart类系统的基础文档,解释了混音机制与继承、接口之间的关系。https://dart.dev/languageclasses
-
Dart语言教程:混音机制:简要介绍了混音机制的语法,并提供了在DartPad中可运行的示例。https://dart.dev/guides/language/language-tour#adding-features-to-a-class-mixins
-
Dart 3混音组件:针对Dart 3中新增的
mixin class声明方式提供的文档,介绍了其使用场景和限制条件。https://dart.dev/language/mixins#class-mixin-or-mixin-class
Flutter框架混合组件
-
SingleTickerProviderStateMixin API:这个混合组件的完整API参考文档,它使得在Flutter部件中使用
AnimationController成为可能。https://api.flutter.dev/flutter Widgets/SingleTickerProviderStateMixin-mixin.html -
TickerProviderStateMixin API:用于多定时器的场景的API参考文档,当某个状态需要使用多个
AnimationController时就会用到这个混合组件。https://api.flutter.dev/flutter Widgets/TickerProviderStateMixin-mixin.html -
AutomaticKeepAliveClientMixin API:用于实现保持连接功能的混合组件的API参考文档,其中包含了该组件所需使用的方法(如
wantKeepAlive和super.build)。https://api.flutter.dev/flutter Widgets/AutomaticKeepAliveClientMixin-mixin.html -
WidgetsBindingObserver API:这个用于观察应用程序生命周期的混合组件的完整参考文档,涵盖了它所提供的所有回调函数。https://api.flutter.dev/flutter Widgets/WidgetsBindingObserver-mixin.html
-
RestorationMixin API:关于Flutter中状态恢复功能的参考文档,包括
restoreState、restorationId以及Restorable类型的相关信息。https://api.flutter.devflutter Widgets/RestorationMixin-mixin.html
学习资源
-
Effective Dart: Design:谷歌官方发布的Dart API设计指南,其中提供了关于在何时使用类、混合组件或扩展方法的建议。https://dart.dev/effective-dart/design
-
Flutter Widget of the Week: 使用混合组件的部件:Flutter官方在YouTube上发布的系列视频,其中有多集讲解了混合组件是如何增强Flutter部件系统的功能的。https://www.youtube.com/@flutterdev
-
Dart规范:混合组件:为那些想要了解混合组件的使用规则及线性化原理的读者提供的正式文档。https://dart.dev/guides/language/spec



