ChatGPT Prompts

Flutter ChatGPT Prompts 项目

设置

  1. 在您的系统中安装 flutter_gen 并运行

    flutter_gen -c pubspec.yaml
  2. 安装 Git Hooks (可选)

    ./scripts/install-git-hooks.sh  
  3. 运行

    make check  

当您想确保代码干净且准备好进行代码审查时,此命令非常有用。它执行以下操作:

  • 运行 flutter_gen
  • 格式化您的代码
  • 运行 flutter analyze
  • 运行单元测试并重新创建所有 golden 测试截图文件
  • 运行 dart_code_metrics 检查

架构

该应用程序遵循 Clean Architecture 原则,并分为 4 个模块: 模块图 模块图

UI

UI 模块包含 Flutter 小部件、动画、路由、页面以及所有负责应用程序外观的代码。此外,重要的是要注意该模块省略了逻辑。

表现层

存放 PresenterPresentationModel 类的位置。

Presenter 决定 **何时** 显示内容。它们会触发 domain 模块的业务逻辑,并用相关数据更新 PresentationModel,然后由 UI 使用。

此外,PresentationModel 存储数据,并通过 ViewModel 接口将其公开给 UI。这确保了 UI 可以以**只读**方式访问数据,并且所有与表示相关的内部内容都是不可见的。

领域层

UseCaseRepositoryEntity 类。

此模块包含应用程序的业务逻辑。它决定在用户交互发生时 **应该** 执行什么。逻辑被封装在 UseCase 类中,这些类通过存储库与外部世界通信。

存储库接口在 domain 模块内指定,并由 data 模块实现。domain 操作的所有数据都封装在领域实体类中。

数据层

数据模块负责与“外部世界”通信并与第三方库交互。这里我们指定 REST API 交互、数据库访问、SharedPrefs 或外部传感器访问。所有数据都会被翻译成领域实体,这些实体对用于访问它们的库和技术是无关的。

导航

应用程序内的导航使用了 mixin Routes 和 per-page Navigators 的概念。

Navigator

Navigator 是一个包含 0 个或多个 Route mixin 的类,它的唯一工作是打开另一个页面。让我们通过一个例子来看一下。

假设我们有一个针对 LoginPage 的 Navigator,称为 LoginNavigator

class LoginNavigator with NoRoutes {
  AppInitNavigator(this.appNavigator);

  @override
  final AppNavigator appNavigator;
}

默认情况下,它与 NoRoutes mixin 混合,这是一个占位符,将被其他路由替换。LoginPresenter 使用此 Navigator 打开其他页面。我们可以通过以下方式实现:

class LoginNavigator with DashboardRoute {
  // ...
}

正如您所见,我们混合了 DashboardRoute

Route

下面您可以看到一个路由的实现。

mixin DashboardRoute {
  Future<void> openDashboard(DashboardInitialParams initialParams) async {
    return appNavigator.push(
      OnboardingPageTransition(
        getIt<DashboardPage>(param1: initialParams),
      ),
    );
  }

  AppNavigator get appNavigator;
}

它指定了 openDashboard 方法,现在可以用于 LoginNavigator 以打开 DashboardPage。它在底层使用 appNavigator,这是一个 Flutter 导航的包装器,可以简化导航,更多关于 AppNavigator 的信息请看下面。

这里的要点是,Routes 指定了给定页面的打开方式,而 Navigators 将这些路由聚集在一起,并通过混合它们来暴露给调用者。

AppNavigator 类

AppNavigator 是 Flutter 导航的包装器,允许我们在默认情况下无需提供 BuildContext 即可执行导航。它将使用 MaterialApp 的根导航器,除非您显式提供 BuildContext 以通过 Flutter SDK 的嵌入式 Navigator 小部件实现嵌套导航。

示例

下面您可以找到一个展示登录页面基本场景的流程图。

example diagram

工具

这是项目中使用的不同工具列表,它们可以增强和帮助我们日常工作。

FVM

为了保持团队所有开发人员一致的 Flutter 和 Dart 版本,我们使用 fvm,它是 Flutter 版本管理器。我们在此仓库下提交了一个配置文件 .fvm/fvm_config.json,其中指定了要使用的确切 Flutter 版本。在您的机器上安装 fvm 后,您可以在项目根目录下发出此命令以安装正确的 Flutter 版本:

