← 返回蜂巢洞察

如何在Flutter开发中运用各种技能:开发者手册

关于人工智能辅助开发,最大的误解之一就是认为使用人工智能就意味着要放弃多年来积累的工程经验。其实并非如此。 你可以将自己所学到的架构模式、犯过的错误、团队遵循的规范,以及作为Flutter工程师所制定的标准,通过特定的技能教给人工智能编码助手。这样一来,你就不必在自身经验与人工智能之间做出选择,而是可以将两者结合起来使用。 然而,几乎每一位Flutter开发者,在第一次在实际项目中使用人工智能编码助手时,都会遇到一些令人沮丧的情况。 比如,当你让助手生成一个个人资料页面时,它虽然能生成能够正常运行的代码,但却没有在`widgets/`文件夹中创建一个结构清晰、可重复使用的`ProfileCar

关于人工智能辅助开发,最大的误解之一就是认为使用人工智能就意味着要放弃多年来积累的工程经验。其实并非如此。

你可以将自己所学到的架构模式、犯过的错误、团队遵循的规范,以及作为Flutter工程师所制定的标准,通过特定的技能教给人工智能编码助手。这样一来,你就不必在自身经验与人工智能之间做出选择,而是可以将两者结合起来使用。

然而,几乎每一位Flutter开发者,在第一次在实际项目中使用人工智能编码助手时,都会遇到一些令人沮丧的情况。

比如,当你让助手生成一个个人资料页面时,它虽然能生成能够正常运行的代码,但却没有在`widgets/`文件夹中创建一个结构清晰、可重复使用的`ProfileCard`组件,而是直接在页面文件中编写了一个`_buildProfileCard()`私有方法。

它也没有按照你精心设计的文件结构来安排代码:没有将`StatefulWidget`及其状态放在预定的位置,而是将其添加到了一个已经包含十个类的文件末尾。

数据模型使用了`Map`而不是你使用`freezed`注解定义的类;导入语句直接指向了内部包路径,而非你的项目结构中规定的路径;主题设置忽略了你的设计规范,而是采用了硬编码的十六进制值;错误处理机制使用了原始字符串,而不是你定义的类型化错误处理结构;而当你的团队使用Bloc进行状态管理时,它却使用了Provider。

严格来说,这些情况并没有什么错。人工智能助手之所以会犯这些错误,并不是因为它不擅长Dart语言,而是因为它不了解你的团队是如何编写Flutter代码的。

而这正是“助手技能”被设计出来所要解决的问题。

助手技能实际上是一些结构化的Markdown文件,它们教会人工智能助手如何完成特定的任务,而不仅仅是告诉它该做什么。当人工智能助手在生成代码之前学习了这些技能后,它就会掌握你们团队的规范、架构模式、文件组织规则、命名标准以及质量要求。这样生成的代码才能真正适合你们的项目。

Flutter团队在`github.com/flutter/agent-plugins`维护了一个官方的技能库,而Dart团队则在`github.com/dart-lang/skills`提供了补充性的技能资源。这些技能涵盖了响应式布局、声明式路由、JSON序列化、单元测试、静态分析、包依赖管理、模式匹配等多个方面。

但最强大的技能还是你自己编写的那些技能——它们凝聚了你作为工程师的丰富经验,反映了你们团队曾经犯过的具体错误,以及你们项目特有的开发模式。基于自己实际工作经验编写出的技能,其价值远超过十种通用的技能,因为它们能够有效避免你的团队在当前项目中真正可能遇到的问题。

这些技能适用于所有主流的人工智能编码辅助工具。无论你的团队使用的是Claude Code、Antigravity、OpenAI Codex、Cursor、GitHub Copilot CLI,还是其他任何兼容的辅助工具,这些技能都遵循相同的通用标准。只需编写一次代码,这些技能就能在所有这些工具中发挥作用。

这本手册涵盖了所有内容:什么是技能,它们内部是如何运作的,如何安装官方的Flutter和Dart技能,如何为每种主要的智能体配置这些技能,如何深入理解现有的技能,以及最重要的是,如何编写属于自己的技能,从而真正提升你的代码库中人工智能的输出效果。

它还介绍了每个Flutter团队都应该掌握的基本技能,每位开发者都能从中受益的Dart技能,以及那些能够使技能效果随着时间逐渐增强的高级模式。

读完这本书后,你不仅会知道如何使用这些技能,还会像编写高质量的Flutter代码一样有意识地去编写它们,并且你会明白,这样做其实是提升团队工程质量的最高效的投资方式之一。

目录

先决条件

在开始学习本指南之前,您需要具备以下条件。

1. 熟练掌握Flutter和Dart

您应该能够熟练开发多屏幕Flutter应用程序,了解状态管理模式,并遵循基本的代码架构原则。虽然不需要成为高级工程师,但本指南中的示例假设您已经知道什么是StatefulWidget、仓库模式的具体结构、密封类的作用,以及json_serializable的作用。

2>一个可正常使用的AI编程辅助工具

这些技能需要与诸如Claude Code、Antigravity、OpenAI Codex、GitHub Copilot CLI、Cursor等辅助工具配合使用。您至少需要安装并启用其中一个工具。本指南会针对所有这些工具提供具体的配置步骤。

3>已安装Node.js

用于安装官方技能的skills CLI工具是通过npm分发的。运行node -v即可检查是否已安装Node.js。如果尚未安装,请从nodejs.org下载。

4>一个可供练习的Flutter或Dart项目

本指南中的示例和练习最好在真实的项目中应用,而不是脱离实际环境进行学习。

5>对Markdown的基本了解

这些技能相关的文档都是用Markdown格式编写的。您需要知道什么是标题(##),代码块应该如何书写(使用三引号),以及YAML前置代码块的具体格式(文件开头的---标签)。

除了这些基本知识外,您不需要任何特殊的工具。这些文档都是普通的文本文件,只需放在项目中的相应文件夹里即可。在初次运行CLI命令之后,无需进行任何构建、编译或安装操作。

什么是辅助工具技能?

想象一下,雇佣一位熟悉Dart的开发者与雇佣一位拥有两年Flutter项目开发经验的开发者之间的区别。

两者都能编写可运行的Flutter代码,但经验丰富的那位开发者了解一些文档中并未明确写明的细节:例如你们的团队总是将组件代码提取到单独的文件中,而不是使用私有的构建方法;你们在处理加载状态时遵循某种特定的模式;你们的Bloc事件名称都是用过去式动词来命名的;在异步操作中,你们从不直接使用BuildContext,而会先检查mounted属性;此外,你们的团队还会使用fpdart库来处理Either类型,而不是让异常在不同层之间传播。

辅助工具技能其实就是将这些经验转化为可供AI辅助工具理解的形式。这类文档不仅说明了具体该做什么,还解释了应该如何去做、哪些行为是需要避免的,以及为什么会有这样的规则存在。

从形式上来说,代理技能为你的AI代理提供了一套标准化的、以任务为导向的操作指南。通过让代理具备特定的领域专业知识以及可重复执行的工作流程,你可以大大减少错误的发生,并确保各种操作能够遵循一致的模式进行。

关键在于“以任务为导向”。技能并非某种风格指南,而是一组针对特定类型工作制定的操作说明。

通用标准

这些技能遵循在agentskills.io上维护的规范。该规范定义了文件格式(使用Markdown格式,并添加YAML前置内容)、目录位置(.agents/skills/)以及命名规则。

由于这一规范具有通用性,因此相同的技能文件可以在Claude Code、Cursor、Antigravity、Codex等任何遵循该标准的AI工具中使用。

这种可移植性对团队来说非常重要。你无需为每个AI工具分别编写技能脚本,只需编写一份技能文件并提交到代码仓库中,团队使用的所有相关工具就能立即从中受益。

技能文件的存放位置

技能文件存储在项目工作区的.agents/skills/目录中。所有兼容的AI工具在开始处理任务时,都会自动找到这个目录并加载其中的技能文件。

yourflutter_project/
  .agents/
    skills/
      flutter-file-organization.md
      flutter-state-management-bloc.md
      flutter-testing.md
      flutter-theming.md
      flutter-error-handling.md
      flutter-navigation.md
      flutter-feature-architecture.md
      dart-unit-testing.md
      dart-static-analysis.md
      dart-pattern-matching.md
  lib/
  android/
  ios/
  pubspec.yaml

.agents/skills/这一目录名称是根据AI技能规范确立的。当某个AI工具开始在你的项目中执行任务时,它会自动找到这个目录,索引其中的技能文件,读取其元数据以了解可用的功能,只有当某项任务与某项技能的描述相匹配时,才会加载该技能的完整内容。

技能与系统提示或规则的区别

一次性提供的提示只会告诉AI工具你在当前会话中想要什么;而AI规则文件(如.cursorrulesCLAUDE.md)则会向所有AI工具传达适用于所有任务的全局性信息;而技能则能教会AI工具如何在未来的所有请求中正确地完成特定类型的工作,而且这些技能内容只会在相关情况下才会被加载。

当你为Flutter文件的组织结构编写一份技能脚本时,就无需在每次会话中都通过聊天来解释这些规则。每当你或你的团队成员要求AI工具创建、修改或重构Flutter文件时,这份技能脚本都会自动被加载,并提供始终如一的高质量指导。当有新成员加入团队并开始使用AI工具时,他们就能立即享受到团队编写的所有技能带来的好处,而无需有人手把手地教他们这些规则。

问题所在:为什么AI工具会在处理Flutter相关任务时出错

<要理解为什么这些技能是必要的,你就需要了解:在没有这些技能的情况下,人工智能系统在处理Flutter和Dart相关任务时会出现哪些具体且可预测的故障。这些故障并非随机发生的,它们其实都源于少数几个根本性原因,而正是这些技能被设计出来就是为了解决这些问题。>

训练数据问题

人工智能代理从其训练数据中获取了关于Dart和Flutter的知识。这些训练数据包括来自公共仓库的数百万行Flutter代码、相关文档、教程以及论坛上的答案。其中既包含旧有的编程模式(在零安全机制出现之前的Dart编码方式),也包含不良的编程习惯(例如使用“God类组件”),还有一些虽然单独来看是正确的,但并不符合特定团队的开发标准。

当代理在没有相应技能的情况下生成代码时,它会从这些混合在一起的训练数据中提取信息。因此,它可能会生成类似于2021年某些教程中的代码——那些教程中到处都在使用`setState`方法;或者会生成与某个仓库中的代码风格相似的代码,而如果你的团队实际上在使用Bloc框架,那么这种写法就是错误的;又或者,当你的团队严格遵循GoRouter来进行深度链接处理时,代理却可能会使用`Navigator.push`方法。

一张分为两部分的图表,展示了没有团队特定技能的人工智能代理与具备这些技能的代理之间的区别。上半部分显示了各种训练数据如何形成一些可能不符合团队开发标准的编程模式;下半部分则说明了当这些训练数据结合团队制定的文件组织规则、BLoC状态管理规范、错误处理机制、主题设计标准以及测试要求后,最终生成的代码是如何与现有代码库保持一致的。

技能并不会取代代理已有的知识,而是为它提供了一个明确的工程开发框架。如果没有这些技能,代理就会从质量参差不齐的各种编程模式中随意选择;而一旦具备了团队定义的技能规范,这些编程模式就会被项目的架构、惯例和标准所约束,从而使得生成的代码与现有代码库更加协调一致。

最常见的Flutter特定错误

1. 使用私有构建方法而非提取出的组件。

有一次,当要求代理生成一个复杂的界面时,它在界面类中定义了诸如`_buildHeader()`、`_buildStatsList()`和`_buildActionBar()`这样的私有方法。这种写法在Dart语言中是可行的,但从架构角度来看却是有害的——这些组件实际上应该被单独提取出来,成为位于`widgets/`文件夹中的可重用组件类,并且应该能够进行独立测试。

2. 将`StatefulWidget`与`State`分开存放。

在分割大型代码文件时,代理可能会将`StatefulWidget`类放在一个文件中,而将`State`类放在另一个文件中。然而,这种做法违反了Flutter编译的基本规则——这两个类必须始终被放在同一个文件中。

3>忽视你选择的状态管理方式。

如果代理不知道你偏好的状态管理方案,它就会从训练数据中选取出现频率最高的那种模式来使用。有时它会生成Bloc组件,有时会生成Provider组件,有时又会使用`setState`方法——而所有这些代码最终都会被集成到同一个代码库中。

4. 使用 Map 而非类型化的模型。

如果不了解您的序列化规范,代理会默认使用 Map。如果您的团队使用了 freezedjson_serializable,那么生成的每个模型都需要被完全重新编写。

5. 硬编码的视觉值。

代理会默认使用字面值,例如 Color(0xFF6750A4)EdgeInsets.all(16)BorderRadius.circular(8)。如果您的项目拥有包含主题扩展和间距常量的设计系统,代理也会完全忽略这些设置。

6>到处使用内联注释。

许多团队会刻意避免使用代码注释,而是通过具有描述性名称的代码来实现自我文档化。但由于大多数训练数据中都包含注释,代理会默认添加解释性注释,因此每次代码审查时都需要对这些注释进行清理。

7>错误的导入路径。

代理可能会从内部包路径导入代码(例如 package:myapp/src/internal/models/user.dart),而不是通过项目的主文件路径来导入(例如 package:myapp/features/profile/profile.dart),这样就会造成与内部 API 的不必要的耦合,而这些内部 API本应被隐藏起来。

8>原始的异常处理方式。

由于不了解您的错误处理机制,代理会在代码中到处使用 try-catch 语句以及原始的 Exception 对象,从而忽略您团队定义的类型化错误层次结构,导致整个代码库中的异常处理方式不一致。

技能的工作原理:渐进式展示

技能背后的实现机制非常简洁高效。代理并不会一开始就将所有指令都加载到上下文窗口中,而是先读取元数据,只有当真正需要执行某项任务时,才会加载详细的指令内容。

Flutter 的文档将这种机制称为“渐进式展示”,这与 Flutter 本身所采用的延迟加载机制类似。

这种设计有效地解决了一个实际问题:AI 代理的上下文窗口容量是有限的。如果每个技能在每次执行任务时都会加载全部内容,那么代理就会浪费大量的资源来处理无关信息。例如,在要求代理编写单元测试时,导航技能并不需要被加载到上下文中;而在设置路由规则时,测试技能也同样不需要被加载。

一张解释 AI 代理技能渐进式展示机制的图示。第一阶段显示代理仅读取每个技能文件的前导内容,从而减少上下文使用的开销;第二阶段显示代理根据用户请求匹配相关的技能,并只加载这些技能的全部内容,而忽略无关的技能。这样就能确保只使用所需的信息,避免不必要的资源消耗。

逐步披露信息的方式有助于让智能体始终保持对相关信息的关注。首先,智能体会为所有可用的技能生成简洁的元数据索引。当有任务出现时,它会利用这些元数据来确定哪些技能与当前任务相关,并仅加载这些技能的完整使用说明,而不会加载不相关的技能。这样一来,既能减少不必要的信息占用,又能确保智能体获得完成任务所需的详细指导。

这种逐步披露信息的机制意味着,你在项目中可以使用很多技能,而无需担心信息量会过度膨胀。拥有二十个技能并不会使开发成本增加二十倍;对于任何具体的任务来说,系统只会加载其中相关的那部分技能内容。

技能文件的构成结构

每种技能都遵循特定的结构格式。深入理解这一结构是编写有效技能描述的前提条件。

---
name: 技能名称(采用驼峰命名法)
description: 一份清晰、具体的描述,需要回答以下问题:这项技能涵盖哪些内容?在什么情况下应该使用它?哪些关键词表明当前任务需要这项技能?智能体在判断某项技能是否相关时,只会读取这一部分描述。
aliases: [其他名称]
sources: [聊天记录、代码]
---

# 技能标题

简要说明这项技能的作用及其存在的必要性。

## 第一主要章节

包含具体且可操作的规则内容。

## 第二主要章节

提供更多的规则、示例以及反例。

## 代码示例

通过具体的代码来展示相关模式。

前言部分的详细构成

---
name: Flutter文件组织结构
description: 用于组织和划分Flutter/Dart文件,同时确保StatefulWidget与State之间的关系得到保持。适用于创建、重构、拆分或重新组织Dart文件及类时。无论是在新建屏幕、组件、模型还是Bloc文件时,还是在修改现有文件的结构时,都可以使用这一规则。
aliases: [flutter-files, dart-organization]
sources: [聊天记录、代码]
---

name是项目中标识这项技能的唯一名称。它遵循驼峰命名法(用连字符连接小写字母),并且通常以所对应的开发平台或领域作为前缀(例如flutter-dart-等)。智能体在进行分析时会使用这个名称来引用相应的技能,而CLI工具在管理技能时也会依赖这一名称。

description是整个文件中最为重要的字段。在智能体进行初步索引时,它会是唯一被读取的部分。如果描述写得不好,那么即使技能内容编写得非常详尽,也不会被加载出来。描述应该回答三个问题:这项技能涵盖哪些功能?在什么情况下应该使用它?有哪些具体的关键词或短语能够表明这项技能与当前任务相关?从示例中可以看出,描述中包含了“适用于创建、重构、拆分或重新组织文件时”这样的表述,同时还列出了各种文件类型;这些信息都是帮助智能体将任务需求与相应技能匹配起来的关键线索。

aliases为该技能提供了替代名称,代理可以使用这些名称来引用它。这些替代名称是可选的,但在某些情况下,当同一技能在不同场景下可能有不同的称呼时,它们会非常有用。

sources指明了这一技能的来源。对于自定义团队技能而言,其来源通常为[chat];而对于来自第三方包作者的技能,其来源可能包括[package]等。

技能内容的结构

技能内容的编写采用纯Markdown格式,并遵循特定的结构规范,这样代理在阅读时就能更有效地理解其中的内容:

# 标题部分 (h1)
简要介绍该技能的背景信息。它解决了什么问题?为什么会被设计出来?
这部分内容应控制在三句话以内。

## 核心规则 (h2部分)
以编号或列表的形式列出具体且可验证的规则。
每条规则都应当能够被独立执行。

## 具体子领域指南 (h2部分)
针对技能的某个特定子领域提供更详细的指导。
先说明错误的操作方式,再展示正确的做法。

代理通过标题结构来理解技能的内容组织方式。清晰明确的##标题能够帮助代理在更大的任务框架中找到与当前子任务相关的具体内容。

安装官方Flutter和Dart技能

Flutter和Dart团队维护着官方技能仓库,其中积累了多年来关于这些技术生态系统中最佳实践的知识。这些仓库就是你开始学习的起点。

安装Flutter技能

npx skills add flutter/agent-plugins --skill '*' --agent universal --yes

npx skills add命令通过npm运行skills命令行工具,无需进行永久性安装。flutter/agent-plugins是指Flutter团队维护官方Flutter技能的GitHub仓库路径。--skill '*'是一个通配符,它会从仓库中安装所有可用的技能,而不会只选择特定的几个。选项会将这些技能放置在.agents/skills/目录中,这是所有兼容代理都会查找的默认位置。--yes选项会跳过交互式确认提示,因此可以将这个命令添加到项目配置脚本或Makefile中。

运行这条命令后,你的项目将获得以下技能:响应式布局功能、使用GoRouter实现的声明式路由系统、使用json_serializable进行的JSON序列化处理、集成测试设置工具、组件预览功能、组件测试工具,以及遵循BLoC和Clean Architecture原则的架构最佳实践、Bloc状态管理机制、Bloc表单设计等。

安装Dart相关技能

npx skills add dart-lang/skills --skill '*' --agent universal --yes

Dart团队维护了一套专门针对Dart语言本身的技能工具,这些工具与Flutter的组件系统无关。对于Flutter应用程序以及纯Dart项目来说,这些技能都非常有用——无论是CLI工具、后端服务还是其他包文件。

官方提供的Dart技能工具包括:单元测试生成、静态分析配置、包依赖管理、模式匹配与密封类处理、CLI应用程序的开发、测试覆盖率的收集与分析、利用LSP修复运行时错误、使用Mockito生成模拟对象、通过ffigen进行FFI绑定、为C/C++集成准备原生资源、优化Dart程序的内存使用,以及将旧的测试断言方式迁移到现代的package:checks标准。

一次性安装两者

npx skills add flutter/agent-plugins dart-lang/skills --skill '*' --agent universal --yes

如果在一条命令中同时列出这两个仓库的名称,系统会一次性将它们全部安装完毕,并完成依赖关系的解析过程,这样比分别执行两条命令要快一些。对于新创建的Flutter项目来说,这种安装方式是推荐的做法。

通过pubspec依赖关系来安装技能

技能工具生态系统最强大的特点之一就是:包开发者可以将这些技能工具与他们的软件包一起发布。作为Dart包提供的skills CLI工具能够自动检测并安装你项目中所有依赖包中包含的技能工具:

dart pub global activate skills
skills get

dart pub global activate skills这条命令会将skills CLI工具全局安装在你的机器上。skills get则会读取你的pubspec.yaml文件和pubspec.lock文件,找出所有依赖包中包含skills/目录的包,并自动将这些技能工具安装到你项目的.agents/skills/目录中。

当你向项目中添加一个新的包并运行skills get命令时,系统会立即根据该包开发者提供的说明来指导你如何正确使用这个包。这种设计方式从根本上改变了以往的情况:不再是系统去猜测包的功能,而是包开发者直接为系统提供了正确的使用方法。

# 每当依赖关系发生变化时,及时更新技能工具
flutter pub get
skills get

运行flutter pub get命令可以更新你的项目依赖关系,而紧接着运行skills get命令则能确保技能工具与新的依赖关系保持一致。养成这种两步操作的习惯,就能保证你的系统始终使用的是与当前依赖关系相匹配的最新技能工具。

验证已安装的技能工具

ls -la .agents/skills/

ls -la .agents/skills/ 命令会列出所有已安装的技能文件及其详细信息。你会看到每个已安装的技能都会对应一个以 .md 为扩展名的文件。-la 参数会显示隐藏文件以及包括文件大小和修改日期在内的详细信息。

安装完成后,测试你的智能体是否能够识别这些技能:

我安装的这些技能中,哪些可以帮助我创建一个新的功能界面?

智能体会列出与这个任务相关的技能,从而确认这些技能已经被正确加载并索引到了系统中。每当你向项目中添加新的技能时,这个测试都是非常有用的。

在 Claude Code 中使用技能

Claude Code 是 Anthropic 开发的一款智能编码助手,可以在终端中运行。它是非常强大的工具,特别适合处理复杂的多步骤 Flutter 开发任务,并且对技能标准也提供了很好的支持。

为 Claude Code 安装 Flutter 插件

推荐的做法是安装完整的 Flutter 插件,因为该插件会将技能与 MCP 服务器配置捆绑在一起:

claude mcp add flutter-mcp -- dart pub global run dart_mcp_server
npx skills add flutter/agent-plugins --skill '*' --agent claude-code --yes
npx skills add dart-lang/skills --skill '*' --agent claude-code --yes

claude mcp add flutter-mcp 命令会将 Dart MCP 服务器注册到 Claude Code 中。MCP 服务器可以让 Claude Code 直接访问 Flutter 的文档、pub.dev 上的包信息以及 Dart 开发工具,而无需进行网络搜索。--agent claude-code 参数确保技能会被存储在 Claude Code 专用的目录中(如果该目录与通用的 .agents/skills/ 目录不同的话);不过 Claude Code 也会从通用目录中读取技能信息。

Claude Code 的技能目录结构

Claude Code 会自动从 .agents/skills/ 目录中读取技能信息。如果你希望将 Claude 特有的技能与通用技能分开存储,也可以将其保存在 .claude/skills/ 目录中。

your_project/
  .agents/
    skills/
      flutter-file-organization.md    <- 通用目录,适用于所有场景
      flutter-bloc-state-management.md
  .claude/
    skills/
      claude-specific-workflow.md     <- 仅适用于 Claude Code
    CLAUDE.md                         <- Claude Code 的规则文件

Claude Code 的规则与技能

Claude Code 会使用位于项目根目录下的 CLAUDE.md 文件作为规则文件:这些规则是适用于整个项目的通用指令,无论执行什么任务,这些规则都会被正确应用。

各项技能会分阶段逐步被加载。对于项目的相关信息(例如使用了哪个包、哪个版本的SDK,或者安装了哪种状态管理库),请使用`CLAUDE.md`文件来查看;而对于那些与特定任务相关的专业技能知识(比如如何实现Bloc框架、如何组织文件结构,或如何编写测试用例),则可以直接利用这些技能相关的内容来进行学习。
# CLAUDE.md 示例

这是一个名为 Kopa 的 Flutter 应用程序,它是一种个人预算管理工具。

## 技术架构
- Flutter 3.47,搭配 Dart 3.10
- 状态管理:flutter_bloc ^9.0.0
- 导航系统:go_router ^14.0.0
- 数据层:使用 firebase_ai ^2.0.0来实现人工智能功能
- 序列化:采用 freezed 和 json_serializable
- 测试工具:bloc_test、mocktail

## 包名
com.example.kopa

## 最低运行要求
Android API 24,iOS 15

## 项目结构
采用以功能为导向的设计架构。具体结构详情可参考“flutter-feature-architecture”相关内容。

CLAUDE.md 文件中记录了项目中一些不会随任务变化而改变的信息,例如应用程序名称、所使用的包、SDK版本以及最低平台要求。而“技能”部分则介绍了如何使用这些包以及如何正确组织代码的相关知识。

在 Claude 代码会话中使用技能

一旦安装了所需的技能,Claude Code 会自动使用它们,无需手动调用。例如,当你提出如下请求时:

创建一个使用 Bloc 状态管理、从 Firestore 获取数据的用户资料功能模块,并设计一个能够显示加载状态、数据内容以及错误信息的界面。

Claude Code 会检测到这个请求涉及多个技能领域(功能架构设计、Bloc 状态管理、文件组织结构,可能还包括主题设置和错误处理),然后加载相应的技能文件,并生成符合你们团队编码规范的代码。

你也可以明确指定需要使用的技能:

利用“flutter-bloc-state-management”技能来实现购物车功能模块中的 CartBloc 逻辑。

通过明确指定技能名称,可以确保 Claude Code 一定会加载该技能,而不会自动选择其他替代方案。

结合 Antigravity 使用技能

Antigravity 是谷歌开发的人工智能编码助手,它与 Flutter 生态系统深度集成,并由 Flutter 团队共同开发。Antigravity 对代理技能提供了全面的支持,也是经过官方 Flutter 技能测试最为完善的辅助工具之一。

安装 Antigravity 的 Flutter 插件

为 Antigravity 手动安装技能

如果您更喜欢手动安装,或者需要添加自定义的团队技能,可以按照以下步骤操作:

npx skills add flutter/agent-plugins --skill '*' --agent antigravity --yes
npx skills add dart-lang/skills --skill '*' --agent antigravity --yes

--agent antigravity这个选项指定了Antigravity专用的技能目录;不过Antigravity也会从通用的.agents/skills/目录中读取技能信息。

使用技能构建Antigravity工作流程

Antigravity支持“工作流程”,这种预定义的任务序列能够引用各种技能。您可以为常见的团队任务创建相应的工作流程:

# .antigravity/workflows/new-feature.md

## 创建新的功能开发工作流程

需要使用的技能:flutter-feature-architecture、flutter-bloc-state-management、flutter-testing、flutter-file-organization

步骤:
1. 创建相应的项目文件夹结构。
2> 使用Freezed框架构建领域模型。
3> 设计并实现仓库接口。
4> 创建包含事件和状态的Bloc组件。
5> 设计屏幕界面对应的Widget组件。
6> 提取可重用的组件代码。
7> 为仓库编写单元测试用例。
8> 为Bloc组件编写`bloc_test`测试用例。
9> 为屏幕界面编写相应的测试用例。

使用技能来构建工作流程,能够确保代理在执行多步骤任务时始终遵循正确的操作规范。如果没有这种明确的关联机制,代理可能会在第一步中使用文件组织相关的技能,但在后续步骤中忽略测试相关的技能。

在OpenAI Codex中使用技能

OpenAI Codex是一款基于终端的智能编码辅助工具,其功能与Claude Code类似。它可以在您的终端环境中运行,并针对您的代码库执行多步骤任务。

为Codex安装所需技能

npx skills add flutter/agent-plugins --skill '*' --agent codex --yes
npx skills add dart-lang/skills --skill '*' --agent codex --yes

--agent codex这个选项指定了Codex专用的技能目录。不过Codex也会从通用的.agents/skills/目录中读取技能信息,因此--agent universal选项也同样适用。

Codex规则文件

与Claude Code的CLAUDE.md文件类似,Codex也会从项目根目录下的AGENTS.md文件中读取配置信息。您需要将这个文件与相关的技能配置一起进行设置:

# AGENTS.md

Flutter项目:Kopa预算管理应用
所使用的技术栈:flutter_bloc、go-router、firebase_ai、freezed
开发架构:以功能为导向,采用清晰的设计结构
测试框架:bloc_test + mocktail

AGENTS.md是整个项目共用的配置文件,Codex会在执行每项任务时都会读取这个文件。请保持文件内容简短,一般5到15行即可,主要说明项目中最重要的信息。详细的操作规范应该放在具体的技能配置文件中,而不是AGENTS.md里,因为技能配置文件是按需加载的,而AGENTS.md文件则会在整个项目启动时就被一次性读取完毕。

关于Codex插件的安装说明

目前,Codex插件无法自动合并规则文件。这意味着,从flutter/agent-plugins安装Flutter插件后,虽然相关技能会被添加进来,但系统不会自动生成AGENTS.md文件。

在完成插件安装后,需要手动创建这个文件。Flutter官方文档提供了适用于Flutter项目的AGENTS.md文件模板。

如何在Cursor中使用这些技能

Cursor是一款基于VS Code开发的人工智能代码编辑器。它将代理功能直接整合到了编辑体验中,并通过其规则系统以及通用的.agents/skills/目录来支持各种技能。

如何在Cursor中安装这些技能

npx skills add flutter/agent-plugins --skill '*' --agent cursor --yes
npx skills add dart-lang/skills --skill '*' --agent cursor --yes

Cursor会从.agents/skills/目录中读取相关技能信息。添加--agent cursor参数可以确保这些技能能够被Cursor的正确识别和加载。

Cursor与规则文件的集成

Cursor会使用.cursorrules目录(在较新版本中则是.cursor/rules/目录)来存储针对整个项目的规则设置,这与Claude Code中的CLAUDE.md文件的作用类似。

# .cursor/rules/flutter.mdc

---
description: 适用于所有Dart文件的Flutter项目规则
globs: ["**/*.dart", "pubspec.yaml"]
alwaysApply: true
---

这是一个使用flutter_bloc、go_router和freezed框架的Flutter项目。
所有的状态管理都采用了Bloc模式。
文件夹结构清晰,以功能为导向进行设计。
具体规则细节请参见 `.agents/skills/` 目录。

globs: ["**/*.dart"]这一设置确保只有在对Dart文件进行编辑时才会应用这些规则,从而避免在编辑Markdown文件或配置YAML文件时加载不必要的Flutter规则。alwaysApply: true则保证了每当相关文件被打开时,这些规则都会被自动执行。

在规则描述中引用.agents/skills/目录是有意为之的——这样可以让代理直接从该目录中获取所需的技能实现细节,而无需让规则文件变得过于冗长。

如何在Cursor中结合Composer、Chat和技能使用

在Cursor的Composer多文件编辑模式下,描述任务时相关技能会自动被加载;而在Cursor的Chat内嵌助手中,则可能需要更明确地指定所需技能:

@flutter-file-organization 创建一个新的PostCard组件
该组件是从帖子列表界面提取出来的

在Cursor的Chat功能中,@前缀可以在某些配置下根据技能名称来直接调用相应技能;而在其他情况下,只要详细描述任务内容,代理就能自动加载相应的技能。

在其他代理中使用技能

GitHub Copilot CLI

当以代理模式运行时,GitHub Copilot CLI支持通用的.agents/skills/目录(例如执行gh copilot explaingh copilot suggest命令):

npx skills add flutter/agent-plugins --skill '*' --agent copilot --yes

根据skills CLI的文档说明,使用skills get命令时,GitHub Copilot并不会被自动检测到;因为.github/目录通常还有其他用途。因此,在为Copilot安装技能时,务必使用明确的--agent copilot参数。

Gemini CLI

Google的Gemini CLI也支持通用的技能目录:

npx skills add flutter/agent-plugins --skill '*' --agent gemini --yes

通用安装方式

如果你希望一次性安装所有适用于各种代理的技能,可以执行以下命令:

npx skills add flutter/agent-plugins --skill '*' --agent universal --yes
npx skills add dart-lang/skills --skill '*' --agent universal --yes

使用universal代理选项时,技能会被保存在.agents/skills/目录中,所有兼容的代理都会自动识别这些技能。对于那些需要同时使用多种代理或希望不受代理类型限制的团队来说,这种设置是推荐的默认方案。

验证技能识别功能

无论你使用的是哪种代理,都可以通过向该代理提出一个自然语言问题来验证其是否能够正确识别这些技能:

请总结一下您为当前项目准备的这些技能所具备的功能。

如果代理配置正确,它会列出已安装的技能及其描述,从而证明技能识别功能是正常的。如果代理提示它没有找到任何技能,或者无法识别这些技能,请检查以下内容:

  1. 项目根目录下是否存在.agents/skills/目录

  2. 该目录中是否包含含有有效YAML格式前置内容的.md文件

  3. 所使用的代理是否支持通用的技能识别规范

官方Flutter技能介绍:深入解析

官方的Flutter技能仓库(flutter/agent-plugins)中收录了一组技能,这些技能体现了Flutter团队在常见开发模式上的最佳实践。了解每项技能的具体功能,有助于你决定应该安装哪些技能,哪些技能需要自定义,以及哪些技能可以通过自己开发的代码来补充。

flutter-responsive-layout

这项技能教会代理如何构建能够在移动设备、平板电脑和桌面电脑上正确显示的布局。它涵盖了AdaptiveScaffoldLayoutBuilderMediaQueryBreakpoints等概念,以及Flutter自适应框架推荐的处理不同屏幕尺寸的方法。

如果没有这项技能,开发人员构建出的布局在某一种设备上看起来很正常,但在其他设备上就会出问题。而掌握了这项技能后,开发人员就能从第一行代码开始就实现响应式布局设计,他们会使用Flutter特有的工具来进行开发,而不是依赖硬编码的像素阈值。

flutter-declarative-routing

这项技能会教授如何配置GoRouter、定义路由规则、实现嵌套导航功能、处理认证过程中的重定向逻辑、配置深度链接,以及如何在不同路由之间传递类型化的参数。

如果没有这项技能,开发人员即使在使用GoRouter的代码库中,也常常会使用Navigator.push这种方法;他们还经常搞错深度链接的实现方式,并且难以掌握GoRouter所要求的类型化参数传递机制。

flutter-json-serialization

这项技能会教授如何使用json_serializablefreezed这些工具:如何添加注释、运行build_runner命令、创建fromJson/toJson方法、处理可空字段,以及如何使用@JsonKey来实现字段名称与序列化数据之间的映射。

如果没有这项技能,开发人员就不得不手动编写序列化代码,或者在整个数据层中使用Map这种数据结构,这样一来,当字段名称发生变化时,代码就会出现故障。

flutter-add-widget-test

这项技能会教授如何使用testWidgetsWidgetTester这些工具,以及各种测试策略(如pumppumpAndSettlepumpWidget),如何查找特定的 widgets(通过find.textfind.byTypefind.byKey等方法),如何模拟用户手势,以及如何为Widgets构建简洁但功能完备的测试环境。

flutter-add-integration-test

这项技能会教授如何在设备上、网页浏览器中或Firebase Test Lab环境中设置并运行端到端的集成测试。它涵盖了测试环境的配置、IntegrationTestWidgetsFlutterBinding的使用方法、应用程序的启动顺序,以及如何在测试过程中与已经运行的应用程序进行交互。

flutter-bloc

这项技能会全面讲解Bloc开发模式:如何定义事件、状态以及Bloc类,如何为Bloc提供BlocProvider,如何使用BlocBuilderBlocListenerBlocConsumer来使用Bloc对象,以及如何使用bloc_test工具来进行测试。

flutter-apply-architecture-best-practices

这项技能会帮助开发人员按照Flutter官方推荐的BLoC模式来遵循“清晰架构”原则(包括数据层、业务逻辑层和用户界面层的划分),它会明确各层次之间的边界、依赖关系规则,以及代码仓库的构建方式。

flutter-add-widget-preview

这项技能会介绍Flutter 3.47版本中引入的Widget预览功能,包括如何使用@Preview注释、如何搭建预览环境,以及如何为复杂的Widgets编写有用的预览代码。

官方Dart技能详解

Dart团队的官方技能仓库(dart-lang/skills)主要涉及Dart语言本身,而非Flutter的组件系统。这些技能适用于任何Dart代码:包括Flutter应用程序逻辑、Dart命令行工具、Dart后端服务以及各种Dart包。

dart-add-unit-test

这是最基础的Dart技能,也是具有最直接实用价值的技能。它教会开发人员如何为任何Dart类编写正确的单元测试,具体内容包括:

  • 设置test/目录,使其结构与lib/目录相同

  • 使用描述性名称来编写grouptest

  • 利用setUptearDown进行测试生命周期管理

  • 正确使用expect及相应的匹配器

  • 通过mocktail模拟依赖关系

  • 使用expectLater及流式匹配器来测试异步代码

如果没有掌握这项技能,开发人员编写的测试可能会检测错误的内容,使用错误的断言方式,而且测试文件的结构也会与源代码结构不一致。而掌握了这项技能后,开发人员从第一次开始就能编写符合package:test规范的测试代码。

# dart-add-unit-test的主要内容

## 测试文件的存放位置
test/features/profile/data/profile_repository_test.dart
mirrors
libfeatures/profile/data/profile_repository.dart

## 测试用例的命名规则
```
group('ProfileRepository', () {
  group('getProfile', () {
    test('当API调用成功时,该方法应返回ProfileLoaded', () async {
      // ...
    });

    test('当连接失败时,该方法应返回NetworkFailure', () async {
      // ...
    });
  });
});
```

## 异步代码的测试方法
```
await expectLater(
  repository.getProfile('user123'),
  completion(isA<Right<AppFailure, UserProfile>>>()),
);
```

dart-run-static-analysis

这项技能教会开发人员如何使用Dart的静态分析工具:配置analysis_options.yaml文件,运行dart analyze命令,应用dart fix --apply命令,理解代码检查规则,正确处理误报情况,并强制执行严格的类型检查。

# dart-run-static-analysis涵盖的内容

## configuration文件analysis_options.yaml的配置方法
include: packageflutter_lints/flutter.yaml

analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'

linter:
  rules:
    avoid_print: true
    prefer_final_fields: true
    require_trailing_commas: true

## 如何正确处理误报
// ignore: avoid_print  <- 仅针对单行代码中的误报
// ignore_for_file: type=lint  <- 仅针对生成的测试文件中的误报

正确配置`analysis_options.yaml`是那些经常需要人工操作却容易出错的环节之一:人们可能会启用错误的规则,忘记排除生成的文件,或者过于宽泛地抑制诊断信息。掌握这项技能就能确保配置从一开始就是正确的。

dart-tooling

这项技能教会人们如何解决`pubspec.yaml`中包版本冲突的问题,如何正确使用依赖项覆盖规则,了解直接依赖与传递依赖之间的区别,以及如何通过阅读`pubspec.lock`文件来诊断版本匹配问题。

在包依赖管理方面,很多人常常会错误地判断包的版本,或者以掩盖实际冲突的方式设置`dependency_overrides`。掌握这项技能就能避免这些错误行为。

dart-use-pattern-matching

这项技能是Dart开发中极具价值的能力之一,因为Dart 3引入的密封类和模式匹配机制代表了一种全新的编程范式,而在Dart 3发布之前接受培训的开发者往往无法熟练运用这些技术。这项技能具体包括以下内容:

  • 如何对密封类使用穷举式的switch表达式

  • 如何在switch语句中运用解构模式

  • 如何使用`when`关键字编写保护性代码

  • 什么是记录模式

  • 列表和映射模式的应用方法

  • 如何在模式中正确使用通配符`_

