Dart扩展|全面介绍和实际使用案例

183 阅读9分钟

级别:初学

本教程是对Dart扩展的完整介绍,Dart扩展是Dart 2.6中增加的一项语言功能。

作为其中的一部分,我们将看到。

  • 如何在一个现有的Flutter项目中启用扩展功能
  • 什么是扩展,以及在什么情况下可以使用它们来改进我们的代码
  • 哪里不应该使用它们

俗话说,大权在握,责任重大。而Dart扩展是一个应该合理使用的语言特性。 好了,让我们开始吧,看看我们如何在我们的Flutter项目中启用扩展。

如何启用扩展功能

打开pubspec.yaml 文件,更新环境部分,使用SDK 2.6.0或更高版本。

environment:
  sdk: ">=2.6.0 <3.0.0"

这就是了。我们现在可以在我们的Flutter项目中使用扩展。

开始使用扩展程序

你可以使用Dartpad来试验扩展。

假设我们正在编写一个程序,将温度值从摄氏度转换为法氏温度。

我们可以用一个double 来表示一个温度值。

首先,我们可以在double 上定义一个扩展,以及一些从摄氏度转换为法氏温度的方法,反之亦然。

extension on double {
  // note: `this` refers to the current value
  double celsiusToFarhenheit() => this * 1.8 + 32;
  double farhenheitToCelsius() => (this - 32) / 1.8;
}

一旦我们有了这个,我们就可以定义一个double ,代表10.0摄氏度的温度,并将其转换为法海特。

double tempCelsius = 10.0;
double tempFarhenheit = tempCelsius.celsiusToFarhenheit();
print('${tempCelsius}C = ${tempFarhenheit}F');

如果我们运行这段代码,它就会打印出来。

10C = 50F

启示:我们可以使用扩展来为现有类型添加新的功能

除了方法之外,我们还可以在我们的扩展中添加getterssetters

但我们不能添加实例变量。

换句话说,我们可以使用扩展来扩展我们类型的功能,但不能扩展底层数据


顺便说一下,在本教程的开头,我说我们在使用扩展时应该注意,有些情况下最好不要使用它们。

而这个温度转换的例子,就是这样一种情况。

事实上,没有什么能阻止我们编写这样的代码。

tempCelsius.celsiusToFarhenheit().celsiusToFarhenheit();

这将温度转换为法海特的次数超过了一次,这没有意义。

启示:在设计一个处理温度的API时,扩展并不适合,我们应该定义一个完全不同的类型。

这里有一个更好的方法,它创建了一个新的Temperature 类型

class Temperature {  
  final double celsius;
  double get farhenheit => celsius * 1.8 + 32;
}

这在内部总是以摄氏度表示温度。

而我们可以通过提供的getter来获得法伦希特值。

然后,我们可以添加一些工厂构造函数来使我们的API更漂亮。

class Temperature {  
  Temperature._({this.celsius});
  factory Temperature.celsius(double degrees) => Temperature._(celsius: degrees);
  factory Temperature.farhenheit(double degrees) => Temperature._(celsius: (degrees - 32) / 1.8);

  final double celsius;
  double get farhenheit => celsius * 1.8 + 32;
}

在我们的主文件中,我们可以像这样创建温度值。

final tempA = Temperature.celsius(10);
final tempB = Temperature.farhenheit(50);

在这个例子中,为温度使用一个定制的类型。

  • 充分利用了类型系统
  • 使得我们的代码更容易被理解
  • 避免重载通用的数字类型,如double ,用特定领域的逻辑进行温度转换。

让我们回到正轨,学习更多关于扩展的知识。

通用类型

Dart扩展可以与通用类型一起使用。

因此,我们的下一个目标是编写一个扩展,使我们能够编写这样的代码。

final total1 = [1, 2, 3].sum(); // 6
final total2 = [1.5, 2.5, 3.5].sum(); // 7.5

要做到这一点,我们可以在Iterable<T> 上创建一个命名的扩展,称为IterableNumX

extension IterableNumX<T extends num> on Iterable<T> {}

这个扩展对任何扩展了 num 的类型Tnum 是一个在Dart中代表数字的类型)进行操作。因此,这个扩展将同时适用于intdouble 类型的集合。

然后,我们可以定义一个sum() 方法,其返回类型为T ,并像这样实现它。

extension IterableNumX<T extends num> on Iterable<T> {
  T sum() {
    // 1. initialize sum
    var sum = (T == int ? 0 : 0.0) as T;
    // 2. calculate sum
    for (var current in this) {
      if (current != null) { // only add non-null values
        sum += current;
      }
    }
    return sum;
  }
}

