likes
comments
collection
share

他山之石,Kotlin/Dart对比学,数据类与密封类

作者站长头像
站长
· 阅读数 28

Dart 中的数据类与密封类实现

前言

在现代编程的世界中,语言的选择成为了构建强大且有效应用程序的基础。每种语言都有其独特之处,它们通过不同的构造和概念来简化开发过程。Kotlin 和 Dart,作为当前流行的两种语言,都在开发者的工具箱中占有一席之地。

Kotlin 以其简洁的语法和对JVM的优秀支持而被广泛使用,而Dart则因为其与Flutter的亲密关系,在构建跨平台移动和Web应用方面变得越来越流行。

值得注意的是,这两种语言都提供了一些独特的特性来处理对象和类型层次,尤其是Kotlin的数据类(data class)和密封类(sealed class)概念。

Kotlin的数据类是一种避免样板代码并简化数据容器创建的方式。这些类自动生成equals()、hashCode()、toString()以及copy()方法,从而使数据处理变得更加高效。

与此相对的是Kotlin中的密封类,它提供了一种定义受限类层次的方法,使得类型更加安全和特定于上下文。

毕竟在 Flutter 开发的过程中一些框架也会涉及到类似 MVI 架构的思想,难免也会应用到数据类与密封类的一些功能。

这篇文章的目的是探讨 Dart 语言是否有类似的概念,以及如何在 Dart 中实现与 Kotlin 数据类和密封类相类似的功能。

一、Kotlin中的数据类与密封类

在Kotlin中,数据类(data class)是一种特殊的类,专门用于持有数据。最常见的数据类的使用场景是在不需要大量逻辑的地方,比如模型(Model)或者传输对象(DTO)。

当你声明一个数据类时,Kotlin编译器会默认为你生成几个重要的方法:equals()、hashCode()、toString()、copy()及所有属性的componentN()函数。

data class User(val name: String, val age: Int)

在上面的例子中,User类是一个数据类,它有两个属性:name和age。Kotlin自动为这个类生成了equals()方法来比较对象的内容,而不是它们的引用。

hashCode()方法为对象提供了哈希码。

toString()方法生成了一个包含所有属性值的字符串。

copy()方法可以用来复制对象,并且可以修改某些属性。

componentN()函数为结构声明提供支持。

都是比较常用的功能,我们在 MVI 中就常用到 copy() 方法复制对象,刷新页面状态。

    sendUiState(AuthUiState.SUCCESS(articleResult.data))

    updateUiState {
        copy(article = articleResult.data)
    }

而 Kotlin 的密封类是另一种特殊类型的类,在Kotlin中被用来表示受限的类继承结构。换句话说,一个密封类可以有子类,但所有的子类都必须在与密封类相同的文件中声明。

这提供了一种比接口更加严格的层次结构控制,因为它限制了可能的子类类型。密封类是一个抽象类,不可以直接实例化,并且它的构造函数默认是私有的。

sealed class Result
data class Success(val data: String) : Result()
data class Error(val error: String) : Result()

Result是一个密封类,它有两个子类:Success和Error。这种方式强制了Result的使用者必须处理所有的子类情况。

sealed class Result<out T> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

fun fetchData(): Result<String> {
    return try {
        // 模拟获取数据
        Result.Success("Data")
    } catch (e: Exception) {
        Result.Error(e)
    }
}

fun handleResult(result: Result<String>) {
    when (result) {
        is Result.Success -> println(result.data)
        is Result.Error -> println("Error: ${result.exception.message}")
    }
}

这样在使用when表达式时特别有用,因为编译器能够检测when表达式是否涵盖了所有的子类情况,从而保证了代码的完备性。

在 MVI 中我们常用密封类定义一个类,然后分别实现多个子类,标记为不同的事件,然后通过页面发送这些事件在ViewModel中处理对应的事件。