fvm install

代码模板

此仓库托管了一些使用 Mason 创建的有用文件模板。您可以在 templates 子目录中找到它们。要使用这些,请安装 Mason(说明在此处)

Mason 命令

?注意?
您必须从 templates 子目录运行以下命令!

首先,运行 mason get 以在本地安装所有模板!

命令 描述
mason get 将所有模板安装到您的本地计算机。
mason list 显示可用模板列表。
mason make page 执行 page 模板。

自定义 lint

我们在 tools/custom_lints/ 文件夹中托管了一套我们自己的自定义 lint。这些是使用 custom_lint 包编写的。要运行 lint,您需要运行以下命令:

 fvm flutter pub run custom_lint

您可以在 custom_lint.dart 中找到所有 lint 的列表。

Fluttergen

我们使用 Fluttergen 为我们的资源生成适当的索引文件,这样所有路径都保存在常量中,您就不必记住在需要使用资源时更新它们或手动输入它们。

在您的系统上安装后,您可以运行以下命令:

fluttergen -c pubspec.yaml

在项目根目录下生成 Assets.gen.dart 文件等。

Dart Code Metrics

Dart Code Metrics 文档

Dart_code_metrics 是一个托管大量附加自定义 lint 的库。

我们使用以下命令来运行它(但请参考 Makefile 以获取最新的命令列表)。

fvm flutter pub run dart_code_metrics:metrics analyze lib --set-exit-on-violation-level=warning --fatal-style --fatal-performance --fatal-warnings
fvm flutter pub run dart_code_metrics:metrics check-unused-code . --fatal-unused
fvm flutter pub run dart_code_metrics:metrics check-unused-files . --fatal-unused --exclude="{templates/**,.dart_tool/**,lib/generated/**}"

Fastlane

Fastlane 文档

我们在 iosandroid 子文件夹中使用 fastlane 完成各种任务。两个平台通过引用 tools/fastlane 文件夹中的脚本来重用大量代码。要更深入地了解 Fastlane 的功能,请查看它们各自的 README。

代码最佳实践和指南

Pull Request 清单

规则 说明
先审查您自己的 PR 通读您自己的 PR 有助于发现明显的错误,并为审阅者节省时间。
确保所有 CI 检查都通过 CI 旨在捕获格式和 lint 错误,请加以利用,不要强迫他人做机器的工作?
运行应用程序并自行测试 在审查 PR 或提交您自己的代码时,请确保运行应用程序并进行测试,以确保代码不会破坏任何内容且运行正常。
不要害怕提问 代码审查不仅是为了发现他人代码中的错误,也是为了确保您理解代码并了解正在发生的事情。如果您有任何不清楚的地方,不要害怕提问,不要认为这是您的错,请提问!?

通用

规则 说明
为方法/构造函数参数和定义使用尾部逗号。 这样,每个参数都在单独的一行,在 PR 中添加新参数也更易于阅读。
优先使用命名参数 当使用多个参数时,请考虑使用命名参数,例如:错误:getBalances(true,"1283184"),错误:getBalances(id: "1283184", refresh: true)

UseCase

  • 执行业务逻辑,通过存储库与外部世界通信。
  • 应包含单个公共 execute() 方法,可选地带有执行业务逻辑的运行时参数。
  • 所有编译时依赖项都应通过构造函数注入。
  • 切勿在 usecases 中使用 json 类,始终通过存储库进行 API 通信。

Repository

  • 应在 domain/repositories 包中指定为接口,例如:UserRepository
  • 实现应放在 data/{tech} 包中,例如:RestApiUserRepositorydata/rest/ 包中。
  • 其工作是抽象与外部世界的通信(API、区块链、数据库、共享首选项)。
  • 将域模型转换为数据模型,反之亦然。以便 use cases 仅依赖于域实体。
  • 存储库方法应始终以 Future<Either<Failure,Result>> 类型的响应形式返回。
  • Result 部分需要是域实体或原始类型,切勿是 json 或特定于库的类!
    • 正确:StringUserint
    • 错误:UserJsonFirebaseUserFirebaseAuthResponse
  • Failure 应该是 domain entities 子文件夹中指定的类(请参阅 Failures)。