这个方法将sum 初始化为interger0 或 double0 ,取决于T 的类型。

然后它通过迭代this (我们的可迭代对象)的值来计算总和,并在最后返回结果。

有了这个扩展,我们可以像这样表达一个数字集合中所有元素的总和。

final total1 = [1, 2, 3].sum(); // 6
final total2 = [1.5, 2.5, 3.5].sum(); // 7.5

经验之谈。扩展可以被命名,它们可以与泛型一起工作。

让我们看看还有哪些方式可以使用它们。

用更少的模板进行填充

我们可以使用扩展来扩展现有的Flutter类型,并减少普通布局任务的模板代码。

考虑一下这段代码。

class ColumnLayout extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: <Widget>[
          Text('One Line'),
          Text('Another line'),
          customRaisedButton(
            context,
            onPressed: () {},
            child: Text('OK'),
            borderRadius: 8,
          ),
        ],
      ),
    );
  }
}

这表示一个有两个Text widgets和一个自定义按钮的Column 布局。

像这样使用,它在父容器内将内容从边缘延伸到边缘。

因此,最好给这一列中的每个小部件添加一些Padding。

Column(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Text('One Line'),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Text('Another line'),
    ),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: customRaisedButton(
        context,
        onPressed: () {},
        child: Text('OK'),
        borderRadius: 8,
      ),
    ),
  ],
)

这样做是可行的,但Padding 小部件增加了很多噪音,使我们的代码不容易阅读。

所以为了克服这个问题,我们可以在Widget 类上定义一个扩展。

extension WidgetPaddingX on Widget {
  Widget paddingAll(double padding) => Padding(
        padding: EdgeInsets.all(padding),
        child: this,
      );
}

这需要当前的widget(this ),添加一个Padding 作为父类,并返回它。

这样做的好处是,我们可以把上面的代码改写成。

Column(
  crossAxisAlignment: CrossAxisAlignment.stretch,
  children: <Widget>[
    Text('One Line').paddingAll(8),
    Text('Another line').paddingAll(8),
    customRaisedButton(
      context,
      onPressed: () {},
      child: Text('OK'),
      borderRadius: 8,
    ).paddingAll(8),
  ],
)

结果是一样的,但代码更易读。

信用:我从Quick Bird Studios的这篇博文中借用了这个具体的例子。

关于命名扩展的说明

使用上面的例子,我们可以删除扩展名,并在一个单独的文件中导入它。

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

extension on Widget {
  Widget paddingAll(double padding) => Padding(
        padding: EdgeInsets.all(padding),
        child: this,
      );
}

// text_with_padding.dart
import 'package:flutter/material.dart';
import 'widget_padding_x.dart';

class TextWithPadding extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // this doesn't compile
    return Text('test').paddingAll(8);
  }
}

然而,这段代码将无法编译,因为扩展名没有名字,而且paddingAll() 方法无法被编译器解决。

经验之谈。如果你想从不同的文件中导入和使用扩展,一定要给它们命名。

带有静态方法的扩展:扩展ShapeBorder

有一个Flutter API,我总是忘记如何正确使用,那就是ShapeBorder 类,我们可以用它来定义各种形状。

这里有一个自定义按钮类的例子,它使用RoundedRectangeBorder 来定义我们想要的形状。

import 'package:flutter/material.dart';

class CustomRaisedButton extends StatelessWidget {
  CustomRaisedButton({
    Key key,
    this.child,
    this.color,
    this.borderRadius: 2.0,
    this.onPressed,
  }): super(key: key);
  final Widget child;
  final Color color;
  final double borderRadius;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: child,
      color: color,
      disabledColor: color,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.all(
          Radius.circular(borderRadius),
        ),
      ),
      onPressed: onPressed,
    );
  }
}

我们可以把创建RoundedRectangleBorder 的代码提取出来,放到ShapeBorder 的扩展中。

extension ShapeBorderX on ShapeBorder {
  static ShapeBorder roundedRectangle(double radius) {
    return RoundedRectangleBorder(
      borderRadius: BorderRadius.all(
        Radius.circular(radius),
      ),
    );
  }
}

有了这个,每次我们需要一个圆角矩形的形状时,我们可以直接写。

shape: ShapeBorderX.roundedRectangle(borderRadius),

这就更容易记住了。

用静态扩展方法创建小部件