sealed class AuthIntent : IUiIntent {
    data class TestGson(val gson: Gson) : AuthIntent()
    object FetchArticle : AuthIntent()
    object PutKV : AuthIntent()
    class GetKV(val key :String) : AuthIntent()

    var name: String? = null
}
enum class AuthIntentEnum {
    FetchArticle,
    PutKV;

    // 枚举类不能直接携带状态,但可以定义方法
    fun performAction(vararg params: Any) {
        when (this) {
            FetchArticle -> println("Fetching Article...")
            PutKV -> if (params.isNotEmpty() && params[0] is Pair<*, *>) {
                val (key, value) = params[0] as Pair<*, *>
                println("Putting key-value: $key -> $value")
            }
        }
    }

    // 对于需要携带Gson或其他特定状态的情况,可以考虑将状态作为参数传递给方法,或者使用其他设计模式
    companion object {
        fun testGson(gson: Any) {
            // 这里处理gson相关的操作
            println("Testing Gson: $gson")
        }
    }
}

既然 Kotlin 数据类和密封类这么好用,如果我想在 Flutter 中使用类似 MVI 这种框架,我应该如何定义 Dart 的数据类和密封类呢?

二、Dart中怎么实现数据类功能?

数据类确实是方便,但是 Dart 并没有数据类,我们通常需要一些手段来实现这个功能达到模拟 Kotlin 数据库的功能。

  1. 你可以自己手撕代码,对类手动实现==、hashCode和toString等方法。

  2. 使用built_value: 这是一个由Google维护的包,它可以为你生成不可变的值类型。

  3. 使用freezed: 这是一个代码生成包,可以帮助你在Flutter/Dart项目中快速创建不可变的类,并自动实现==、hashCode、toString以及copyWith方法。

  4. 使用equatable: 这个包简化了对象比较的过程,你只需要定义props属性。

当然还剩下很多其他的第三方插件来实现对应的功能,这里不一一列举,抛开手动实现的我们不谈,先说说我们常用的数据类实现插件,这里以使用最多的 freezed 和 equatable 这两个插件为例。

freezed

freezed 是一个 Dart 的代码生成器,允许你定义不可变的类,它会自动生成一些常见的方法,如 ==、hashCode、toString以及copyWith等。这对于状态管理非常有用,特别是在 Flutter 应用程序中。

  1. 添加依赖。将 freezed 和 build_runner 添加到你的 pubspec.yaml 文件中的 dev_dependencies 部分,并将 freezed_annotation 添加到 dependencies 部分:
dependencies:
  flutter:
    sdk: flutter
  freezed_annotation: ^[latest_version]

dev_dependencies:
  flutter_test:
    sdk: flutter
  freezed: ^[latest_version]
  build_runner: ^[latest_version]

  1. 创建一个带有 freezed 注解的 Dart 文件:
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'my_class.freezed.dart';

@freezed
class MyClass with _$MyClass {
  const factory MyClass({String? name, int? age}) = _MyClass;
}

  1. 运行代码生成器:
flutter pub run build_runner build

我们就能看到生成的代码啦,需要注意的是我们需要在 const factory MyClass({String? name, int? age}) 中填写自己的变量属性。

equatable

equatable 插件可以帮助你轻松实现对象的等价比较,而无需手动重写 == 和 hashCode 方法。它通过简单地列出应该用于比较的属性来工作。

第一步,我们需要添加依赖

dependencies:
  flutter:
    sdk: flutter
  equatable: ^[latest_version]

其次我们只需要继承自 Equatable 即可:

import 'package:equatable/equatable.dart';

class MyClass extends Equatable {
  final String name;
  final int age;

  MyClass(this.name, this.age);

  @override
  List<Object?> get props => [name, age];
}

equatable 不需要额外的代码生成,因此一旦你写好类并添加了属性,你就可以开始使用它了。需要注意的是我们需要自己定义props的属性。

需要注意的是 equatable 虽然看似简单,不需要编译执行生成代码,但是它并没有生成 copyWith 方法。