// 使用dart-use-pattern-matching后代码示例

// 之前:传统的基于枚举的switch语句
switch (state) {
  case AppStateLoading:
    return CircularProgressIndicator();
  caseAppStateLoaded:
    return ContentWidget(data: data);
  default:
    return ErrorWidget();
}

// 现在:使用模式匹配的switch表达式(符合Dart 3的风格)
return switch (state) {
  AppStateLoading() => const CircularProgress(),
  AppStateLoaded(:final data) => ContentWidget(data: data),
 AppStateError(:final message) => ErrorWidget(message: message),
};

其中`AppStateLoaded(:final data)`这种解构模式是典型的Dart 3风格,代码结构也非常简洁。但那些没有掌握这项技能的开发者很少会使用它,因为旧版本的培训资料中并没有涉及这一内容。

dart-collect-coverage

这项技能教会人们如何收集测试覆盖率数据、生成LCOV报告和HTML报告,以及如何从覆盖率报告中过滤掉那些无法被有效测试的生成代码文件(如`*.g.dart`和`*.freezed.dart`),从而确保统计结果能够真实反映程序的实际覆盖情况。

dart-generate-test-mocks

这项技能介绍了如何使用`mockito`和`build_runner`工具,从接口和抽象类生成类型安全的测试模拟对象。它包括添加相应的注解、运行`dart run buildrunner build`命令,以及如何在测试中利用这些生成的模拟对象。

