Claude Code 用于 Flutter/Dart 开发:项目梳理、状态、测试与构建
用 Claude Code 做 Flutter/Dart 开发:梳理项目、管理 pubspec、修改状态、编写测试并验证构建。
在 Flutter/Dart 项目里,Claude Code 最有价值的地方不是一次生成一个漂亮组件,而是能读懂现有工程,把界面、状态、pubspec.yaml、平台差异、测试和构建检查串起来。Widget 可以理解为界面部件,state 是会改变界面显示的数据,pubspec 是记录 SDK 约束、依赖和资源文件的配置。只让 Claude Code “做一个页面”,往往会得到看起来可用、但 Android 权限、iOS plist、Web 资源路径或 widget test 没跟上的实现。
本文用一个购物车示例说明安全做法。harness 在这里指“给智能体搭好的工作脚手架”:允许编辑的文件、禁止事项、检查命令、验收标准和复查清单。官方资料建议以 Flutter CLI、Flutter testing、Widget testing、Flutter pubspec options、Dart pubspec、Platform channels 和 Claude Code common workflows 为准。相关内容还可以读 React Native 开发、测试策略 和 CLAUDE.md 最佳实践。
先画项目地图
第一步不要让 Claude Code 直接改代码。先让它阅读 pubspec.yaml、lib/、test/、平台目录和分析配置,确认现有约定。这样可以避免它引入新的状态管理、错误的目录结构或不适合当前项目的测试风格。
请先阅读这个 Flutter 项目,不要编辑文件。
请报告:
1. pubspec.yaml 中的 SDK 约束、依赖、assets 设置
2. lib/ 下 UI、状态、data 层的组织方式
3. test/ 和 integration_test/ 是否存在,以及现有测试风格
4. android/ ios/ web/ macos/ windows/ linux/ 中实际启用的目标
5. flutter analyze、flutter test、build 命令是否有记录
最后列出可以安全修改的文件和不应触碰的文件。
| 范围 | 交给 Claude Code | 人类需要决定 |
|---|---|---|
| 项目地图 | 文件结构、依赖、测试风格调查 | 可编辑边界 |
| Widget | 界面部件、无障碍、响应式细节 | 文案和设计判断 |
| state | 按现有 setState、ChangeNotifier、Riverpod 或 Bloc 风格实现 | 全局状态策略 |
| pubspec | 依赖和资源变更建议 | 是否接受新包 |
| 平台 | Android/iOS/Web/Desktop 差异清单 | 支持目标和商店要求 |
| 测试/构建 | widget test、integration test、命令执行 | 是否合并和发布 |
谨慎管理 pubspec
pubspec.yaml 控制依赖、资源、SDK 约束和测试包。让 Claude Code 修改它之前,先要求说明原因、替代方案、影响范围和验证命令。
name: cart_ai_demo
description: A small Flutter cart screen used to verify Claude Code prompts.
publish_to: "none"
environment:
sdk: ">=3.4.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter:
uses-material-design: true
assets:
- assets/images/
新实验可以这样建立基线;已有项目不要机械执行,而是让 Claude Code 对照当前配置提出命令。
flutter create cart_ai_demo
cd cart_ai_demo
flutter pub get
flutter analyze
flutter test
常见坑有三个:assets 缩进错误导致图片找不到;没有看 pubspec.lock 就升级依赖,影响其他页面;为了一个小状态变化引入不必要的状态管理包。安全提示词可以这样写:
检查 pubspec.yaml,判断购物车改动是否真的需要新增依赖。
编辑前请先列出原因、替代方案、受影响文件和验证命令。
未经确认不要修改 pubspec.yaml 或 pubspec.lock。
小步修改 Widget 和 state
下面代码可以放进 flutter create cart_ai_demo 后的 lib/main.dart。它只使用 Flutter SDK。CartController 负责状态变化,Widget 只负责渲染和响应点击。
import 'package:flutter/material.dart';
void main() => runApp(const CartDemoApp());
class CartDemoApp extends StatelessWidget {
const CartDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cart AI Demo',
theme: ThemeData(colorSchemeSeed: Colors.teal, useMaterial3: true),
home: const CartSummaryPage(),
);
}
}
class CartLine {
const CartLine({required this.name, required this.price, required this.quantity});
final String name;
final int price;
final int quantity;
CartLine copyWith({int? quantity}) =>
CartLine(name: name, price: price, quantity: quantity ?? this.quantity);
}
class CartController extends ChangeNotifier {
final List<CartLine> _lines = const [
CartLine(name: 'Dart notebook', price: 1800, quantity: 1),
CartLine(name: 'Flutter sticker', price: 500, quantity: 2),
].toList();
List<CartLine> get lines => List.unmodifiable(_lines);
int get total => _lines.fold(0, (sum, line) => sum + line.price * line.quantity);
void increment(int index) {
final line = _lines[index];
_lines[index] = line.copyWith(quantity: line.quantity + 1);
notifyListeners();
}
void decrement(int index) {
final line = _lines[index];
if (line.quantity == 1) return;
_lines[index] = line.copyWith(quantity: line.quantity - 1);
notifyListeners();
}
}
class CartSummaryPage extends StatefulWidget {
const CartSummaryPage({super.key});
@override
State<CartSummaryPage> createState() => _CartSummaryPageState();
}
class _CartSummaryPageState extends State<CartSummaryPage> {
late final CartController controller;
@override
void initState() {
super.initState();
controller = CartController();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Cart summary')),
body: AnimatedBuilder(
animation: controller,
builder: (context, _) => ListView(
padding: const EdgeInsets.all(16),
children: [
for (final entry in controller.lines.indexed)
ListTile(
title: Text(entry.$2.name),
subtitle: Text('JPY ${entry.$2.price} x ${entry.$2.quantity}'),
trailing: Wrap(
spacing: 8,
children: [
IconButton(
tooltip: 'Decrease ${entry.$2.name}',
onPressed: () => controller.decrement(entry.$1),
icon: const Icon(Icons.remove_circle_outline),
),
IconButton(
tooltip: 'Increase ${entry.$2.name}',
onPressed: () => controller.increment(entry.$1),
icon: const Icon(Icons.add_circle_outline),
),
],
),
),
const Divider(),
Text('Total: JPY ${controller.total}',
style: Theme.of(context).textTheme.headlineSmall),
],
),
),
);
}
}
要明确告诉 Claude Code:不要在 build 中创建 controller;不要把价格计算散落在 Widget 内;不要忘记 dispose;不要为了一个小页面新增包。好的提示词是:
只修改购物车 UI,并把状态变化保持在 CartController 中。
不要在 build 中创建 CartController,不要新增依赖。
请同时添加 widget test,证明点击加号和减号后总价会变化。
测试和设备验证
如果项目名是 cart_ai_demo,下面可以放到 test/cart_summary_test.dart。它验证初始总价、点击加号后的总价和点击减号后的总价。
import 'package:cart_ai_demo/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('updates the cart total when quantity changes', (tester) async {
await tester.pumpWidget(const CartDemoApp());
expect(find.text('Total: JPY 2800'), findsOneWidget);
await tester.tap(find.byIcon(Icons.add_circle_outline).first);
await tester.pump();
expect(find.text('Total: JPY 4600'), findsOneWidget);
await tester.tap(find.byIcon(Icons.remove_circle_outline).first);
await tester.pump();
expect(find.text('Total: JPY 2800'), findsOneWidget);
});
}
flutter analyze
flutter test
需要模拟器或真机时,再加 integration test。它更接近用户操作,但运行更慢。
import 'package:cart_ai_demo/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('cart can be changed on a real device', (tester) async {
await tester.pumpWidget(const CartDemoApp());
await tester.tap(find.byIcon(Icons.add_circle_outline).last);
await tester.pumpAndSettle();
expect(find.text('Total: JPY 3300'), findsOneWidget);
});
}
flutter devices
flutter test integration_test -d <device_id>
平台差异和构建检查
Flutter 共享 Dart 层,但不共享所有部署规则。Android 可能需要 manifest 权限,iOS 可能需要 Info.plist 文案和签名,Web 可能遇到 CORS 或浏览器 API 限制,桌面端可能需要文件权限或窗口尺寸处理。涉及插件、权限或原生功能时,让 Claude Code 同时检查平台目录。
flutter analyze
flutter test
flutter build apk --debug
flutter build web --release
iOS release 构建需要 macOS 和 Xcode 环境。不要在无法运行 Xcode 的机器上让 Claude Code 声称“iOS 已验证”。提示词里写清楚可执行和不可执行的环境条件。
实际用例至少有四类。第一类是在列表中加入数量、过滤或排序状态;第二类是整理 pubspec.yaml 和 assets;第三类是处理 Android/iOS/Web 单独出现的权限或插件问题;第四类是给没有测试的旧页面补回归测试。每类都要给 Claude Code 目标文件、预期命令、失败时如何报告。
安全提示词和验证记录
调查:读取 lib/、test/、pubspec.yaml 和平台目录,不要编辑,输出项目地图和风险。
实现:只修改 lib/features/cart,遵循现有状态管理,不新增依赖。
pubspec:需要依赖时暂停,先给理由、替代方案、影响范围和验证命令。
验证:添加 widget test,运行 flutter analyze 和 flutter test,并用文件名报告失败。
复查:检查 dispose、异步处理、build 中副作用、平台配置、无障碍、包变更和缺失测试。
Masa 的实务经验是,Flutter 改动最容易出问题的不是 Dart 语法,而是范围太大。先做项目地图,再做小差分,最后用测试和构建确认,结果更容易审核。团队导入时,可以先用免费速查表统一提示词,再用Claude Code 培训与咨询整理团队规则。想进一步设计 AI 工作边界,可以阅读 harness engineering。
本文示例按 flutter create cart_ai_demo 的标准结构编写。当前写作环境没有安装 Flutter SDK,因此没有在本机执行 flutter test。发布前请在真实 Flutter 环境中运行 flutter analyze、flutter test 和目标平台构建。Claude Code 能提高速度,但公开质量仍来自范围控制、官方文档确认、测试和真机验证。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。