Dart 密封类生成器

为 Dart 和 Flutter 生成密封类层次结构。

特点

  • 生成带有抽象超类型和数据子类的密封类。
  • 静态工厂方法。例如 Result.success(data: 0)
  • 类型转换方法。例如 a.asSuccessa.isSuccessa.asSuccessOrNull
  • 三种等值和 hashCode 生成方式:数据(类似 kotlin 的 data classes)、标识和独立。
  • 使用流行的 equatable 库实现数据等值。
  • 支持泛型。甚至可以混合类型。
  • 支持空安全项目中的可空和非空类型。
  • 支持在一个密封类型中使用另一个。
  • 支持空安全。
  • 为数据类生成 toString。
  • 生成 6 种不同的匹配方法。例如 whenmaybeWhenmap

用法

将依赖项添加到您的 pubspec.yaml 文件中。

dependencies:
  sealed_annotations: ^latest.version

dev_dependencies:
  sealed_generators: ^latest.version

导入 sealed_annotations

import 'package:sealed_annotations/sealed_annotations.dart';

添加 part 指向您希望类生成在其中的文件。使用 .sealed.dart 扩展名。

part 'weather.sealed.dart';

添加 @Sealed 注释,以及一个抽象的私有类作为生成代码的清单。例如

@Sealed()
abstract class _Weather {
  void sunny();

  void rainy(int rain);

  void windy(double velocity, double? angle);
}

然后运行以下命令为您生成代码。如果您是 flutter 开发者

flutter pub run build_runner build

如果您正在为纯 Dart 开发

dart run build_runner build

生成的代码将如下所示:(以下代码已简化)

abstract class Weather {
  const factory Weather.rainy({required int rain}) = WeatherRainy;

  bool get isRainy => this is WeatherRainy;

  WeatherRainy get asRainy => this as WeatherRainy;

  WeatherRainy? get asRainyOrNull {
    /* ... */
  }

  /* ... */

  R when<R extends Object?>({
    required R Function() sunny,
    required R Function(int rain) rainy,
    required R Function(double velocity, double? angle) windy,
  }) {
    /* ... */
  }

  R maybeWhen<R extends Object?>({
    R Function()? sunny,
    R Function(int rain)? rainy,
    R Function(double velocity, double? angle)? windy,
    required R Function(Weather weather) orElse,
  }) {
    /* ... */
  }

  void partialWhen({
    void Function()? sunny,
    void Function(int rain)? rainy,
    void Function(double velocity, double? angle)? windy,
    void Function(Weather weather)? orElse,
  }) {
    /* ... */
  }

  R map<R extends Object?>({
    required R Function(WeatherSunny sunny) sunny,
    required R Function(WeatherRainy rainy) rainy,
    required R Function(WeatherWindy windy) windy,
  }) {
    /* ... */
  }

  R maybeMap<R extends Object?>({
    R Function(WeatherSunny sunny)? sunny,
    R Function(WeatherRainy rainy)? rainy,
    R Function(WeatherWindy windy)? windy,
    required R Function(Weather weather) orElse,
  }) {
    /* ... */
  }

  void partialMap({
    void Function(WeatherSunny sunny)? sunny,
    void Function(WeatherRainy rainy)? rainy,
    void Function(WeatherWindy windy)? windy,
    void Function(Weather weather)? orElse,
  }) {
    /* ... */
  }
}

class WeatherSunny extends Weather {
  /* ... */
}

class WeatherRainy extends Weather with EquatableMixin {
  WeatherRainy({required this.rain});

  final int rain;

  @override
  String toString() => 'Weather.rainy(rain: $rain)';

  @override
  List<Object?> get props => [rain];
}

class WeatherWindy extends Weather {
  /* ... */
}

注意事项

  • 建议在超类中使用工厂而不是子类构造函数。例如使用 Whether.rainy() 而不是
    WhetherRainy()
  • 尽量少用类型转换方法,大多数情况下可以用匹配方法代替。

等值和生成的类名

您可以使用 @WithEquality(...) 注释在三种等值类型之间进行选择。如果没有指定,默认等值类型为 data
这将成为所有子类的默认等值类型。您可以通过在单个方法上使用此注释来更改每个子类的等值类型。
此注释可以在个别方法上使用。

等值类型

  • data 等值通过 Equatable 包实现。它的行为类似于 kotlin 的 data classes。
  • identity 只有相同的实例才相等。这就像您没有实现任何特定的等值。
  • distinct 所有实例都不相等。甚至一个实例本身也不等于自身。

一个基本示例

@Sealed()
abstract class _Weather {
  void sunny();

  void rainy(int rain);

  void windy(double velocity, double? angle);
}

在前面的示例中,所有类都将具有 data 等值。例如,如果您希望所有类都具有 identity 等值,
windy 使用 distinct 等值

@Sealed()
@WithEquality(Equality.identity)
abstract class _Weather {
  void sunny();

  void rainy(int rain);

  @WithEquality(Equality.distinct)
  void windy(double velocity, double? angle);
}