在我的代码中,我经常需要创建一个小部件类,它有一个父BlocChangeNotifier

这里有一些代码,使用一个ChangeNotifierProvider ,同时使用一个ValueNotifier ,作为实现Flutter计数器例子的方法,而不使用StatefulWidgetsetState()

class CounterPage extends StatelessWidget {
  static Widget create(BuildContext context) {
    return ChangeNotifierProvider<ValueNotifier<int>>(
      builder: (_) => ValueNotifier<int>(0),
      child: CounterPage(),
    );
  }
  // build method here
}

这里我们使用了一个static create() 方法,它可以在调用站点作为CounterPage.create(context) 来调用。

然而,这是一个附加的辅助方法,没有它,CounterPage 类就已经很完整了。

如果我们想要更好地分离关注点,我们可以把它移到一个叫做CounterPageX 的扩展中。

extension CounterPageX on CounterPage {
  static Widget create(BuildContext context) {
    return ChangeNotifierProvider<ValueNotifier<int>>(
      builder: (_) => ValueNotifier<int>(0),
      child: CounterPage(),
    );
  }
}

然后我们可以更新调用站点,使用CounterPageX.create(context)


一般来说,我觉得扩展可能是存放现有类的静态辅助方法的一个好地方。

因为它们可以让我们更清楚地将类中的所有主要功能与我们可能需要的任何特定类的辅助方法分开。

注意事项

现在我们已经看到了所有这些例子,你可能会认为异常是下一个大事件,并想到处使用它们。

所有的东西都是扩展

不是那么快。

仅仅因为我们可以使用扩展,并不意味着我们应该到处使用它们。

归根结底,我们希望我们的代码是可以理解的易于浏览的

如果我们过度使用扩展,它们会很快成为我们不知道该放在哪里的辅助方法的垃圾场。

所以我鼓励你只在有意义的地方使用它们。

在设计基于扩展的API时,一个好的标准是问自己,你的改变是否会带来更好的API。

而一个好的API的理想特征是,它应该很难被错误地使用

我们的第一个温度转换例子很好地说明了这一点。

// not recommended
extension on double {
  double celsiusToFarhenheit() => this * 1.8 + 32;
  double farhenheitToCelsius() => (this - 32) / 1.8;
}

// much better
class Temperature {  
  Temperature._({this.celsius});
  factory Temperature.celsius(double degrees) => Temperature._(celsius: degrees);
  factory Temperature.farhenheit(double degrees) => Temperature._(celsius: (degrees - 32) / 1.8);

  final double celsius;
  double get farhenheit => celsius * 1.8 + 32;
}

void main() {
  double tempCelsius = 10.0;
  // API **can** used incorrectly
  tempCelsius.celsiusToFarhenheit().celsiusToFarhenheit();
  
  final tempA = Temperature.celsius(10);
  // API **can't** be used incorrectly
  tempA.farhenheit;
}

扩展不能做什么

扩展不能做的事情包括。

  • 为扩展添加构造函数(无论它们是否是工厂构造函数)
  • 从基类中扩展接口或方法(使用*@override*)。

如果你尝试后者,你会得到这样的错误。

Extensions can't declare members with the same name as a member declared by 'Object'.

DartX

DartX是一个包,它为Dart语言中的类型增加了很多有用的扩展方法。

这包括与Iterable 、字符串、时间工具和更多的附加功能。

所以我鼓励你熟悉这个包,并将其纳入你自己的项目中。

styled_widget

styled_widget是另一个作为实验开始的包,展示了如何改进widget风格的API,使用的语法类似于苹果的SwiftUI框架。

在README中,我们展示了如何对TextIcon 小工具进行样式化,其方式比 "标准 "Flutter API更易读。例子。

@override
Widget build(BuildContext context) => FlutterLogo()
  .padding(all: 20)
  .backgroundColor(Colors.blue)
  .constraints(width: 100, height: 100)
  .borderRadius(all: 10)
  .elevation(10)
  .alignment(Alignment.center);

在引擎盖下,这与copyWith 一起使用扩展,作为修改现有 widget 的一种方式,并以具有一些不同属性的新副本来替换它。

注意:这个包仍然是一个实验,可能会有一些突破性的变化,而且还没有准备好用于生产。这在未来可能会也可能不会改变。

在任何情况下,看看Flutter团队是否会引领潮流,改进一些widget的API,以便在未来使用扩展,这将是一件有趣的事情。

结语

谢谢你的阅读!