Failures

  • 应存储在 domain/model/failures 中。
  • 表示特定操作失败的特定域实体。Failure 应包含:
    • 类型
    • 原因
    • String toString() 方法。
    • DisplayableFailure displayableFailure() 方法(能够显示说明失败原因的错误对话框很有用)。

示例

enum VerifyPasscodeFailureType {
  Unknown,
  ValidationError,
}

class SomeFailure {
  // ignore: avoid_field_initializers_in_const_classes
  const SomeFailure.unknown([this.cause]) : type = SomeFailureType.Unknown;

  const SomeFailure.validationError(PasscodeValidationFailure fail)
      : cause = fail,
        type = SomeFailureType.ValidationError;

  final SomeFailureType type;
  final dynamic cause;

  DisplayableFailure displayableFailure() {
    switch (type) {
      case SomeFailureType.Unknown:
        return DisplayableFailure(
          title: strings.failureTitle,
          message: strings.failureMessage,
        );
    }
  }

  @override
  String toString() {
    return 'SomeFailure{type: $type, cause: $cause}';
  }
}

Domain entity

  • 应存储在 domain/model 中。
  • 域实体应继承自 Equatable
  • 所有字段都应为 final(以便我们鼓励 不变性)。
  • 所有字段都应为非可空,除非 null 值在给定上下文中是有意义的。

示例

class User extends Equatable {
  final String username;


  const User({
    required this.username,
    required this.firstName,
    required this.lastName,
  });

  @override
  List<Object> get props =>
      [
        username,
        firstName,
        lastName,
      ];
}

Presenter

  • 响应用户交互(例如:所有 onTap 方法都会转发给 presenter)。
  • 调用 use cases。
  • 使用新数据更新 presentation model。
  • 使用 cubit 发射状态(presentationModel)。
  • 切勿直接访问 PresentationModel 的 initialParams,而是通过 presentationModel 中的 getter 访问。
  • 不存储任何数据,它所需的所有数据都保存在 presentationModel 中。
  • presenter 永远不需要 BuildContext,它总是独立于其在 flutter 中使用的事实,因此永远不应依赖于 flutter 类。

ViewModel

  • 公开 presentation model 数据到页面的接口。
  • 仅包含 getter,视图不应直接修改任何状态。
  • 不应公开对 presenter 内部数据,不应由页面直接使用。

PresentationModel

  • 存储 presenter 和页面使用的数据。
  • 将 InitialParams 作为构造函数参数。

Page

  • 切勿直接访问 initialParams。
  • 将所有用户交互路由到 presenter,例如:InkWell(onTap: () => presenter.onTapLogin()
  • 使用 BlocBuilder 小部件监听 ViewModel 中的更改。
  • 仅从 ViewModel 访问要显示的数据。
  • 将 UI 分成更小的部件,提取到单独的文件中。

Json 类

  • *Json 类中的所有字段都应该是可空的。
  • 所有字段都应该是 final。
  • 应包含 toDomain() 方法,该方法将 json 转换为域实体。

示例

class UserJson {
  UserJson({
    required this.username,
    required this.firstName,
    required this.lastName,
  });

  factory UserJson.fromJson(Map<String, dynamic> json) =>
      UserJson(
        username: json['username'] as String? ?? '',
        firstName: json['first_name'] as String? ?? '',
        lastName: json['last_name'] as String? ?? '',
      );

  final String? username;
  final String? firstName;
  final String? lastName;

  Balance toBalanceDomain() =>
      Balance(
        username: username ?? "",
        firstName: firstName ?? "",
        lastName: lastName ?? "",
      );
} 

协作指南

以下是一些在处理此项目时需要记住的规则/想法:

新库

在向项目中添加新库之前,请与团队和技术负责人进行协商。重要的是不要用低质量的库污染代码,因为这可能会带来代码漏洞,并使维护更加困难。如果库提供的解决方案是微不足道的,最好将代码托管在项目内部,而不是依赖第三方。

// TODO

GitHub

查看 Github