dart-fix-runtime-errors

这是一项程序性技能:它教会开发人员如何使用LSP(语言服务器协议)来获取当前的堆栈跟踪信息,定位出出现故障的代码行,应用相应的修复措施,并通过热重载来验证问题是否已经得到解决。对于在运行中的Flutter应用程序来说,这种处理方式才是纠正运行时错误的正确途径,而不是盲目猜测问题的原因。

dart-genkit

这项技能教授人们如何使用Genkit Dart SDK来构建基于人工智能的工作流程和开发工具。对于那些需要为Flutter应用程序添加人工智能功能的开发者来说,这一技能尤其具有实用性,它涵盖了流程定义、工具调用、模型选择以及数据流处理等相关内容。

dart-migrate-to-checks-package

这项技能指导人们如何将旧的package:matcher断言风格迁移到新的package:checks风格。新风格能够生成更清晰的错误信息,同时也更具可组合性。

// 旧风格 (package:matcher)
expect(result, isA>());
expect(result.getOrElse(() => null?).name, equals('Ade');

// 新风格 (package:checks)
check(result).isA>();
check(resultgetOrElse(() => null?).name).equals('Ade');

dart-memory

这项技能教授人们如何防止Flutter和Dart应用程序中出现内存泄漏现象,以及如何减轻垃圾收集带来的压力。它涵盖了StreamController的释放机制、AnimationController的处理方法、那些会阻碍垃圾收集发生的代码结构,同时还介绍了如何利用DevTools来识别内存相关的问题。

dart-build-cli-app

对于那些同时需要编写Dart命令行工具、后端脚本或进行部署自动化操作的Flutter开发者来说,这项技能会讲解入口文件的结构设计、如何使用package:args来解析命令参数、退出码的处理方法、子进程的管理技巧,以及跨平台脚本的开发规范。

dart-logic-patterns

这项技能涵盖了算法、数据结构,以及专门适用于Dart语言的业务逻辑组织模式:包括如何正确使用Iterable类中的各种方法,在不同场景下选择合适的ListSetMap数据结构,实现高效的搜索和排序操作,以及如何高效地利用Dart提供的集合字面量。

Flutter文件组织技能:全面讲解

文件组织技能是所有Flutter开发者都应该掌握的一项基础能力,同时也是了解技能结构应该如何构建的绝佳范例。仔细学习这一技能,就能领悟到任何高效技能背后所蕴含的原则。

---
name: flutter-file-organization
description: 在维护EstadofulWidget与State之间的关联关系的同时,对Flutter/Dart文件进行合理的组织和拆分。适用于创建、重构、拆分或重新整理Dart文件及类的场景。
---

# Flutter文件组织规则

在创建、拆分、重构或重新整理Flutter/Dart文件时,请遵循以下规则。

## 核心规则

1. 在修改文件之前,先仔细检查其内容。
2> 明确所有类、枚举类型、扩展模块、混合组件、类型定义以及顶层声明的内容。
3> 在进行拆分操作之前,先确定这些声明之间的依赖关系。
4> 将每一个独立的主体类保存在单独的文件中。
5> 将紧密关联的声明视为一个整体,将它们放在一起处理。
6> 绝不要将`EstadofulWidget`与其对应的`State`类分开。
7> 在移动声明的位置后,及时更新所有的导入语句和引用关系。
8> 避免创建不必要的私有辅助类或方法。
9> 确保文件组织方式不会改变应用程序的功能。
10> 对修改后的Dart文件运行`dart format`命令进行格式化处理。
11> 运行项目的分析工具并执行相关的测试用例。

规则1(“在修改文件之前先检查其内容”)能够有效避免一种最为常见且代价高昂的错误:在不阅读文件内容的情况下就对其结构进行假设。

如果代理程序跳过了这一检查步骤,就可能会重复声明某些元素、破坏代码之间的依赖关系,或者引发与现有代码的命名冲突。将这一检查列为首要规则,可以确保代理程序始终能够基于当前文件的完整结构来执行后续操作。

规则2和规则3(“识别所有类”以及“确定各类之间的关系”)属于必须执行的预处理步骤。在代理程序开始处理文件的任何内容之前,它必须先弄清楚文件中存在哪些元素,以及这些元素之间是如何相互关联的。

这其实就相当于在代码重构过程中遵循“三思而后行”的原则,这样就能有效避免那些会破坏现有功能的重构操作。

规则6(“永远不要将 StatefulWidget类与其对应的State类分开”)体现了Flutter特有的编译规则。对于那些对Dart语言非常熟悉但并不了解Flutter框架的开发者来说,他们可能会误以为可以将某个文件中的所有类分别提取到不同的文件中,但实际上这样做会导致编译错误——因为 `_ProfilePageState` 类是通过 `widget` 属性引用 `ProfilePage` 组件的,而 `widget` 的类型是由 `State` 这个类型定义的,这两个类共同构成了一个不可分割的编译单元。因此,这一规则能够有效避免那些仅凭一般Dart知识就无法发现的编译错误。

规则10和规则11(“运行dart format命令”以及“执行项目的分析工具”)则确保了任务能够顺利完成。如果没有这些规则,代理程序在生成文件后会直接报告任务完成,但这样就会留下格式不一致的问题或分析工具提示的错误,需要用户后来再去发现并处理;而有了这些规则,代理程序会在报告完成之前先运行这两个工具,从而及时发现问题并进行修复。

## 组件提取规则

不要通过编写私有的 `_build...()` 方法来构建复杂的UI组件。

例如,不要这样做:

```
Widget _buildUserCard() {
  return Container(
    ...
  );
}
```

正确的做法应该是将这样的代码提取到一个公开的类中,并将其放在 `widgets` 或 `components` 文件夹中。

“组件提取规则”明确指出了应该避免的行为,详细解释了哪些做法是不被允许的,并通过具体的代码示例来消除任何可能产生的歧义,同时也告诉开发者应该如何正确地实现相同的功能。

在训练数据中,使用 `_build...()` 这种编写方式非常普遍(因为教程为了简化讲解常常会采用这种方式),因此如果只是简单地说“避免使用这种写法”,而没有给出具体的例子,就很难让开发者真正改掉这个习惯。而通过展示具体的错误代码示例,并将其与正确的实现方法进行对比,才能使这条规则变得清晰易懂。

## UI元素提取规则

不要将大量的UI元素放在同一个组件中。

在合适的情况下,应该将这些逻辑上相关的元素提取出来,制成可重用的组件。

例如:

- 头部导航栏
- 统计信息展示区
- 过滤栏
- 搜索框
- 列表显示
- 表格行
- 按钮
- 空状态界面
- 加载提示界面
- 表单元素
- 对话框内容

相比编写庞大的构建方法,使用小型、可重用的组件会更加高效。

“组件提取”部分中列出的示例都是基于实际经验总结得来的。这些就是在实际开发的Flutter应用程序中,会出现在屏幕部件内部的UI元素。阅读这份列表的智能体在检查代码时能够识别出这些模式,并知道应该如何提取它们。

如果没有这份列表,“提取逻辑组件”这个要求就过于模糊,无法确保智能体会做出正确的操作:智能体必须明确知道什么才算作“逻辑组件”。

## 代码注释

不要编写代码注释。

这条规则适用于所有地方和所有层次。

关于代码注释的规定之所以如此简洁,是因为它具有绝对性。“适用于所有地方和所有层次”这一表述也是经过深思熟虑后确定的。如果没有这种范围限定,智能体可能会误以为这条规则仅适用于当前文件的组织结构相关操作,从而在创建或修改其他文件时继续添加注释。明确的范围界定消除了歧义,使这条规则的意图在各种情况下都能被清晰理解。

编写自己的技能:完整指南

官方提供的技能模板是你的基础,但最有价值的技能往往是你自己编写的那些内容——它们体现了你自己在项目中总结出的具体模式、经验教训以及所遵循的标准。

编写技能所需的正确心态

为智能体编写技能与为人类编写文档是不同的。人类的文档依赖于共同的背景知识、隐含的理解能力以及提问的能力,而为智能体编写的技能必须表述得清晰明确、精确无误,并且不能假设读者已经掌握了技能文件中未包含的任何知识。

最优秀的技能来源于你对自己代码库的实际操作经验。记录下每次你手动修改由AI生成的代码的情况吧——每一次这样的修改其实都构成了一个技能规则。当你向新团队成员解释某种编程规范时,这种解释本身也属于技能内容;如果你在代码审查中连续三次遇到同样的错误,那么这个错误就需要被归纳为一种技能规则。

在编写任何规则之前,先问问自己:“如果一个不了解我的代码库的智能体看到这条规则,它知道该怎么做吗?”如果答案是否定的,那么这条规则就应该被纳入某个技能体系中。

描述:最重要的二十个字

描述字段起着至关重要的作用。请在完成技能内容的编写后再来填写描述部分,这样它才能准确反映该技能的实际覆盖范围。一个好的描述应该满足这样的标准:如果智能体只阅读描述内容,就能判断这个技能是否适用于某项任务。

# 不佳的描述:过于模糊,没有明确的触发条件
description: 如何在Flutter应用程序中处理状态管理。

# 更好的描述:具体明确,包含多个触发条件,范围界定清晰
description: 在Flutter应用程序中使用flutter_bloc来实现状态管理。
适用于以下场景:为屏幕添加状态管理功能、开发需要加载或显示错误状态的新增功能、从API获取数据、处理会改变用户界面状态的用户交互操作,或者实现BlocProvider、BlocBuilder、BlocListener或BlocConsumer等组件。
当你在某项任务中看到与“bloc”、“cubit”、“state”、“event”或“stream”相关的内容时,就可以参考这条描述。

第二种描述方式在几个方面更具优势。它列举了一些具体的触发场景(例如“创建那些会引发加载问题或出现错误的状态的新功能”),这些场景比笼统的“处理状态”更有可能与实际的任务描述相匹配。它还提到了相关包的API接口(如BlocProviderBlocBuilder),因为这些信息很可能会出现在任务描述中。此外,它还列出了一些具有指示意义的关键词汇(如bloccubitstateevent)。

改变智能体行为的规则编写技巧

并非所有的规则都同样重要。那些只是要求智能体继续执行它原本已经在做的操作的规则,其实并无实际意义;而那些能够改变智能体行为方式的规则才真正具有价值。要想编写出能够改变智能体行为的规则,首先需要通过观察来分析问题:智能体究竟产生了哪些错误结果,以及哪条规则本可以防止这些错误的发生?

## 有效的规则与无效的规则

**无效的规则**(智能体本来就在尝试遵守这些规则):
- 编写清晰、易读的代码。
- 遵循Flutter的最佳实践。
- 使用适当的状态管理机制。
- 保持代码库的可维护性。

**有效的规则**(这些规则能够改变智能体的具体行为):
- 将任何长度超过30行的组件构建代码提取出来,放入`widgets/`目录下的单独类中。
- 在拥有对应BlocBuilder的组件内部,绝对不要调用`setState`方法。
- 将Bloc事件的名字命名为过去式动词,例如`ProfileLoadRequested`而不是`LoadProfile`。
- 将所有的Bloc相关文件(包括`.bloc`、`.event`和`.state`文件)放在功能模块内的`bloc/`子目录中。
- 使用`sealed`关键字来定义状态类,例如`sealed class ProfileState {}`。
- 在每个组件的构造函数中都必须指定`super.key`,例如`const MyWidget({super.key})`。
- 在任何异步方法中调用`setState`之前,必须先检查组件是否已经成功挂载。

需要注意的是,有效的规则通常会包含具体的数字(如30行代码)、特定的文件夹名称(如`widgets/`、`bloc/`)、明确的命名规范以及具体的代码结构。而像“编写清晰易懂的代码”这样的笼统要求,智能体本来就会尝试去遵守;而像“将Bloc事件的名字命名为过去式动词,并给出具体示例”这样的具体规则,才能真正改变智能体的行为结果。

反例模式

对于那些针对训练数据中常见模式的规则来说,仅仅通过文字描述这些规则是远远不够的;将错误的操作方式与正确的操作方式进行对比展示,会显得更加有效。因为在训练过程中,智能体已经见过成千上万的错误操作案例,单纯的文字说明可能无法改变它的行为习惯;而视觉上的对比则能让人清楚地理解规则的意图。

## 错误状态的命名规则

不要仅在错误状态的名字后面加上“Error”这个词。

不要这样做:

```
final class ProfileError extends ProfileState {
  const ProfileError();
}

应该包含错误的上下文信息:

final class ProfileLoadFailure extends ProfileState {
  const ProfileLoadFailure({required this.message});
  final String message;
}

在错误信息中包含操作名称(如Load),能够使错误信息明确对应于具体哪项操作出现了问题。当一个Bloc负责处理多个可能独立出错的操作时,这一点尤为重要。ProfileLoadFailureProfileUpdateFailure这样的错误代码具有明确的含义,而ProfileErrorProfileError2则不能这样区分。

反例后面的解释部分(“在错误信息中包含操作名称……”)将规则与其背后的原因联系起来,这有助于代理在特殊情况下正确应用这些规则,而不仅仅是机械地遵守规则的文字表述。

每个团队都应掌握的Flutter必备技能

根据AI代理在处理Flutter代码时最常出现错误的环节,以下是每个Flutter开发团队都应该掌握的必备技能。这些技能都已完整列出,你可以根据自己的实际需求进行调整使用。

Bloc状态管理技能

---
name: flutter-bloc-state-management
description: 使用flutter_bloc来实现状态管理。适用于创建新功能、为界面添加状态信息、从API获取数据、处理会导致加载或错误状态的用户交互操作,以及使用BlocProvider、BlocBuilder、BlocListener、BlocConsumer等组件进行状态管理,或者执行任何涉及状态转换的任务。
---

# Flutter Bloc状态管理

这个项目完全采用flutter_bloc来进行状态管理。除非有明确指示,否则禁止使用setState、ChangeNotifier、Provider或Riverpod等组件。

## 文件结构

任何需要状态管理的功能模块,都会在bloc/子目录下包含三个Bloc文件:
lib/
  features/
    profile/
      bloc/
        profile_bloc.dart      <- Bloc类及相应的处理方法
        profile_event.dart     <- 所有事件都被定义为密封类
        profile_state.dart     所有状态也被定义为密封类
      screens/
        profile_screen.dart
      widgets/
        profile_card.dart
      profile.dart              用于导出相关代码

密封类

为了实现对各种情况的全面处理,Dart允许使用密封类来定义事件和状态:

// profile_event.dart
sealed class ProfileEvent {}

final class ProfileLoadRequested extends ProfileEvent {
  const ProfileLoadRequested({required this.userId});
  final String userId;
}

final class ProfileUsernameUpdated extends ProfileEvent {
  const ProfileUsernameUpdated({required this.newUsername});
  final String newUsername;
}
// profile_state.dart
sealed class ProfileState {}

final class ProfileInitial extends ProfileState {}

final class ProfileLoading extends ProfileState {}

final class ProfileLoaded extends ProfileState {
  const ProfileLoaded({required this.profile});
  final UserProfile profile;
}

final class ProfileLoadFailure extends ProfileState {
  const ProfileLoadFailure({required this.message});
  final String message;
}
sealed class能够确保状态层次结构是完备的:Dart编译器可以验证switch语句中确实涵盖了所有可能的状态。而对于具体实现来说,final class能够防止出现意外的子类化行为。所有的状态和事件都是finalsealed的。

命名规范

Bloc类应该使用功能名称后加上Bloc作为后缀,例如ProfileBlocAuthBlocCartBloc。事件的名字则应采用过去分词形式的动词短语加上功能名称和Event后缀,比如ProfileLoadRequestedAuthLoginAttempted。状态的名字则应由功能名称再加上描述性的名词或形容词构成,例如ProfileInitialProfileLoadingProfileLoadedProfileLoadFailure

不要将事件命名为命令式名称(例如不要用LoadProfile,而应该使用ProfileLoadRequested)。错误状态也不应简单地称为ProfileError,而应该包含具体的操作信息,比如ProfileLoadFailureProfileUpdateFailure

Block类

// profileBloc.dart
class ProfileBloc extends Bloc {
  final ProfileRepository _repository;

  ProfileBloc({required ProfileRepository repository})
      : _repository = repository,
        super(ProfileInitial()) {
    on(_onProfileLoadRequested);
    on(_onProfileUsernameUpdated);
  }

  Future _onProfileLoadRequested(
    ProfileLoadRequested event,
    Emitter emit,
  ) async {
    emit(ProfileLoading());

    final result = await _repository.getProfile(event.userId);

    result.fold(
      (failure) => emit PROFILE_LOAD_FAILURE(message: _mapFailure(failure)),
      (profile) => emit PROFILELoaded(profile: profile),
    );
  }

  String _mapFailure(AppFailure failure) => switch (failure) {
    NetworkFailure(:final message) => message,
    ServerFailure(:final message) => message,
    NotFoundFailure() => '未找到该资料',
    UnauthorizedFailure() => '请重新登录',
    _ => '发生了意外错误',
  };
}

每个事件处理方法都是一个以_on加上事件类名命名的私有方法。这种命名规则在所有的Bloc类中都是一致的。在异步操作开始之前,所有处理方法都会先发出“加载中”的状态信号;操作完成后,则会根据结果发出成功或失败的状态信号。没有任何处理方法会直接返回数据,所有的信息传递都是通过这些状态信号来完成的。

组件集成

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key, required this.userId});
  final String userId;

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => ProfileBloc(
        repository: context.read(),
      )..add(ProfileLoadRequested(userId: userId)),
      child: BlocConsumer(
        listener: (context, state) {
          if (state is ProfileLoadFailure) {
            ScaffoldMessenger.of(context).showSnackBar(
              SnackBar(content: Text(state.message)),
            );
          }
        },
        builder: (context, state) => switch (state) {
          ProfileInitial() => const SizedBox.shrink(),
          ProfileLoading() => const Center(child: CircularProgressIndicator()),
          ProfileLoaded(:final profile) => ProfileContent(profile: profile),
          ProfileLoadFailure(:final message) => ProfileErrorView(message: message),
        },
      ),
    );
  }
}

BlocConsumer结合了监听器(副作用)和构建器(用户界面)。对于那些被定义为“密封状态”的情况,switch表达式的覆盖范围是完备的:编译器会确保每种状态都对应着相应的用户界面。

禁止使用的模式

不要在任何拥有相应Bloc的小部件中使用setState方法。在initState方法内部,不要直接调用context.read().add(event),而应该先使用addPostFrameCallback来延迟执行该操作。在进行await操作后,在没有检查mounted属性的情况下,也不要直接访问BuildContext。另外,不要在StatelessWidget.build方法内部创建Bloc对象(因为每次重新构建组件时,这个Bloc都会被重新创建)。

特性架构技能

---
名称:flutter-feature-architecture
描述:使用包含存储层、服务层和展示层的清晰架构来组织Flutter应用程序中的各个特性。适用于创建新特性、添加屏幕界面、实现数据获取功能、整理现有代码,或者确定新文件应存放在哪个目录中等涉及文件夹结构、层次划分或项目目录管理的任务。
---

# Flutter特性架构

这个项目采用了以特性为优先级的文件夹结构,并遵循清晰的架构设计原则。

顶层结构

lib/
  core/
    constants/     <- 全局常量,与特定特性无关
    errors/         <- AppFailure密封类层次结构
    extensions/     
    theme/          主题相关扩展、颜色代码及排版设置
    utils/          纯工具函数
  features/
    auth/
    profile/
    home/
    settings/
  shared/
    widgets/        在3个以上特性中共同使用的组件
    models/         多个特性之间共享的模型
    services/       被多个特性所使用的服务
  app.dart          MainApplication设置文件
  main.dart         程序入口点文件

特性文件夹结构

每个特性都遵循这样的内部结构:

features/
  profile/
    bloc/
      profileBloc.dart
      profile_event.dart
      profile_state.dart
    data/
      profile_repository.dart          接口层
      profile_repository_impl.dart     实现层
      profile_remote_data_source.dart
      profile_local_data_source.dart
    domain/
      profile_model.dart                冻结后的领域模型
    screens/
      profile_screen.dart
      edit_profile.screen.dart
    widgets/
      profile_card.dart
      profile_header.dart
      profile_stats_row.dart
    profile.dart                         最终输出的文件

层次依赖规则

<展示层(包括屏幕界面及各种组件)仅依赖于“Bloc”模型与相应的领域模型。而“Bloc”模型本身仅依赖于仓库接口,与其实现细节无关;仓库的实现方式则取决于所使用的数据源,而这些数据源往往需要借助外部库来实现,例如Firebase、HTTP或SharedPreferences等。>

切勿以错误的方向在不同的层之间导入数据。数据层永远不会从展示层导入数据;而领域层则不会从项目中的任何其他部分导入数据。

桶文件导出规则

每个功能模块都会对应一个桶文件,该文件仅用于导出该功能模块的公共API接口:

// features/profile/profile.dart
export 'domain/profile_model.dart';
export 'screens/profile_screen.dart';
export 'screens/edit_profile_screen.dart';
export 'bloc/profile_bloc.dart';
export 'bloc/profile_event.dart';
export 'bloc/profile_state.dart';

内部实现文件(如数据源代码、仓库实现逻辑等)不会被导出。使用这些文件的代码应该直接导入package:myapp/features/profile/profile.dart,而绝不应该使用嵌套的路径来导入。

核心文件夹规则

只有当一个文件被三个或更多的功能模块所使用时,它才应该被放在core/文件夹中;如果只有一个或两个功能模块使用了该文件,那么它就应该被放在这些功能模块对应的文件夹里。切勿仅仅根据文件未来可能被使用的场景就提前将其移放到core/文件夹中。

错误处理技巧

---
name: flutter-error-handling
description: 使用类型化的AppFailure类以及Either返回类型来实现错误处理。
适用于处理API调用、仓库方法产生的错误、Bloc模块中的错误状态,以及在数据源中捕获异常、显示错误提示界面、实现try-catch语句等涉及错误处理的各种场景。
---

# Flutter错误处理

该项目采用了一种类型化的错误处理机制。原始异常不会跨越不同的层进行传播。

AppFailure错误层次结构

// core/errors/app_failure.dart
sealed class AppFailure {
  const AppFailure();
}

final class NetworkFailure extends AppFailure {
  const NetworkFailure({required this.message});
  final String message;
}

final class ServerFailure extends AppFailure {
  const ServerFailure({required this.statusCode, required this.message});
  final int statusCode;
  final String message;
}

final class CacheFailure extends AppFailure {
  const CacheFailure({required this.message});
  final String message;
}

final class NotFoundFailure extends AppFailure {
  constNotFoundFailure();
}

final class UnauthorizedFailure extends AppFailure {
  const UnauthorizedFailure();
}

final class ValidationFailure extends AppFailure {
  const ValidationFailure({required this.field, required this.message});
  final String field;
  final String message;
}

sealed class AppFailure这一设计确保了错误处理层次的完整性。新的错误类型总是以final class的形式作为子类被添加进来;编译器会强制要求,针对AppFailure类型的switch语句必须能够覆盖所有可能的子类型。

仓库方法的返回类型

`fpdart`包中的仓库方法会返回`Either`类型的结果:
abstract class ProfileRepository {
  Future>> getProfile(String userId);
  Future>> updateUsername(String userId, String username);
}
返回`Either`类型可以明确表示可能发生失败的情况,而且这种失败会在类型层面就被体现出来。使用该仓库功能的代码开发者无法忽视这种失败的可能性,因为返回类型强制他们必须处理这两种情况。

数据源异常处理

数据源是唯一会使用`try-catch`结构的层。它们会捕获原始异常,并将其转换为`AppFailure`对象:
class ProfileRemoteDataSource {
  Future> getProfile(String userId) async {
    try {
      final doc = await _firestore.collection('users').doc(userId).get();

      if (!doc.exists) return left(const NotFoundFailure());

      return right(UserProfileDto.fromJson(doc.data()!));
    } on FirebaseException catch (e) {
      return switch (e.code) {
        'permission-denied' => left(const UnauthorizedFailure()),
        'unavailable' => left(NetworkFailure(message: e.message ?? '网络错误')),
        _ => left(ServerFailure(statusCode: 0, message: e.message ?? '服务器错误')),
      };
    } catch (e) {
      return left(NetworkFailure(message: e.toString()));
    }
  }
}

禁止使用的模式

不要在Bloc、仓库代码或展示层代码中使用`try-catch`结构。也不要从仓库方法中抛出异常。在状态类中,不要使用`String`类型作为错误信息;应该使用有类型的错误对象。不要将原始的异常信息直接传递给用户界面,在Bloc中应将异常情况转换为更易于用户理解的提示信息。

主题设计技巧

---
name: flutter-theming
description: 使用项目中的主题扩展系统来设置颜色、字体样式、间距以及视觉效果。在任何涉及颜色、文本样式、内边距、外边距、边框半径、阴影或UI组件外观的代码中,都应使用这一系统。当遇到与样式设计、颜色选择、字体格式、间距调整或视觉布局相关的问题时,也请使用这一方法。
---

# Flutter主题设计

该项目完全依赖主题扩展系统来进行所有视觉效果的设置。在代码库的任何地方都不允许使用硬编码的视觉参数值。

颜色访问方式

// 不要这样做 color: const Color(0xFF6750A4) color: Colors.deepPurple backgroundColor: Theme.of(context).colorScheme.primary // 应该这样做 color: context.appColors(primary) backgroundColor: context.appColors.surface
`context.appColors`是`BuildContext`类型的一个扩展,其定义在`core/theme/app_colors_extension.dart`文件中。这个扩展允许以具有明确含义的名称来访问完整的颜色调色板。

可用的颜色:请使用通过 context.appColors 提供的语义颜色。

对于品牌标识与界面元素,主品牌颜色可使用 context.appColors.primary,次要点缀色可使用 context.appColors.secondary,卡片和容器的背景色可使用 context.appColors.surface,屏幕背景色则使用 context.appColors.background

对于不同状态显示,错误状态可使用 context.appColors.error,成功状态则使用 context.appColors.success

对于文本显示,主要可读文本可使用 context.appColors.textPrimary,标题、标签及次要信息可使用 context.appColors.textSecondary,而禁用的控件或文字则使用 context.appColors.textDisabled

间距设置

// 不要这样做:
padding: const EdgeInsets.all(16);
margin: const EdgeInsets_symmetric(horizontal: 24, vertical: 8);

// 而应该这样做:
padding: const EdgeInsets.all(AppSpacing.md);
margin: const EdgeInsets_symmetric(
  horizontal: AppSpacing.lg,
  vertical: AppSpacing.sm,
);

AppSpacingcoreconstants/app-spacing.dart 中定义,提供了以下间距值:

xs: 4 · sm: 8 · md: 16 · lg: 24 · xl: 32 · xxl: 48

排版样式

// 不要这样做:
style: constTextStyle(fontSize: 16, fontWeight: FontWeight.w600);

// 而应该这样做:
style: context.appTypography.bodyMedium;
style: context.app Typography.headlineLarge.copyWith(
  color: context.appColors.textPrimary,
);

context.appTypography 是对 BuildContext 的扩展,提供了完整的类型样式体系。

边框圆角设置

// 不要这样做:
borderRadius: BorderRadius.circular(8);

// 而应该这样做:
borderRadius: BorderRadiusCircular(AppRadius.sm);

AppRadius 中定义的常量包括:xs (4)、sm (8)、md (12)、lg (16)、xl (24) 以及 round (999)。

导航功能

---
name: flutter-navigation
description: 使用 GoRouter 实现导航功能。适用于添加路由、在屏幕间切换、处理深度链接、设置路由守卫或重定向规则、处理需要身份验证的路由,以及处理嵌套导航或壳层导航等涉及导航的各种场景。
---

# Flutter Navigation

该项目使用 GoRouter 进行所有导航操作。请不要使用 Navigator.push、Navigator.pushNamed、Navigator.pop(这些方法必须通过 GoRouter 来调用),也不得使用任何绕过 GoRouter 的 Navigator API。

路由常量

所有路由路径在core/router/routes.dart中都是常量:

abstract class Routes {
  static const splash = '';
  static const login = '/auth/login';
  static const register = '/auth/register';
  static const home = '/home';
  static const profile = '/home/profile/:userId';
  static const editProfile = '/home/profile/:userId/edit';
  static const settings = '/settings';
}

进行导航时切勿使用字符串字面量。应始终使用Routes.home,而不是'/home'

导航方法

// 替换当前位置(无法通过返回按钮回到上一个页面)
context.go(Routes.home);

// 将当前页面推到顶部栈帧(返回按钮会回到上一个页面)
context.push(Routes.profile.replaceAll(':userId', userId));

// 从栈顶弹出当前页面
context.pop();

// 带结果从栈顶弹出当前页面
context.pop(result);

切勿使用Navigator.of(context).push(...)。这种写法会绕过GoRouter,从而导致深度链接失效。

路由器定义

所有路由都在core/router/app.router.dart中定义:

final router = GoRouter(
  initialLocation: Routes.splash,
  redirect: _redirectLogic,
  routes: [
    GoRoute(
      path: Routes.home,
      pageBuilder: (context, state) => NoTransitionPage(
        child: const HomeScreen(),
      ),
    ),
    GoRoute(
      path: Routes.profile,
      builder: (context, state) {
        final userId = state.pathParameters['userId']!;
        return ProfileScreen(userId: userId);
      },
    ),
  ],
);

类型化参数

路径参数可以从state.pathParameters中获取,查询参数则可以从state.uri.queryParameters中获取。切勿手动解析路径字符串。

每位开发者都应该掌握的必备Dart技能

除了与Flutter相关的特定技能外,纯Dart开发在很大程度上也依赖于团队层面的通用技能。这些技能适用于任何Dart代码,无论是业务逻辑处理、数据操作、测试工作,还是命令行工具的开发。

Dart模型与类型化定义

---
name: dart-models-freezed
description: 使用freezed包结合json_serializable创建不可变的数据模型。在创建新的数据模型、数据传输对象、请求或响应对象、值对象,或是任何用于表示结构化数据的Dart类时,都可以使用这种方法。它特别适用于处理JSON解析、API响应映射以及定义数据结构等场景。
---

# 使用freezed构建Dart模型

所有数据模型都会利用freezed包来实现不可变性并自动生成代码。

模型定义

import 'package:freezed.annotation/freezed_annotation.dart';

part 'user_profile.freezed.dart';
part 'user_profile.g.dart';

@freezed
class UserProfile with _$UserProfile {
  const factoryUserProfile({
    required String id,
    required String name,
    required String email,
    String? avatarUrl,
    @Default(false) bool isVerified,
    required DateTime createdAt,
  }) = _UserProfile;

  factory UserProfile.fromJson(Map<String, dynamic> json) =>
      _$UserProfileFromJson(json);
}

@freezed 会触发代码生成过程,从而生成一个具有命名构造函数的不可变类、用于创建修改后副本的 copyWith 方法、基于所有字段实现的 ==hashCode 方法、便于调试用的 toString 方法,以及通过 json_serializable 实现的 fromJson/toJson 方法。

part 指令是必需的,且其名称必须与文件名相匹配。例如,user_profile.dart 会生成 user_profile.freezed.dartuser_profile.g.dart 这两个文件。

字段规则

对于那些必须始终存在的字段,应使用 required 标注;对于可选字段,则使用 String?(可为空);对于那些具有合理默认值的字段,可以使用 @Default(value) 来避免出现空值情况;而当 JSON 字段的名称与 Dart 字段的名称不同时,应使用 @JsonKey(name: 'field_name') 进行标注。

添加或修改模型后的操作

务必执行以下命令:

dart run build_runner build --delete-conflicting-outputs

切勿手动编辑 .freezed.dart.g.dart 文件,因为这些文件是自动生成的,在下次构建时会被重新生成。

数据传输对象与领域模型

数据传输对象(DTO)位于 data/ 目录下,它们直接对应于 API 的结构;而领域模型则位于 domain/ 目录下,用于表示应用程序内部的 数据模型。

DTO 可能会包含像 created_at 这样的字段(其命名遵循 API 的蛇形命名规则),而领域模型则会使用驼峰式命名法来表示相同的字段。系统会将 DTO 映射到相应的领域模型中。

Dart 的模式匹配技巧

---
名称:Dart 模式匹配技巧
描述:使用 Dart 3 的模式匹配、switch 表达式以及密封类层次结构,来实现对控制流程的精确控制。这种技术适用于处理密封类、枚举类型、区分性联合体、基于类型的条件逻辑,或者任何可以用 switch 表达式替代的逻辑结构。在重构 if-else 语句链、处理多种子类型,或实现根据类型进行分支的业务逻辑时,这种技巧也非常有用。
---

# Dart 模式匹配

对于所有涉及类型判断、密封类层次结构或数据结构分解的控制流程,都应使用 Dart 3 的模式匹配功能。

使用 switch 表达式而非传统的 switch 语句

// 不要这样做(传统的 switch 语句属于命令式编程风格)
switch (state) {
  case ProfileLoading():
    return const CircularProgressIndicator();
  case ProfileLoaded():
    return ProfileContent(profile: state.profile);
  case ProfileLoadFailure():
    return ErrorView(message: state.message);
  default:
    return const SizedBox.shrink();
}

// 应该这样做(switch 表达式属于函数式编程风格,适用于构建方法中)
return switch (state) {
  ProfileInitial() => const SizedBox.shrink(),
  ProfileLoading() => const CircularProgressIndicator(),
  ProfileLoaded(:final profile) => ProfileContent(profile: profile),
  ProfileLoadFailure(:final message) => ErrorView(message: message),
};

switch表达式是值,而不是语句。它们既可以作为return的参数使用,也可以作为变量的值来使用。由于密封类层次结构的存在,这种用法具有完备性:当你添加一个新的状态时,编译器会自动提示所有需要处理这个新状态的switch表达式。

模式中的解构操作

// 在模式中直接访问字段
case ProfileLoaded(:final profile) => ProfileContent(profile: profile),
// 等价于:
case ProfileLoaded() => ProfileContent(profile: state.profile),

在模式中使用:final field语法,可以直接将字段的值绑定到相应的case分支中。这样就不需要单独访问state.profile了,从而使代码更加简洁。

保护条件语句

return switch (state) {
  ProfileLoaded(:final profile) when profile.isVerified => VerifiedProfileView(profile: profile),
  ProfileLoaded(:final profile) => UnverifiedProfileView(profile: profile),
  _ => const LoadingView(),
};

when为模式添加了保护条件语句。只有当模式匹配的同时保护条件也为真时,该case分支才会被执行。这种机制允许在同一个类型内部实现更细粒度的分支逻辑。

记录类型的模式匹配

// 对记录类型进行匹配
final (name, age) = getUserInfo();

// 在switch表达式中使用
final description = switch ((user.name, user.isAdmin)) {
  (final name, true) => '$name (Admin)',
  (final name, false) => name,
};

记录类型实际上是结构化的元组。对记录类型进行模式匹配时,可以直接提取其中的各个组成部分,而无需使用专门的访问器。

将if-else链转换为switch表达式

当看到一个根据类型或值来分支的if-else链时,应该将其转换为switch表达式:

// 不要这样做
String label;
if (priority == Priority.high) {
  label = 'Urgent';
} else if (priority == Priority.medium) {
  label = 'Normal';
} else {
  label = 'Low';
}

// 应该这样做
final label = switch (priority) {
  Priority_high => 'Urgent',
  Priority.medium => 'Normal',
  Priority.low => 'Low',
};

Dart测试规范技巧

---
name: dart-testing-conventions
description: 遵循package:test提供的规范来编写Dart单元测试,包括使用mocktail进行模拟测试、为测试用例和测试组指定描述性名称,以及采用正确的异步测试模式。无论是在编写新的测试文件、为现有文件添加测试用例、对依赖项进行模拟测试、测试异步函数,还是验证错误处理行为时,都应遵循这些规范。
---

# Dart测试规范

测试文件的结构

import 'packageflutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:myapp/features/profile/data/profile_repository_impl.dart';
import 'package:myapp/core/errors/app_failure.dart';

class MockProfileRemoteDataSource extends Mock
    implements ProfileRemoteDataSource {}

class MockProfileLocalDataSource extends Mock
    implements ProfileLocalDataSource {}

void main() {
  late MockProfileRemoteDataSource mockRemote;
  late MockProfileLocalDataSource mockLocal;
  late ProfileRepositoryImpl repository;

  setUp(() {
    mockRemote = MockProfileRemoteDataSource();
    mockLocal = MockProfileLocalDataSource();
    repository = ProfileRepositoryImpl(
      remote: mockRemote,
      local: mockLocal,
    );
  });

  group('ProfileRepositoryImpl', () {
    group('getProfile', () {
      test(
        '当远程数据源正常工作时,该方法应返回正确的结果',
        () async {
          when(() => mockRemote.getProfile(any()))
              .thenAnswer((_) async => right(fakeProfileDto));

          final result = await repository.getProfile('user123');

          expect(result.isRight(), isTrue);
          expect(result.getOrElse(() => null)?.id, equals('user123'));
        },
      );

      test(
        '当远程数据源出现网络错误时,该方法应返回错误信息',
        () async {
          when(() => mockRemote.getProfile(any()))
              .thenAnswer((_) async => left(NetworkFailure(message: '没有互联网连接');

          final result = await repository.getProfile('user123');

          expect(result.isLeft(), isTrue);
          expect(result.fold((f) => f, (_) => null), isA());
        },
      );
    });
  });
}

测试命名规则

请使用描述性强的测试名称,这些名称应遵循“在Y条件下执行X操作”或“在Y条件下返回X结果”的格式。

示例: 当远程数据源正常工作时,该方法会返回Right(profile); 当连接失败时,该方法会返回Left(NetworkFailure); 如果远程数据源无法使用,则该方法会调用本地数据源。

请避免在测试名称中使用“test”或“should”这样的词汇。例如,应使用当仓库调用成功时,该方法会返回profile,而不是验证该方法是否能正确返回profile该方法在调用时应该返回profile

模拟对象的设置

请在setUp方法中创建新的模拟对象,而不要在main方法的顶层进行操作。这样就能确保一个测试中的状态不会影响到另一个测试。

对于那些被传递给any()方法的自定义类型,請在setUpAll方法中使用registerFallbackValue方法:

 setUpAll(() {
  registerFallbackValue(const ProfileLoadRequested(userId: ''));
  registerFallbackValue(left < AppFailure, UserProfile > (const NotFoundFailure()));
});

异步测试

对于使用Future返回结果的情况,请始终使用await;而对于通过Stream获取结果的情况,请结合expectLateremitsInOrder进行测试。

请不要在测试中使用await Future.delayed(...)这种写法。对于小部件的测试,可以使用pump()方法;而对于需要模拟异步行为的场景,则可以使用thenAnswer方法。

针对大型代码库和架构设计的技能

随着Flutter项目的规模不断扩大,相关的架构决策也会变得越来越复杂。这些技能专为处理大型代码库而设计,在这类环境中,保持架构的一致性尤为重要。

性能优化技巧

---
name: flutter-performance
description: 遵循Flutter的性能最佳实践,包括使用const小部件、有选择地重新构建组件、实现延迟加载机制,以及正确使用键值对。这些技巧适用于优化屏幕显示效果、处理列表数据、添加动画效果、处理图像相关操作,或任何与渲染性能、帧率或内存使用量相关的场景。
---

# Flutter性能优化

常量小部件的使用

凡是可以被声明为常量的小部件,都必须被定义为常量;凡是具有常量构造函数的类,也必须提供相应的常量构造函数:

// 不正确的写法
class UserAvatar extends StatelessWidget {
  UserAvatar({super.key, required this.url}); // 缺少const关键字
  final String url;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(  // 应该使用const关键字
      backgroundImage: NetworkImage(url),
    );
  }
}

// 正确的写法
class UserAvatar extends StatelessWidget {
  const UserAvatar({super.key, required this.url});
  final String url;

  @override
  Widget build(BuildContext context) {
    return CircleAvatar(
      backgroundImage: NetworkImage(url),
    );
  }
}

列表性能

对于项目数量未知或较多的列表,应使用ListView.builder;而对于可能超过20个项目的列表,绝对不要使用带有children属性的ListView

// 对于长度可变的列表,请勿采用这种方式
ListView(
  children: items.map((item) => ItemCard(item: item)).ToList(),
)

// 正确的做法是这样写
ListView.builder(
  itemCount: items.length,
  itemBuilder: (context, index) => ItemCard(item: items[index]),
)

使用BlocSelector进行选择性重建

当部件树中只有部分内容依赖于状态的某些变化时,可以使用 BlocSelector仅重新构建那些受影响的部件:

// 这种做法是错误的(任何状态变化都会导致整个子树被重新构建)
BlocBuilder〈CartBloc, CartState〉(
  builder: (context, state) => CartBadge(count: state is CartLoaded ? state.itemCount : 0),
)

// 正确的做法是这样写(只有当项目数量发生变化时才会重新构建)
BlocSelector〈CartBloc, CartState, int〉(
  selector: (state) => state is CartLoaded ? state.itemCount : 0,
  builder: (context, count) => CartBadge(count: count),
)

图像优化

对于从网络获取的图像,应使用cached_network_image;切勿直接使用Image.network。可以通过设置cacheWidthcacheHeight在解码时调整列表项中图像的大小。在Android平台上使用WebP格式,在iOS平台上使用HEIC/WebP格式,这样文件大小会显著减小。

无障碍功能开发技巧

---
name: flutter-accessibility
description: 实现包括语义标签、焦点管理、对比度要求以及屏幕阅读器支持在内的无障碍功能。在创建交互式部件、图像、图标、表单字段或任何需要让残障人士能够使用的元素时,都应运用这些技术。在处理Semantics、ExcludeSemantics、Focus或FocusNode相关逻辑时,也需要参考这些内容。
---

# Flutter无障碍功能开发

// 这种做法是错误的 IconButton( onPressed: _onShare, icon: const Icon(Icons.share), ) // 正确的做法是这样写 IconButton( onPressed: _onShare, icon: const Icon'iconshare'), tooltip: '分享帖子', // 在移动设备上,这个工具提示文字起到了语义标签的作用 )

// 装饰性图标(没有语义意义) Icon( Icons.star, semanticLabel: '', // 设置为空标签即可将其标记为装饰性图标 ) // 提供信息的图标(具有语义意义) Icon( Icons.warning, semanticLabel: '警告:此操作不可撤销', )

表单的可访问性

所有表单字段都必须配有标签,以便屏幕阅读器能够读取这些标签内容。切勿仅依赖占位文本来标识字段:

TextFormField(
  decoration: const InputDecoration(
    labelText: '电子邮件地址',    // 屏幕阅读器会读取这一标签
    hintText: 'name@example.com', // 仅在字段为空时显示
  ),
)

最小触摸目标尺寸

所有交互式元素的尺寸必须至少为48x48 dp。如果其可视尺寸较小,可以使用SizedBoxPadding来扩大可点击区域:

SizedBox(
  width: 48,
  height: 48,
  child: IconButton(
    iconSize: 20,
    onPressed: _onClose,
    icon: const Icon'icon.close),
  ),
)

高级技能模式

## 必须执行的验证步骤 在任何代码生成或修改操作之后,务必执行以下步骤: 1. 运行`dart format .`来格式化所有Dart文件 2. 运行`flutter analyze`来检查是否存在分析错误或警告 3. 运行`flutter test`来确认这些更改没有破坏任何测试用例 4. 如果上述步骤中的任何一项出现错误,请在将任务标记为完成之前先修复这些问题 如果这些命令中有任何一条失败,切勿将任务标记为完成。

这种模式将原本仅用于指导代码生成的技能,转化成了一个完整的质量保障流程。智能助手不仅会编写代码,还会在代码编写完成后根据你的质量标准对其进行验证,确认其符合要求后才视为任务完成。

基于上下文的条件规则

有些规则只适用于特定的情况。应使用条件语句来明确这些规则,这样智能助手才能正确地应用它们:

## 与上下文相关的规则

当某个组件发起网络请求时:
- 在请求执行期间,禁用所有交互式元素
- 根据当前的UI界面显示相应的加载指示器
- 使用用户能够理解的错误信息来处理错误
- 当请求完成时(无论结果是成功还是失败),重新启用所有交互式元素

当一个Bloc同时处理多个独立的操作时:
- 为每个操作创建单独的错误状态(而不是使用统一的Error状态)
- 每个错误状态的名称应与该操作相对应:例如ProfileLoadFailure、ProfileUpdateFailure

在创建用于ListView中的组件时:
- 必须为该组件提供一个键值对
- 在可能的情况下,使用const构造函数来定义组件
- 如果列表中的项目数量可能超过50个,可以考虑在列表层使用ListView.builder

跨技能关联

复杂的任务可能需要多种技能协同发挥作用。请在技能描述中明确列出相关的技能,这样智能体就能知道需要加载这些技能:

## 相关技能

当使用该技能的规则提取组件时,还需应用
flutter-file-organization技能来确定正确的文件位置。

如果提取出的组件需要状态管理,应使用
flutter-bloc-state-management技能来判断它是否需要独立的Bloc状态管理结构。

在为使用该技能编写的代码编写测试时,需遵循
dart-testing-conventions规范来命名和组织测试用例。

那些蕴含了实践经验教训的技能

一些最有价值的技能内容实际上来源于特定的生产场景中的经验。请将这些经验以技能规则的形式记录下来,并提供足够的背景信息,以便任何人(包括智能体)都能理解这些规则的存在原因:

从生产实践中总结出的构建上下文知识

在任何使用`await`语句的代码中,在使用`BuildContext`之前,务必先检查组件是否已经成功挂载:

Future _onSubmit() async {
  final result = await _repository.save(formData);

  // 错误做法:如果在等待操作期间组件被卸载,那么上下文信息可能会失效
  ScaffoldMessenger.of(context).showSnackBar(...);

  // 正确做法:先检查组件是否已挂载
  if (!mounted) return;
  ScaffoldMessenger.of(context).showSnackBar(...);
}

在开发环境中,这种错误通常不会被体现出来(因为异步操作完成时,组件往往已经成功挂载),但在生产环境中,由于网络延迟较大,或者用户在操作进行过程中离开了页面,就可能会出现“FlutterError (试图访问已卸载的组件的父组件)”这样的错误。

包级技能:向智能体介绍你的库

`skills`命令行工具(作为Dart包在pub.dev/packages/skills中提供)提供了一种非常便捷的方式:可以直接从项目的依赖包中安装所需的技能。

# 全局安装Dart skills命令行工具
dart pub global activate skills

# 安装项目中所有包含skills功能的包
skills get

当你将某个包添加到`pubspec.yaml`文件中,然后运行`skills get`命令时,该工具会自动搜索你的项目依赖树中的每个包,查找其中是否包含`skills/`目录,并将这些技能安装到项目中。这样一来,包的开发者就可以直接为使用这些技能的智能体用户提供使用说明。

为什么这很重要

在引入包级技能之前,如果向Flutter项目中添加一个新的包,智能体虽然知道这个包的存在(因为它的训练数据中包含了相关信息),但可能并不了解该包当前的API接口、推荐的使用方式或常见的使用误区。这就可能导致智能体错误地理解方法名称,使用已经被弃用的API,或者无法按照包开发者预期的方式来使用这些功能。

通过这些包级技能,智能体能够直接从编写这些包的人那里获得权威的使用说明。当go-router发布skills/go-router-navigation.md文件时,任何在添加GoRouter后运行skills get命令的Flutter团队,都会获得一份由GoRouter团队提供的技能文档,这份文档会详细解释GoRouter的工作原理。

为自己的包编写使用说明

如果你维护着团队内部使用的Dart或Flutter包,那么将这些使用说明打包在一起发布,无疑是一项极具价值的投资:

my_design_system/
  lib/
    src/
      components/
    my_design_system.dart
  skills/
    my-design-system-components.md    <- 教导智能体如何使用你的组件
    my-design-system-theming.md       <- 教导智能体如何使用你的主题系统
  pubspec.yaml
  README.md
---
name: my-design-system-components
description: 使用MyDesignSystem组件库来创建UI元素。无论是需要按钮、卡片、表单字段、导航元素,还是任何视觉组件,都可以使用这个库。只要存在相应的设计系统组件,就应该优先使用它们,而不是原始的Material或Cupertino部件。
---

# MyDesignSystem组件的使用方法

只要有可替代的MyDesignSystem组件,就永远不要使用原始的Flutter部件。

## 可用的组件

DsButton可以替代ElevatedButton、TextButton和OutlineButton;
DsCard可以替代Card;
DsTextField可以替代FormField;
DsAvatar可以替代CircleAvatar;
DsChip可以替代Chip;
DsBottomSheet可以替代>ShowModalBottomSheet。

## DsButton的使用方法

千万不要这样做:ElevatedButton( onPressed: _onSubmit, child: const Text('Submit'), )

应该这样做:DsButton( label: 'Submit', onPressed: _onSubmit, variant: DsButtonVariant.primary, )


`DsButton.variant`支持`primary`、`secondary`、`destructive`和`ghost`这几种样式。在加载组件时,可以传递`isLoading: true`参数来显示按钮的加载状态。

当你们团队中的开发者运行skills get命令时,这些技能会自动与任何官方的Flutter或Dart技能一起被安装到智能体系统中,从而使智能体完全了解你们内部使用的组件库。

技能、规则与MCP:区分它们的区别

智能体的这些技能与另外两种定制机制并存:AI规则文件和MCP服务器。理解它们各自的不同作用,有助于你将知识应用在正确的地方。

三种定制机制

1. AI规则(始终与具体上下文相关联,适用于整个项目)。

CLAUDE.mdAGENTS.md.cursorrules这些文件应该包含那些对整个项目而言始终成立的事实性信息。这些文件会在每次任务开始时以及每个会话期间被加载进来。

这些信息最适合用来描述项目名称和包标识符、Flutter与Dart SDK的版本号、核心包如flutter_blocgo_router、最低平台要求(例如Android API 24或iOS 15),以及项目的架构风格(比如以功能为导向的架构还是简洁清晰的架构)。详细的操作指南不适合放在这里,因为这些内容应该归入“技能”部分。

2. 技能(.agents/skills/*.md文件,会逐步加载)。

技能文件应包含如何完成特定类型工作的说明。只有当代理程序判断这些内容与当前任务相关时,才会加载这些文件。

这类信息最适合用来介绍如何组织Flutter项目文件结构、如何实现BLoC状态管理机制、如何编写测试用例、如何处理错误,以及其他与具体任务相关的操作步骤。而那些与整个项目相关的基本信息则不适合放在技能文件中,因为它们应该归入“项目规则”部分。

3. MCP服务器(通过工具扩展代理程序的功能)。

MCP服务器是通过针对特定代理程序的配置文件进行设置的,它们的作用是提供对各种工具和外部数据的访问权限,从而扩展代理程序的功能。这些工具在整个会话期间都是可用的。

这类信息最适合用来指导如何通过Dart MCP服务器查找Flutter文档、从pub.dev获取包信息、在项目中运行Flutter命令、读取连接设备的日志数据,或在代码库中搜索相关代码。操作指南、约定规则以及项目特定的要求也不适合放在MCP服务器中,因为这些内容应该归入“规则”或“技能”文件。

一个有用的判断标准是:如果某条信息适合放在README文件中,那么它很可能也属于“规则”或“技能”文件;如果获取这条信息需要网络请求或执行程序,那么它应该被放入MCP服务器中;而如果只有某种特定类型的任务才需要这条信息,那么它更适合被归入“技能”文件。

另一个判断标准是“上下文消耗量”。规则文件总是与具体的任务环境相关联,因此无论信息是否相关,它们在每次执行任务时都会消耗一定的“上下文资源”。因此,规则文件应该写得简短些(不超过50行),并且只包含事实性内容。而技能文件由于只有在相关任务时才会被加载,所以它们的“上下文消耗量”会相对较低。MCP服务器则根据工具调用的次数来计算其成本。

在团队中整理技能资料

将技能作为团队的共享知识

.agents/skills/目录必须被添加到你的Git仓库中。每当有人提交一份技能文档,团队中的所有开发人员在下次执行git pull操作时都会收到这份文档。当有新成员加入团队时,他们只需克隆仓库,就能立即掌握团队已经积累的所有技能知识。如果有人根据实际工作中的经验编写了新的技能文档,那么这些经验也会与相关的代码一起被保存在仓库中。

这使得这些技能成为一种实用的、系统化的知识体系:技能文件既为AI代理提供了操作指令,也记录了相关标准的具体内容。与维基页面或Confluence文章不同,这些技能文件是由实际生成代码的工具来读取的,而不仅仅是那些可能记得或忘记使用这些规则的开发者。

技能审核流程

对技能文件的任何修改都应经过与代码修改相同的拉取请求审核流程。如果某个技能文件采用了错误的规范或表述了过于模糊的规则,那么在整个团队使用该技能的过程中,都可能会产生错误的输出结果,直到这些问题得到纠正为止。

以下是合并技能修改内容之前需要检查的项目清单:

  • 该技能的描述应准确且全面地说明其在什么情况下适用。

  • 每一条规则都应当足够具体,能够切实改变代理的行为,而不会只是些模糊不清的指导原则。

  • 对于在训练数据中常见的情况,应该提供反例来进行说明。

  • 相关的代码示例在单独运行时应该能够正确编译通过。

  • 该技能不应与其它技能中的内容重复。

  • 需要通过让代理执行相关任务来测试该技能,确认其输出结果确实符合规则要求。

  • 至少还有一位会在日常工作中使用该技能的团队成员对其进行了审核。

保持技能内容的时效性

当团队的操作规范发生变化时,这些技能就会变得过时了——比如当你从某个导航库切换到另一个库,或者采用了新的测试框架、更新了设计系统,又或是重新设计了错误处理机制时。一个过时的技能比完全没有技能更糟糕,因为它会引导代理继续使用那些已经不再适用的方法。

将依赖关系的升级视为触发技能审核的契机。当你将go-router升级到新的版本时,需要检查相关的导航技能文件,确保它们仍然符合当前的API规范;当从团队总结会议中了解到新的设计模式时,也应在同一份拉取请求中更新相应的技能文件。

团队内部对技能的查找便利性

随着技能库规模的不断扩大,开发者需要能够快速找到适合自己任务的技能。因此,应使用统一的命名规范,并考虑维护一个简短的技能索引:

# .agents/skills/README.md (并非真正的技能文件,而是一个索引)

## Flutter相关技能
flutter-feature-architecture      -- Feature文件夹的结构及层次规则
flutter-bloc-state-management     -- Bloc事件、状态及组件集成方式
flutter-file-organization         -- 文件的划分、提取及命名规则
flutter-error-handling            -- 错误处理机制及返回类型
flutter-navigation                -- GoRouter路由配置、导航方法及深度链接处理
flutter-theming                   -- 设计元素、颜色设置及间距常量
flutter-testing                   -- 组件测试、Bloc相关测试及测试命名规范
flutter-accessibility             -- 语义标签、焦点设置及触摸交互功能
flutter-performance               -- 性能优化相关的组件与重建策略、列表处理技巧

## Dart相关技能
dart-models-freezed               -- 冻结模型、可序列化为JSON的数据结构、数据传输对象
dart-testing-conventions          -- 测试相关的规范、mocktail模拟框架、异步测试方法
dart-pattern-matching-idiomatic   -- Switch表达式、密封类及解构操作
dart-run-static-analysis          -- 静态分析工具及配置文件

这个索引并不会被智能体读取(它只是一个README.md文件,而非技能配置文件)。它是为那些刚接触这个项目、想在让智能体执行任务之前了解该项目中有哪些可用技能的开发者准备的。

编写技能描述的最佳实践

从实际错误入手,而非遵循理想化的模式

最有效的技能描述来源于对那些由人工智能生成但存在特定且可复现错误的代码的分析。这些错误恰恰证明了智能体的默认行为需要根据你的项目需求进行调整。每当你手动修复人工智能生成的错误时,这种修正过程本身就形成了一条技能规则。

那些基于理想化模式编写的技能描述(比如“理论上Bloc应该这样工作”),其效果往往不如那些通过纠正错误得出的技能描述。因为错误本身能告诉你训练数据在哪些地方与你的使用规范存在偏差,而规则则用于纠正这些偏差。

在提交之前先测试技能描述的正确性

在编写完某项技能的描述后,要通过让智能体执行与该技能相关的任务来对其进行测试。例如,可以让智能体使用Bloc的状态管理功能创建一个新的界面,或者分割一个较大的文件,又或者为某个代码库编写单元测试。之后要确认智能体的输出结果确实符合你设定的所有规则。

对于那些没有被正确遵守的规则,要么需要将其表述得更加明确,要么需要提供反例进行说明,或者结合更具体的描述来帮助智能体判断何时应该使用该技能。

使用丰富的触发词来撰写描述

技能描述的文字是唯一会被智能体始终读取的部分。因此,在描述中要使用那些能够明确表明该技能适用场景的特定词汇和短语:


# 描述中缺乏触发词的例子
description: 如何在Flutter项目中设置导航功能。

# 描述中包含丰富触发词的例子
description: 在Flutter应用中使用GoRouter来实现导航功能。适用于添加路由、在不同界面之间切换、配置深度链接、处理认证重定向、设置嵌套导航结构,或任何与Navigator、route、path、deep link、URL、back button或go_router包相关的操作。

使用丰富触发词编写的描述能够匹配更多种类型的任务需求,从而确保智能体只在真正需要的时候才会加载相应的技能描述,而不会因为只是简单匹配了某些关键词就错误地执行该技能。

编写技能描述时常见的错误

那些过于模糊、无法促使行为发生改变的规则

# 这些规则并不会带来任何变化:因为代理程序本来就在努力遵循这些原则
- 编写清晰、易于维护的代码。
- 遵循Flutter的最佳实践。
- 选择合适的状态管理方案。
- 以逻辑清晰的方式组织文件结构。

# 这些规则会直接影响具体的行为:因为代理程序之前采取的是其他做法
- 将所有提取出来的组件类放在其所属功能文件夹的`widgets/`子目录中。
- 将`BlocEvent`的子类命名为过去式动词短语,例如`ProfileLoadRequested`而不是`LoadProfile`。
- 绝不要使用`Navigator.push()`;应使用`context.go()`或`GoRouter`中的`context.push()`方法。
- 除非某个组件构造函数的参数有默认值,否则都应将其标记为“必填项”。

模糊的规则只表达了某种期望或目标,而具体的规则才能明确指出哪些行为是可被验证和实现的。任何一项技能规范都应该能够回答这样一个问题:“在了解了这条规则之后,代理程序会做出哪些不同的行为?”

缺乏针对高频错误模式的反例说明

有些错误的编程模式会在训练数据中出现数百万次。如果一个代理程序通过成千上万的例子学会了将`_buildHeaderSection()`视为有效的Flutter编码方式,那么仅凭一条文本规则,它很可能不会放弃这种习惯。

应该展示代理程序实际会生成的代码,并将其与你期望的代码进行对比。这种方法非常有效,因为代理程序能够识别出具体的代码模式,而这种对比能够在代码层面而非仅仅在文字层面帮助人们理解规则的含义。

描述内容无法触发正确的操作

如果一项关于`Bloc`状态管理的技能规范中只写着“需要实现状态管理”,那么当有人询问“如何在结账页面添加加载状态”时,这个规范就无法起到指导作用。描述内容中必须包含像“加载状态”这样的关键词汇,才能确保人们能够根据这些提示来采取正确的行动。

为了检验你的描述是否有效,可以思考人们可能会用哪些不同的方式来描述需要这项技能的任务,并确保描述中包含了所有这些可能的表达方式。

没有将技能规范提交到版本控制系统中

如果某些技能规范仅保存在某位开发者的个人电脑上,那么它们就只是个人的笔记而已,而不是团队共有的知识。只有被提交到版本控制系统中的技能规范,才能成为全团队成员都可以使用的知识资源;新员工从入职第一天起就可以使用这些规范,而无需进行额外的配置;同时,这些规范还可以像代码一样被定期审查、改进和维护。请务必将`/.agents/skills/`目录下的所有文件都提交到Git版本控制系统中。

编写过于详细的技能规范

技能规范应该用来规定一些通用的规则和惯例,而不是对每一个可能的实现细节都进行明确规定。如果你的规范指定了组件的确切像素尺寸、特定加载指示器的颜色,或者构造函数的参数顺序,那么这种过度详细的规定反而会限制代理程序在面对新情况时做出合理的决策。

技能应该能够捕捉那些在没有指导的情况下确实会产生的结构性和架构性差异。那些存在多种同样可行实现方案的细节,不应该被纳入技能体系中。

结论

在Flutter中转向智能开发,并不是为了取代开发者,而是为了让开发者能够完成更多的工作。

一个具备强大技能的AI智能体可以在几分钟内制定出符合你们团队规范、结构正确的功能实现方案。资深开发者会对该方案进行审核、调整,然后将其投入实际使用。正是这些技能填补了AI智能体的通用知识与你们团队具体标准之间的差距。

使这些技能真正具备强大作用的原因在于:它们是AI开发工作流程中唯一包含那些模型在训练过程中并未接触过的知识的部分。虽然该模型学习了数百万行公开的Flutter和Dart代码,但它从未见过你们的代码库,也从未在你们的项目中犯过错误并被纠正过;它更没有参加过你们团队的架构讨论或回顾会议。它不知道你们的团队曾经尝试过某种开发模式,但发现其使用起来很不方便,因此才特意选择了另一种方案。而你们的技能恰恰包含了所有这些知识。

来自github.com/flutter/agent-plugins的官方Flutter技能库以及来自github.com/dart-lang/skills的官方Dart技能库为你们提供了覆盖最常见的Flutter和Dart开发模式的、高质量的开发起点。通过skills命令行工具,安装这些技能库仅需执行一条npm命令即可完成。由于这些技能是作为包级组件存在的,因此它们的依赖关系也可以附带自己的使用说明,并且会随着这些包的更新而得到相应的更新。

但是,那些由你们根据自己的实际开发经验、代码审查反馈以及架构决策编写出来的技能,才真正具有最高的价值。因为这些技能所蕴含的知识是任何公共代码库都无法提供的。

像“永远不要将StatefulWidget与其State类分开”这样的规则,是基于对Flutter编译机制的深入理解而制定的;而“对于所有的Bloc事件和状态,都应该使用final具体类来构建封闭的类层次结构”这一规则,则既体现了Dart 3的类型系统特性,也考虑到了在实际开发中采用这种设计方式所带来的种种好处;至于“在任何使用await语句之后,在使用BuildContext之前必须先检查是否已经完成了mount操作”这一规则,则是通过观察在实际情况中违反这一规则时所会出现的具体错误而总结出来的。

这些源自你们实践经验的规则,被记录成技能库并保存在你们的代码仓库中,它们使得你的AI智能体从一个通用的Flutter开发者转变成了一个真正了解你项目的开发者。为了编写这些技能而花费的每一分钟时间,都是值得的。

参考资料

针对Flutter和Dart的智能体技能库(Flutter官方文档): 这份指南全面介绍了智能体技能的相关内容,包括渐进式展示机制、官方代码仓库以及通用的安装命令。https://docs.flutter.dev/ai/agent-skills

开始使用Flutter中的AI功能(Flutter官方文档): 为Claude Code、Antigravity、Codex、Cursor等工具提供了详细的设置指南,同时还包含了每个工具的官方插件安装说明。https://docs.flutter.dev/ai/get-started

Flutter代理插件仓库(GitHub): 这是由Flutter团队维护的官方仓库,其中包含了与响应式布局、GoRouter导航、JSON序列化、组件测试、集成测试、BLoC模式等相关的技术资源。https://github.com/flutter/agent-plugins

Dart技能仓库(GitHub): 这也是由Dart团队维护的官方仓库,涵盖了单元测试、静态分析、包管理工具、模式匹配、命令行应用程序以及原生资源等相关内容。https://github.com/dart-lang/skills

Flutter AI规则文档(Flutter官方文档): 介绍了项目范围内使用的AI规则文件格式(如CLAUDE.md、AGENTS.md、.cursorrules等),以及这些规则如何与各种技能配合使用。https://docs.flutter.dev/ai/ai-rules

代理技能规范: 这个网站定义了通用的SKILL.md文件格式、目录结构规则以及各代理组件的兼容性要求,是判断技能是否符合标准的权威依据。https://agentskills.io

skills Dart包(pub.dev): 这是一个Dart命令行工具,用于从项目依赖关系中安装代理技能。包开发者可以通过这个工具将技能随自己的包一起发布,团队成员也可以自动进行安装。https://pub.dev/packages/skills

skills CLI(npm): 这是一个通过npm发布的命令行工具,用于从GitHub仓库中安装代理技能。常用的安装命令是`npx skills add flutter/agent-plugins`。https://www.npmjs.com/package/skills

skills-registry Serverpod: 这个仓库为许多尚未自行提供技能的Dart和Flutter包提供了代理技能资源,例如Riverpod、flutter-shadcn-ui等。该仓库由Serverpod团队维护。https://github.com/serverpod/skills-registry

dhruvanbhalara/skills高级Flutter技能文档: 这是一个内容丰富的文档项目,详细列出了所有可用的Flutter代理技能,并对每种技能的功能进行了说明。https://github.com/dhruvanbhalara/skills

相关文章

技术实践

了解人工智能软件开发生命周期流程——构建智能代理功能的完整指南

也许你可以理解这样的场景:本周,你用了同样的说明四次向别人解释人工智能模型的使用方法。 你反复讲解过团队是如何构建演示文稿的框架的,哪些检查步骤需要在部署之前完成,以及为什么测试数据库并不是文档中提到的那个。 每次你都要把这些内容重新写一遍,每次智能助手也能完成得不错,但每次新的会话开始时,一切都得从零开始。 而这正是 智能助手技能 所要解决的问题。 技能 实际上就是一个文件夹,其中只包含一个名为 Skill.md 的文件。智能助手在启动时会阅读其中的一行总结内容,而只有当真正需要时才会打开完整的说明文件。你只需把解释内容编写一次,将其与代码一起提交,那么团队中的每个智能助手就能访问这些信息,

阅读全文
技术实践

OpenTelemetry的工作原理:一份全面的指南

如果你是一名软件开发人员或DevOps工程师,那么你很可能已经听说过OpenTelemetry。在讨论可观测性、监控或分布式系统的调试时,这个术语经常会被提及。 你可能也知道它的基本定义,但了解OpenTelemetry是什么与真正理解它的运作原理其实是两回事。 读完本指南后,你将能够明白OpenTelemetry是如何从端到端工作的——从请求进入你的应用程序的那一刻起,直到这些数据被显示在可观测性后端系统中。你还会了解到追踪信息、时间跨度、上下文传播机制以及数据导出工具是如何共同构成一个完整的处理流程的。 如果你完全不了解OpenTelemetry,也别担心:接下来的部分会帮助你快速掌握相关

阅读全文
技术实践

Flutter中的模块化设计:如何将简洁的架构设计与领域驱动设计相结合,从而打造出独立性强、可扩展性高的功能模块

作为在小型团队中工作的工程师,目前的开发结构或许还可以应付得来。但倘若你的团队规模扩大到20人或更多,且所有人都在同一个代码库上进行开发,那么你就需要运用精心的设计思路、顺畅的协作流程,以及一些能够保持代码库简洁性的方法。 在选择文件夹结构、应用程序架构以及项目中使用的各种模式时,你也必须十分谨慎、有目的性地进行决策。 大多数Flutter应用程序的开发过程都是类似的。起初,你只需要一个“lib”文件夹、几个界面模板、一个“models”目录,以及一个负责处理所有逻辑的“services”文件即可。这样的结构能够正常运行,应用程序也能顺利发布,大家也会对此感到满意。 但随着应用程序的发展,新的

阅读全文
技术实践

Firestore是如何存储数据的,以及如何使用它来执行CRUD操作

大多数应用程序最终都需要存储和操作数据。如果你使用Firebase进行开发,那么这些数据就会存储在Firestore中——这是谷歌提供的一种灵活且可扩展的NoSQL文档数据库。 但在能够自信地创建、读取、更新或删除数据之前,你首先需要了解Firestore实际上是如何组织信息的。Firestore的结构与SQL数据库不同,如果将其视为SQL数据库来使用,那么最终结果很可能就是得到一个结构混乱、难以查询的数据模型。 在本教程中,你将学习 Firestore的NoSQL数据模型是如何工作的,然后通过使用Firebase Web SDK(v9及以上版本)来构建一个简单的任务管理应用程序,从而练习各种

阅读全文