Dart Data Class

难道要我一个一个的自己写copyWith?那不得疯,这里推荐一个 AS 插件。

他山之石,Kotlin/Dart对比学,数据类与密封类

简单暴力直接按照类的属性,生成对应的DataClass需要的类,包括 == 和 hashCode 和 copywith 等方法。

他山之石,Kotlin/Dart对比学,数据类与密封类

例如我们可以一键生成全部的 DataClass 方法。

@JsonSerializable()
class ArticleEntity {
	String? id;
	String? title;

	factory ArticleEntity.fromJson(Map<String, dynamic> json) => $ArticleEntityFromJson(json);

	Map<String, dynamic> toJson() => $ArticleEntityToJson(this);


//<editor-fold desc="Data Methods">
	ArticleEntity({
    this.id,
    this.title,
  });

	@override
  bool operator ==(Object other) =>
      identical(this, other) || (other is ArticleEntity && runtimeType == other.runtimeType && id == other.id && title == other.title);

	@override
  int get hashCode => id.hashCode ^ title.hashCode;

	@override
  String toString() {
    return 'ArticleEntity{' + ' id: $id,' + ' title: $title,' + '}';
  }

	ArticleEntity copyWith({
    String? id,
    String? title,
  }) {
    return ArticleEntity(
      id: id ?? this.id,
      title: title ?? this.title,
    );
  }

	Map<String, dynamic> toMap() {
    return {
      'id': this.id,
      'title': this.title,
    };
  }

  factory ArticleEntity.fromMap(Map<String, dynamic> map) {
    return ArticleEntity(
      id: map['id'] as String,
      title: map['title'] as String,
    );
  }

//</editor-fold>
}

当然你如果想配合 equatable 使用,只生成对应的 copyWith 方法也是可以的哦,不过都已经有这个编辑器插件了,直接用岂不是更方便。

其实除了这个编辑器插件,我发现还有很多还用的编辑器插件,可以替代 Flutter 插件,包括 build_runner 生成代码之类的,感觉更方便更快捷。

三、Dart中怎么实现密封类功能?

虽然Dart语言本身没有提供内置的密封类支持,但我们可以通过一些设计模式来模拟其功能。

这通常涉及创建一个抽象基类,然后定义一系列继承自这个基类的子类,以此限制类的扩展,并封装特定的一组类。以下是模拟Dart中密封类功能的步骤:

我们最常用的方法是创建一个抽象类作为基类,然后在创建基于它实现的每一个之类:

abstract class HomePageEvent {
  const HomePageEvent();
}

这个基类标明了它是一个用于组织相关子类的通用接口,并且通过声明为抽象类,它阻止了直接实例化。

接下来,我们为每个具体的事件创建继承自HomePageEvent的子类:

class HomePageDots extends HomePageEvent {
  final int index;

  const HomePageDots(this.index);
}

在这里,HomePageDots类代表了一个具体的事件,它携带了必要的信息,在这个例子中,是一个表示页面索引的index。

通过这种方式,我们能够定义一组明确的事件类型,每一种类型都有其特定的数据和行为。

这就模拟了密封类的核心特征:限制了可能的子类,并且每个子类可以拥有其特定的状态和逻辑。

同时我们也能使用 freezed 模仿密封类

import 'package:freezed_annotation/freezed_annotation.dart';

part 'home_page_event.freezed.dart'; // 文件名
part 'home_page_event.g.dart'; // 文件名

@freezed
abstract class HomePageEvent with _$HomePageEvent {
  const factory HomePageEvent.dots(int index) = HomePageDots;
  const factory HomePageEvent.anotherEvent(String value) = AnotherHomePageEvent;
}

或者使用 equatable 来辅助密封类:

abstract class HomePageEvent extends Equatable{
  const HomePageEvent();
}

class HomePageDots extends HomePageEvent {
  final int index;
  HomePageDots(this.index);
}

