Flutter密封类注解插件sealed_class_annotations的使用

发布于 1周前 作者 songsunli 来自 Flutter

Flutter密封类注解插件sealed_class_annotations的使用

Dart密封类生成器

构建状态 覆盖率 sealed_class_annotations版本 sealed_generators版本 sealed_class_writer版本

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

特性

  • 生成带有抽象父类型和数据子类的密封类。
  • 静态工厂方法。例如 Result.success(data: 0)
  • 类型转换方法。例如 a.asSuccessa.isSuccessa.asSuccessOrNull
  • 三种类型的相等性和hashCode生成:数据(如Kotlin数据类)、身份和唯一。
  • 使用流行的equatable库实现数据相等。
  • 支持泛型。甚至可以混合类型。
  • 支持在空安全项目中的可空和不可空类型。
  • 支持在一个密封类型中使用另一个密封类型。
  • 支持空安全。
  • 为数据类生成toString方法。
  • 生成六种不同的匹配方法。例如 whenmaybeWhenmap

使用

在你的 pubspec.yaml 文件中添加依赖项:

dependencies:
  sealed_class_annotations: ^latest.version

dev_dependencies:
  sealed_generators: ^latest.version

导入 sealed_class_annotations

import 'package:sealed_class_annotations/sealed_class_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,
  }) {
    /* ... */
  }

  R? whenOrNull<R extends Object?>({
    R Function()? sunny,
    R Function(int rain)? rainy,
    R Function(double velocity, double? angle)? windy,
    R 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,
  }) {
    /* ... */
  }

  R? mapOrNull<R extends Object?>({
    R Function(WeatherSunny sunny)? sunny,
    R Function(WeatherRainy rainy)? rainy,
    R Function(WeatherWindy windy)? windy,
    R 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的数据类。
  • 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 将生成 Weather 类。每个方法都会成为子类。至少应有一个方法。子类名称基于方法名称前缀加上超类名称(例如 WeatherSunny)。命名过程可以通过使用 @WithPrefix@WithName 注解进行调整。每个方法参数将成为相应子类中的字段。字段名称与参数名称相同,字段类型与参数类型相同或为 dynamic 如果未指定。参数类型可以在构建时使用 @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_class_annotations/sealed_class_annotations.dart';

part 'result.sealed.dart';

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

  void error(Object exception);
}

或者你可以有多个泛型类型并混合它们。

import 'package:sealed_class_annotations/sealed_class_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 生成代码,你会看到结果具有 dynamic 类型。这是因为 Result 本身在构建时没有被代码生成。

你应该使用 @WithType 注解。

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

  // 你也可以有可空类型。
  void nullable(@WithType('Result<WeatherData>?') result);
}

层次特性

如果密封类在同一文件中,你可以直接引用它们的清单类名称。这是为了避免 @WithType 注解并提高重构能力。

@Sealed()
abstract class _Apple {
  void eat();
}

@Sealed()
abstract class _Banana {
  void eat();
}

@Sealed()
abstract class _Basket {
  void friends(_Apple? apple, _Banana? banana);

// 或等效地
// void friends(@WithType('Apple?') apple, @WithType('Banana?') banana);
}

对于泛型情况:

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

  void error(Object exception);
}

@Sealed()
abstract class _Basket {
  void hold(_Result<int> x);

// 或等效地:
// void hold(@WithType('Result<int>') x);
}

@WithType 注解将覆盖层次特性。

公共字段

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

要声明一个公共字段,你可以向清单类添加一个getter或final字段,并且它将自动添加到所有密封类中。例如:

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

  // 使用final字段
  final String? code = null;

  // code和message将自动添加到这个
  void internetError();

  void badRequest();

  void internalError(Object? error);
}

你也可以使用构造函数与final字段等效。

公共字段在 ApiError 对象及其子类中都是可用的。

如果你在密封类中指定了公共字段,则无效。例如:

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

  // one和two将具有相同的签名
  void one(Object x);

  void two();
}

你可以使用公共字段类型的子类在密封类中。例如:

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

  // x的类型为int
  void one(int x);

  // x的类型为String
  void one(String x);

  // x的类型为Object
  void three();
}

公共字段也适用于其他dart_sealed构造,如泛型和@WithType。例如:

@Sealed()
abstract class _Common {
  @WithType('num')
  dynamic get x; // 你可以省略dynamic

  // x的类型为int
  void one(@WithType('int') dynamic x); // 你可以省略dynamic