会生成一个抽象超类,其名称等于清单类的名称,但不带下划线(此处为 Weather)。
每个方法将成为一个子类。至少应有一个方法。子类名称基于方法名称
以超类名称为前缀(例如 WeatherSunny)。可以使用 @WithPrefix
@WithName 注释来定制命名过程。每个方法的参数将成为相应子类中的字段。字段名称等于
参数名称,字段类型等于参数类型或动态(如果未指定)。参数类型可以
通过 @WithType 注释覆盖,例如在构建时类型信息不可用时。请注意,您可以
拥有可空和非空字段。

要更改子类名称的前缀(默认为顶级类名),可以使用 @WithPrefix 注释。
示例

@Sealed()
@WithPrefix('Hello')
abstract class _Weather {
  void sunny();
}

现在 sunny 将被命名为 HelloSunny,而不是默认的 WeatherSunny。您可以使用 @WithPrefix('') 来移除
所有子类名称的前缀。

要直接更改子类名称,可以使用 @WithName 注释。如果指定了,它将覆盖 WithPrefix
示例

@Sealed()
abstract class _Weather {
  @WithName('Hello')
  void sunny();
}

现在 sunny 将被命名为 Hello,而不是默认的 WeatherSunny。如果您不想使用前缀,这很有用
对于某些项目。

密封类上的几乎所有方法都使用从清单方法名称中提取的短名称。不会使用完整的子类名称。
建议不要直接使用子类。超类上为每个项目都有工厂方法。

泛型用法

对于泛型密封类,您应该像实现泛型类一样编写清单类。

建议,如果您想要可空泛型字段,请将泛型参数声明为 T extends Base? 并使用 T
而不带空值后缀。如果您想要非空泛型字段,请将泛型参数声明为 T extends Base
使用 T 而不带空值后缀。如果您不指定上界,它将默认为 Object?,因此您的泛型类型
将是可空的。

import 'package:sealed_annotations/sealed_annotations.dart';

part 'result.sealed.dart';

@Sealed()
abstract class _Result<D extends num> {
  void success(D data);

  void error(Object exception);
}

或者您可以有多个泛型类型,甚至可以混合它们。

import 'package:sealed_annotations/sealed_annotations.dart';

part 'result.sealed.dart';

@Sealed()
abstract class _Result<D extends num, E extends Object> {
  void success(D data);

  void error(E exception);

  void mixed(D data, E exception);
}

动态类型和在一个密封类型中使用另一个

考虑您有一个密封的结果类型,例如

@Sealed()
abstract class _Result<D extends Object> {
  /* ... */
}

您想在另一个密封类型中使用此类型。

@Sealed()
abstract class _WeatherInfo {
  void fromInternet(Result<WeatherData> result);
}

如果您为 WeatherInfo 生成,您会看到 result 的类型是 dynamic。这是因为 Result 本身不是代码
在构建时生成。

您应该使用 @WithType 注释。

@Sealed()
abstract class _WeatherInfo {
  void fromInternet(@WithType('Result<WeatherData>') result);

  // you can also have nullable types.
  void nullable(@WithType('Result<WeatherData>?') result);
}

通用字段

有时您需要一些字段存在于所有密封类中。例如,考虑为不同类型的错误创建密封类,
并且所有这些都必须具有 codemessage。手动为所有密封类添加代码和消息会非常烦人。此外,如果您有一个错误对象,您无法在不使用类型转换或匹配方法的情况下获取其代码或消息。在这里,您可以使用通用字段。
您无法在不使用类型转换或匹配方法的情况下获取其代码或消息。
您可以使用通用字段。

要声明通用字段,您可以向清单类添加一个 getter 或一个 final 字段,它将自动添加到您所有的密封类中。例如
通用字段也可以在 ApiError 对象及其子类上使用。

@Sealed()
abstract class _ApiError {
  // using getter
  String get message;

  // using final field
  final String? code = null;

  // code and message will be added to this automatically
  void internetError();

  void badRequest();

  void internalError(Object? error);
}

通用字段也可以在 ApiError 对象及其子类上访问。

如果您在密封类中指定通用字段,则无效。例如

@Sealed()
abstract class _Common {
  Object get x;

  // one and two will have identical signatures
  void one(Object x);

  void two();
}

您可以在密封类中使用通用字段的子类。例如

@Sealed()
abstract class _Common {
  Object get x;

  // x has type int
  void one(int x);

  // x has type String
  void one(String x);

  // x has type Object
  void three();
}

通用字段也与其他 dart_sealed 结构(如泛型和 @WithType)配合使用。例如

@Sealed()
abstract class _Common {
  @WithType('num')
  dynamic get x; // you can omit dynamic

  // x has type int
  void one(@WithType('int') dynamic x); // you can omit dynamic

  // x has type num
  void two();
}

并且,例如

@Sealed()
abstract class _Result<D extends num> {
  Object? get value;

  void success(D value);

  void error();
}

忽略生成的文件的说明

建议在 Git 中忽略生成的文件的说明。将此添加到您的 .gitignore 文件中

*.sealed.dart

不建议排除生成的文件的说明。但如果您决定这样做,请将其添加到
您的 analysis_options.yaml 文件中

analyzer:
  exclude:
    - **.sealed.dart

GitHub

https://github.com/6thsolution/dart_sealed