这样在 Bloc 这样的框架中,我们处理 Event 就会更加的准确。

class HomePageBloc extends Bloc<HomePageEvent, HomePageState> {
  HomePageBloc() : super(const HomePageState()) {
    on<HomePageDots>(_homePageDots);
  }

Dart 3.0 的密封类

了解过的可能知道,在Dart 3.0 之后已经支持了 sealed class 这种密封类的写法,但是很多人并没有直接使用这种方案,还是使用的 abstract class 的这种方式,难道是因为以后想兼容鸿蒙吗?要知道鸿蒙可不支持 Dart 3.0 。

sealed class 的用法其实和 abstract class 的用法类似,只是多了一些限制,多了一些编辑器支持,在使用switch 的时候要 case 所有的情况,因为编译器知道所有可能的情况,如果漏掉了会编辑器错误。(和Kotlin一样的)

标记一个类为 sealed 之后就多了一些限制。

  1. 该类为 abstract ,无法创建具体实例,无法直接构造。

  2. 所有子类型必须在同一个文件中定义,任何实现,扩展,继承都需要在同一个文件中。

示例:

sealed class ArticleState {
  final List<Article> data;
  const ArticleState({this.data = const []});
  factory ArticleState.empty() => const ArticleWithData();
}

class ArticleLoading extends ArticleState {
  const ArticleLoading({List<Article> data = const []}) : super(data: data);
}

class ArticleWithData extends ArticleState {
  final int total;
  const ArticleWithData({List<Article> data = const [], this.total = 0}) : super(data: data);
}

class ArticleFailed extends ArticleState {
  final String error;
  const ArticleFailed(this.error, {List<Article> data = const []}) : super(data: data);
}

当然如果你的项目没有升级到 sdk:sdk: ">=3.0.0 <4.0.0" 或者说你想兼容鸿蒙2.x 的语法,那么使用 abstract class 的方式也是完全可行的。

后记

在探讨Dart的数据类和密封类时,我们发现有多种方法可以实现相关功能。

对于数据类,equatable提供了一种直观而简便的方法,它通过继承Equatable基类来使对象比较更简单。同时,对于需要生成copyWith方法的场景,可以使用Android Studio插件如Dart Data Class来增强开发效率,而不是依赖于代码生成器如 build_runner。

当然如果你喜欢 build_runner 帮你自动生成代码,那么选择 freezed 也是很不错的方案,可以全自动化。

总的来说,个人还是比较喜欢 Equatable 这种简单的方式,如果是在 State 这种需要生成 copyWith 方法的时候我们使用 AS 插件 Dart Data Class 来生成对应的 copyWith 即可,相比使用 build_runner 自动生成的方式,更加效率,更加快速,更加节省。

对于密封类,大家完全可以根据自己的项目版本和需求使用 abstract class 或者 sealed class 的方式来实现。

我们理解了 Dart 的数据类与密封类之后,再回过头看 MVI 这种插件,比较类似的有 Bloc 插件,在其中就非常广泛的应用到了 Dart 的数据类与密封类,那么大家在学习或者工作中就能很好的理解,为什么要这么用,以及作者是用哪种方式实现的,怎么使用的。

同时我们可以更深入地理解一些框架是如何工作的,以及它们为何要采用特定的设计模式。这种知识可以帮助我们更好地理解和使用这些框架,无论是在学习还是在工作中,通过掌握这些概念,我们就能够认识到它们在构建可维护和可扩展的Flutter应用程序中的重要性。

文章到此就结束啦,其实本人开发Flutter项目的时间并不长,并不是资深Flutter开发者,文章难免有错误,思路难免会走弯,如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

由于不是具体的 Demo ,本文代码都已在文中贴出作为参考,如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

Ok,这一期就此完结。

他山之石,Kotlin/Dart对比学,数据类与密封类

转载自:https://juejin.cn/post/7372946993696849939
评论
请登录