  // x的类型为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:
    - lib/**/*.sealed.dart

完整示例

以下是完整的示例代码:

import 'result.dart';
import 'weather.dart';

void main() {
  final a = Weather.sunny();
  final b = Weather.rainy(rain: 12);
  final c = Weather.windy(velocity: 1.5, angle: null);

  print(a);
  print(b);
  print(c);

  final d = Result.success(data: 1);
  final e = Result.success(data: 5.6);
  final f = Result.error(exception: 'error');

  print(d);
  print(e);
  print(f);
}

更多关于Flutter密封类注解插件sealed_class_annotations的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter密封类注解插件sealed_class_annotations的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用sealed_class_annotations插件的一个示例。这个插件可以帮助你在Dart中实现密封类(sealed class)模式,这在某些情况下可以提高代码的安全性和可读性。虽然Dart本身不支持密封类的概念,但我们可以利用注解和代码生成工具来实现类似的效果。

首先,你需要在你的pubspec.yaml文件中添加sealed_class_annotations依赖:

dependencies:
  flutter:
    sdk: flutter
  sealed_class_annotations: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来获取依赖。

接下来,让我们创建一个示例密封类。假设我们有一个表示操作结果的密封类,它可以是成功或失败:

  1. 创建一个Dart文件(例如result.dart),并添加以下代码:
import 'package:sealed_class_annotations/sealed_class_annotations.dart';
import 'package:json_annotation/json_annotation.dart';

part 'result.g.dart';

@sealed
abstract class Result<T> with _$Result<T> {
  const factory Result.success(T data) = Success<T>;
  const factory Result.failure(String message) = Failure<T>;
}

@JsonSerializable()
class Success<T> implements Result<T> {
  final T data;

  const Success(this.data);

  factory Success.fromJson(Map<String, dynamic> json) => _$SuccessFromJson(json);
  Map<String, dynamic> toJson() => _$SuccessToJson(this);
}

@JsonSerializable()
class Failure<T> implements Result<T> {
  final String message;

  const Failure(this.message);

  factory Failure.fromJson(Map<String, dynamic> json) => _$FailureFromJson(json);
  Map<String, dynamic> toJson() => _$FailureToJson(this);
}

注意,我们使用了@sealed注解来标记我们的抽象类Result,并且我们使用了json_annotation包来支持JSON序列化/反序列化。part 'result.g.dart';是为了让代码生成工具生成需要的代码。

  1. 运行flutter pub run build_runner build来生成result.g.dart文件。这个文件将包含序列化/反序列化所需的代码。

  2. 现在你可以在你的Flutter项目中使用这个密封类了。例如,在一个假想的ViewModel中:

class MyViewModel {
  Result<String>? _latestResult;

  Result<String>? get latestResult => _latestResult;

  void fetchData() async {
    // 模拟一个异步操作
    await Future.delayed(Duration(seconds: 1));
    _latestResult = Result.success("Data fetched successfully!");
    // 或者,如果操作失败
    // _latestResult = Result.failure("Failed to fetch data.");
  }
}
  1. 在你的UI层(例如一个Flutter组件中)使用这个ViewModel
import 'package:flutter/material.dart';
import 'my_view_model.dart';
import 'result.dart';

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final MyViewModel _viewModel = MyViewModel();

  @override
  void initState() {
    super.initState();
    _viewModel.fetchData();
    // 监听结果变化(这里简化为直接设置状态,实际应用中可能需要使用Provider等状态管理库)
    Future.delayed(Duration(seconds: 2), () {
      setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Sealed Class Example')),
      body: Center(
        child: _viewModel.latestResult == null
            ? CircularProgressIndicator()
            : _buildResultWidget(_viewModel.latestResult!),
      ),
    );
  }

  Widget _buildResultWidget(Result<String> result) {
    return result.when(
      success: (data) => Text(data),
      failure: (message) => Text('Error: $message'),
    );
  }
}

在这个例子中,我们创建了一个简单的Flutter应用,它模拟了一个数据获取操作,并根据操作的结果显示不同的UI。Result类是一个密封类,它只能有两种实例:SuccessFailure,这有助于确保我们的代码在处理结果时更加安全和清晰。

请注意,虽然sealed_class_annotations插件本身并不强制密封性(因为Dart不支持真正的密封类),但它通过注解和代码生成提供了一种模式,可以帮助开发者遵循这种设计模式。

回到顶部