使用 Claude Code 进行 Flutter UI 迭代:实用工作流程
使用 Claude Code 的 Flutter UI 迭代:一套实用循环,将用户故事转化为 widget 树、状态和导航,同时保持变更模块化、易于审阅。

问题:快速的 UI 迭代如何避免变成混乱
快速的 Flutter UI 开发通常开局不错。你调整布局、增加按钮、移动字段,屏幕很快变得更好。但经过几轮修改后,速度会变成一堆没人愿意审查的改动,问题就出现了。
团队通常会遇到相同的失败模式:
- widget 树无规划地膨胀,导致“一个小改动”需要修改很多文件。
- 状态被直接钉在 UI 代码上,使重建变得难以预测,错误也更难追踪。
- 导航逻辑四处散落(这里 push、那里 pop),直到流程与用户实际行为不一致。
- 命名漂移、组件重复,没人确定哪个 widget 是“真实”的那个。
- diff 变得巨大,审查者只能走马观花,问题漏过,回归也会在之后显现。
一个主要原因是“一个大提示”的方法:描述整个功能,要求生成全部屏幕并接受大量输出。助手本想帮忙,但同时触及太多代码部分,使得改动混乱、难以审查、合并有风险。
可重复的循环通过强制清晰并限制影响范围来解决这个问题。不要一次性“构建整个功能”;改为重复做以下事情:挑选一个用户故事,生成能验证该故事的最小 UI 切片,只添加该切片所需的状态,然后为一条路径接入导航。每次迭代都足够小,易于审查,错误也容易回滚。
目标是提供一套实用工作流程,能把用户故事变成具体屏幕、状态处理与导航流,同时保持可控性。做好之后,你会得到模块化的 UI 片段、更小的 diff,以及在需求变更时更少的意外情况。
把用户故事转成可构建的 UI 规范
用户故事是写给人的,不是写给 widget 树的。在生成任何代码之前,把故事转换成一个小的 UI 规范,描述可见行为。“完成”应该是可测试的:关注用户能看到、能点击、并能确认的内容,而不是设计“感觉现代”与否。
让范围具体化的一个简单方法是把故事拆成四个桶:
- 屏幕:哪些改变,哪些保持不变。\n- 组件:哪些新 UI 出现、放在哪儿。\n- 状态:loading、success、error、empty,以及每种状态显示什么。\n- 事件:点击、滑动、下拉刷新、返回、重试。
如果故事仍然模糊,用简单语言回答这些问题:
- 哪些屏幕改变,哪些保持不变?
- 出现了哪些新组件,它们放在哪?
- 存在哪些状态,每个状态显示什么?
- 哪些事件会驱动状态改变?
- 运行应用后 30 秒内你能做的验收检查是什么?
提前添加约束可以指导每一个布局选择:主题基础(颜色、间距、排版)、响应式(先做手机纵向,再考虑平板宽度)以及可访问性的最低要求,比如点击目标大小、文本缩放可读性和图标的语义标签。
最后,决定哪些是稳定的、哪些是可变的,这样就不会让代码库发生频繁震荡。稳定项是其他功能所依赖的东西,如路由名、数据模型和现有 API。可变项更安全去迭代,例如布局结构、微文案和具体的 widget 组合。
示例:"作为用户,我可以在详情页把一项保存到收藏。" 一个可构建的 UI 规范可能是:
- 详情页显示一个书签图标。
- 点击切换已保存状态。
- 保存时显示小的进度指示器。
- 失败时显示内联错误并提供重试操作。
- 导航保持不变(不新增路由)。
这些足够构建、审查并迭代,而不需要猜测。
设定迭代循环以保持 diff 小而可审阅
小的 diff 并不是要求你慢下来。它们让每次 UI 改动都容易审查、容易撤销、不容易破坏。最简单的规则:每次迭代只做一个屏幕或一个交互。
在开始前先选好一个紧凑的切片。"给 Orders 屏添加空状态" 是个好切片。"重构整个 Orders 流程" 则不是。目标是做出一个队友一分钟内能看懂的 diff。
一个稳定的文件夹结构也能帮助你把改动控制在小范围内。简单的 feature-first 布局防止你把 widgets 和 routes 四处散落:
lib/
features/
orders/
screens/
widgets/
state/
routes.dart
保持 widget 小而可组合。当一个 widget 有明确的输入和输出时,你可以在不碰状态逻辑的情况下改变布局,也可以在不重写 UI 的情况下改变状态。优先使用接受普通值和回调的 widget,而不是直接依赖全局状态。
一个保持可审查的循环:
- 为切片写 3 到 6 行 UI 规范(出现什么、点击做什么、loading/error 源自何处)。
- 仅生成或编辑最少需要的文件(通常是一个屏幕和一两个 widget)。
- 运行屏幕,然后做一次清理(命名、间距、移除未使用的属性)。
- 使用与切片相匹配的提交信息提交。
设定一个硬规则:每次变更都必须易于回退或隔离。迭代屏幕时避免顺手做大范围重构。如果你发现无关的问题,把它们记下来并在单独的提交中修复。
如果你的工具支持快照和回滚,把每个切片当作一个快照点。有些 vibe-coding 平台(例如 Koder.ai)包含快照与回滚功能,在尝试大胆 UI 变更时能让试验更安全。
另一个让早期迭代保持平静的习惯是:优先新增 widget,而不是修改共享组件。共享组件是小改动变成大 diff 的地方。
按步骤:从用户故事生成 widget 树
如果把思考和敲代码混在一起,快速的 UI 开发会出问题。先拿到清晰的 widget 树计划再生成代码。
-
先只要求生成 widget 树大纲。你想要的是 widget 名称、层级结构以及每一部分展示的内容。不要代码。这一阶段你可以发现缺失的状态、空屏或奇怪的布局选择,修改代价也很小。
-
要求组件职责分解。保持每个 widget 的单一关注点:一个 widget 渲染头部,另一个渲染列表,另一个处理空/错展示。如果将来需要状态,在此标注但先不用实现。
-
生成屏幕脚手架和无状态 widget。先从单文件屏幕开始,放占位内容和明确的 TODO。保持输入显式(构造函数参数),以便之后接入真实状态时无需重写树。
-
单独一轮处理样式与布局细节:间距、排版、主题和响应式行为。把样式作为独立的 diff 以便审查简单。
一个有效的提示模板
把约束放在最前面,阻止助手发明无法交付的 UI:
- 目标设备(仅手机、也包括平板、方向)
- 设计约束(Material 3、现有主题颜色、间距规则)
- 导航期望(返回行为、deep link(若有))
- 验收标准(必须可见和可点击的项)
- 现有代码边界(必须保留的文件/组件名称约定)
具体示例:用户故事是 "As a user, I can review my saved items and remove one." 先要求一个包含 app bar、带行的列表和空状态的 widget 树。然后请求像 SavedItemsScreen、SavedItemTile、EmptySavedItems 这样的分解。仅在确认后再生成用假数据填充的 stateless scaffold,最后再单独改样式(分割线、内边距和显眼的删除按钮)。
在不膨胀 UI 代码的情况下加入状态处理
当每个 widget 都开始做决策时,UI 迭代就会失控。让 widget 保持“哑”:它们读取状态并渲染,而不是包含业务规则。
先用自然语言命名状态。大多数功能需要比“loading”和“done”更多的状态:
- Loading(首次加载或刷新)
- Empty(暂无数据)
- Error(请求失败、权限被拒)
- Success(数据就绪)
- Partial input(表单开始填写但未通过校验)
然后列出会改变状态的事件:点击、表单提交、下拉刷新、返回、重试和“用户编辑字段”。提前写这些可以避免后续的猜测。
把状态和 widget 分离
为该功能选择一种状态处理方式并坚持使用。目标不是“最好的模式”,而是能带来一致的 diff。对于小屏幕来说,简单的 controller(ChangeNotifier 或 ValueNotifier)通常足够。把逻辑放在一个地方:
- 输入:来自 UI 的事件(submit、refresh、edit)\n- 输出:一个单一状态对象供 UI 渲染\n- 副作用:API 调用与导航请求
在加入代码前,用简单的英语写出状态转换。例如登录屏:\n\n"当用户点击 Sign in:设为 Loading。如果邮箱无效:保持 Partial input 并显示内联消息。如果密码错误:设为 Error 并允许 Retry。如果成功:设为 Success 并导航到 Home。"\n\n然后生成匹配这些句子的最小 Dart 代码。审查时可以直接将 diff 与规则对照,保持简洁清晰。
为无效输入添加可测试规则
把校验明确写出来。决定在输入无效时的行为:\n\n- 是阻止提交,还是允许提交后显示错误?\n- 哪些字段在什么时候显示错误?\n- 返回是否会丢弃部分输入或保留?\n 当这些答案写下来后,UI 保持干净,状态代码也会更小、更易测。
设计与真实用户行为匹配的导航流
良好的导航始于一张小地图,而不是一堆路由。针对每个用户故事,写下四个时刻:用户从哪里进入、最可能的下一步、如何取消,以及“返回”意味着什么(返回到上一个屏幕还是安全的主页)。
先画路由图,再锁定在屏幕间传递的内容
一张简单的路由图应回答常导致返工的问题:\n\n- Entry:哪个屏幕首先打开,从哪里进入(Tab、通知、deep link)\n- Next:主要的前进路径是什么\n- Cancel:放弃流程时用户会去哪儿\n- Back:是否允许返回,返回时应保留什么\n- Fallback:若缺少必要数据应如何处理\n
然后明确屏幕间传递的参数。要具体:IDs(productId、orderId)、筛选条件(日期范围、状态)、草稿数据(部分填写的表单)。若跳过这一步,你会把状态塞到全局单例或为“查找”上下文而重建屏幕。
为 deep link 和“返回结果”模式做规划
即便不在第一天就发布 deep link,也要决定用户中途进入时发生什么:你能否加载缺失数据,还是应该重定向到安全的入口屏?\n\n还要决定哪些屏应返回结果。例如:Select Address 屏返回一个 addressId,结账页在接收后无需完整刷新即可更新。保持返回值简单且有类型,这样改动也更易审查。
编码前列出边缘情况:未保存更改(显示确认对话框)、需要鉴权(登录后暂停并恢复)、缺失或被删除的数据(显示错误并提供明确出口)。
让 UI 变更可审查与模块化
快速迭代时真正的风险不是“UI 错了”,而是“无法审查的 UI”。如果队友无法看出改变了什么、为什么改、哪些保持不变,那么每次后续迭代都会更慢。
一个有帮助的规则:先锁定接口,然后允许内部移动。稳定公共 widget 的 props(输入)、简短的 UI 模型和路由参数。一旦这些被命名和类型化,你就可以在不破坏其他部分的情况下重塑 widget 树。
偏好小而稳定的接缝(seams)
在生成代码前先要一个对 diff 友好的计划。你需要一个计划说明哪些文件会改变,哪些必须保持不动。这样审查更聚焦,避免意外的重构改变行为。
保持 diff 小的模式包括:\n\n- 保持公共 widget 轻量:只接受必要的数据和回调,避免访问单例。\n- 及早把业务规则移出 widget:放在 controller 或 view model 中,UI 仅渲染状态。\n- 当某个 UI 片段不再频繁变更时,提取为可复用 widget,暴露清晰有类型的 API。\n- 路由参数显式(有时用单个参数对象比多个可选字段更清晰)。\n- 在 PR 描述中加入简短的变更日志:改了什么、为什么改、需要测试的点是什么。
一个审查者会喜欢的具体示例
假设用户故事是 “作为购物者,我可以在结账时从结账页编辑我的收货地址。” 先锁定路由参数:CheckoutArgs(cartId, shippingAddressId) 保持稳定。然后在屏幕内部迭代。布局稳定后,把界面拆成 AddressForm、AddressSummary 和 SaveBar。
如果状态处理发生变化(例如校验从 widget 移到 CheckoutController),审查仍然清晰:UI 文件主要变化在渲染层,而 controller 在一个地方展示逻辑变更。
与 AI 助手迭代时常见错误与陷阱
最快速却最伤进度的方式是让助手一次改全局。如果一次提交同时触及布局、状态与导航,审查者无法判断出错的来源,回滚也会变得混乱。
更安全的习惯是:每次迭代只带一个意图——先成型 widget 树,然后接入状态,最后连接导航。
会造成杂乱代码的错误
一个常见问题是让生成的代码在每个屏幕上引入不同的模式。如果一个页面用 Provider,下一页用 setState,第三页又引入自定义 controller,整个应用会迅速变得不一致。挑一小套模式并强制执行它们。
另一个错误是在 build() 里直接做异步工作。快速 demo 时看起来没问题,但会在重建时重复调用,造成闪烁与难以追踪的 bug。把调用移到 initState()、view model 或专门的 controller 中,让 build() 专注于渲染。
命名是一个安静的陷阱。能编译但读起来像 Widget1、data2 或 temp 的代码会让未来重构变得痛苦。清晰的命名也能帮助助手在后续修改时产生更符合意图的代码。
防止最糟结果的护栏:\n\n- 每次迭代只改布局、状态或导航中的一个\n- 在功能范围内重用相同的状态模式\n- 不要在 build() 里做网络或数据库调用\n- 在添加更多功能前先重命名占位符\n- 优先提取 widget,而不是增加嵌套层级
嵌套陷阱
一个典型的视觉修复是不断加 Container、Padding、Align 和 SizedBox 来达到预期效果。几轮下来,树会变得难以阅读。
如果一个按钮位置不对,先尝试删掉无用的包装、用单一父布局控件,或提取一个小部件并给它自己的约束,而不是不断套娃。
示例:结账页中总价在加载时抖动。助手可能会多包几层 widget 来“稳定”它。更干净的做法是用简单的占位符保留空间,同时保持行结构不变。
提交下一个 UI 迭代前的快速清单
提交前做两分钟检查,确认用户价值并保护你免受意外回归。目标不是完美,而是确保本次迭代易于审查、易于测试并且易于回退。
提交就绪清单
阅读用户故事一次,然后在运行的应用上(或至少在简单的 widget 测试中)核对:
- widget 树是否匹配故事: 验收标准中的关键元素存在且可见。文本、按钮和空白都是有意安排的。
- 所有状态都能触发: Loading、Error、Empty 状态不仅在代码里有注释,你能触发并看到它们(即使用临时 debug 标志)。
- 导航与返回行为合理: 返回到预期屏幕、对话框能正常关闭、deep link(如用到)落点合理。
- diff 保持小且有归属: 变更限定在少数组文件并有明确责任。没有顺手改动的重构。
- 回滚干净: 回退此提交后,其他屏幕仍能编译运行。移除临时标志或占位资源以免日后出问题。
一个现实检查:如果你新增了订单详情屏,你应能(1)从列表打开它,(2)看到加载 spinner,(3)模拟错误,(4)看到空订单,以及(5)按返回回到列表且没有奇怪跳转。
如果你的工作流支持快照/回滚,在较大布局改动前先拍快照会很有帮助。一些平台(例如 Koder.ai)支持此类功能,能让你在不把主分支置于风险下的情况下更快迭代。
一个现实示例:三个迭代把用户故事变成屏幕
用户故事:"作为购物者,我可以浏览商品、打开详情页、把商品保存到收藏,稍后查看我的收藏。" 目标是在三次小而可审查的步骤中把文字变成屏幕。
迭代 1: 只关注浏览列表屏。创建足够渲染的 widget 树,但不接真实数据:带 AppBar 的 Scaffold,一个占位行的 ListView,并为 loading 与 empty 提供清晰 UI。状态保持简单:loading(显示 CircularProgressIndicator)、empty(显示简短信息和可选的 Try again 按钮)、ready(显示列表)。
迭代 2: 添加详情页并接入导航。明确地在 onTap 中 push 路由并传递一个小的参数对象(例如:item id、title)。把详情页先做成只读:标题、描述占位和一个 Favorite 操作按钮。目标是实现故事里的路径:列表 -> 详情 -> 返回,不添加额外流程。
迭代 3: 引入收藏状态的更新与 UI 反馈。添加单一事实源(即便仍是内存级别),并把它接入两个屏。点击 Favorite 立刻更新图标并显示一个简短确认(比如 SnackBar)。然后增加一个读取同一状态并处理 empty/list 的 Favorites 屏。
一个典型的可审查 diff 看起来像:
browse_list_screen.dart:widget 树 + loading/empty/ready UI\n-item_details_screen.dart:UI 布局并接受导航参数\n-favorites_store.dart:最小的状态持有者与更新方法\n-app_routes.dart:路由与类型化导航辅助工具\n-favorites_screen.dart:读取状态并显示空/列表 UI
如果某个文件变成了“所有事情发生的地方”,在继续之前先把它拆分。小而清晰命名的文件会让下一次迭代更快、更安全。
后续步骤:让这个循环在各功能间可重复
如果这个工作流程只有在你“进入状态”时才有效,那当你切换屏幕或有同事接手时它就会崩溃。把循环写下来,并为变更大小设定护栏,让它成为习惯。
建立可复用的提示模板
使用一个团队模板,让每次迭代从相同输入开始并产生相同类型的输出。保持简短但具体:\n\n- 用户故事 + 验收标准(“完成”意味着什么)\n- UI 约束(设计系统、间距、必须重用的组件)\n- 状态规则(状态在哪儿,什么可本地化什么应共享)\n- 导航规则(路由、deep link、返回行为)\n- 输出规则(要修改的文件、要更新的测试、在 diff 中需要解释的点)
这能降低助手在功能中途自创模式的概率。
定义“小”,让 diff 可预测
选一个容易在代码审查时强制执行的“小”的定义。例如限制每次迭代修改的文件数,并把 UI 重构与行为改动分开。
一个简单规则集:\n\n- 每次迭代改动不超过 3–5 个文件\n- 每次迭代只新增一个 widget 或一个导航步骤\n- 不在同一循环中引入新的状态管理方案\n- 每次变更在进入下一次迭代前必须能编译并运行
添加检查点以便你能快速撤销错误步骤。至少在重大重构前打标签或保持本地检查点。如果你的工作流支持快照/回滚,请积极使用。
如果你想要一个能在聊天中端到端生成并精炼 Flutter 应用的工作流,Koder.ai 提供了一个规划模式,能在应用变更前帮助你审查计划与预期文件变动。
常见问题
How do I keep a Flutter UI iteration small enough to review?
先写一个小而可测试的 UI 规范。用 3–6 行说明:
- 会出现什么(关键控件/组件)
- 点击会做什么(一个主要交互)
- 加载/错误/空状态的表现
- 如何在 30 秒内验证完成度
然后只实现该切片(通常是一个屏幕 + 1–2 个小组件)。
What’s the best way to turn a user story into a buildable UI spec?
把用户故事拆成四类:
- Screens(屏幕): 哪些会改变,哪些保持不变
- Components(组件): 哪些新控件出现,它们放在哪
- States(状态): loading、empty、error、success(每种展示什么)
- Events(事件): 点击、返回、重试、刷新、表单编辑
如果你不能快速描述验收检查项,那说明故事对 UI 来说仍然太模糊,难以产出干净的差异。
What should I ask an AI assistant for first: code or structure?
先只生成widget 树大纲(名字 + 层级 + 每部分展示什么),不要生成代码。
接着要求组件责任分解(每个 widget 负责什么)。
只有在树和责任明确后再生成无状态的脚手架(构造函数参数明确的 stateless widgets),样式单独作为一轮变更。
Why does the “one big prompt” approach usually create messy diffs?
把规则当做硬限制:每次迭代只做一件事。
- 迭代 A:widget 树 / 布局
- 迭代 B:状态接入
- 迭代 C:导航接入
如果一次提交同时改变布局、状态和路由,审查者无法判断哪个改动导致了问题,回滚也会变得混乱。
How do I add state without bloating my widget code?
让 widget 保持“哑”的角色:它们应该渲染状态,而不是做业务决策。
一个实用默认做法:
- 创建一个 controller/view-model,负责事件与异步工作
- 暴露一个单一状态对象(loading/empty/error/success)供 UI 渲染
- UI 通过回调(retry、submit、toggle)通知 controller
切记不要把异步调用放在 build() 中——那会在重建时重复触发。
Which UI states should I plan for on most screens?
在编码前用自然语言定义状态与转换。常见模式:
- Loading: 显示 spinner / 骨架屏
- Empty: 显示信息 + 动作(例如重试)
- Error: 显示内联错误 + 重试
- Success: 渲染内容
然后列出推动状态变化的事件(刷新、重试、提交、编辑)。这样代码更容易与书面规则对照,评审时也更清晰。
How do I keep navigation flows from getting scattered and inconsistent?
为用户故事写一个小型“流图”:
- Entry(入口): 用户从哪里进入(哪个 Tab、通知或 deep link)
- Next(下一步): 主要的前进路径是什么
- Cancel(取消): 放弃时去哪里
- Back(返回): 返回时应保留或丢弃什么
- Fallback(兜底): 必要数据缺失时的去向
同时锁定在屏幕之间传递的参数(IDs、筛选条件、草稿数据),防止把上下文藏进全局单例。
What folder structure helps keep UI changes contained?
采用 feature-first 的目录结构可以把变更限制在特性范围内。例如:
lib/features/<feature>/screens/lib/features/<feature>/widgets/lib/features/<feature>/state/lib/features/<feature>/routes.dart
把每次迭代集中在一个 feature 文件夹内,避免顺手做别处的重构。
How do I make my Flutter UI more modular without over-engineering it?
一个简单规则:先稳定接口,再允许内部变化。
- 公共 widget 的 props 保持小且有类型
- 优先传递值 + 回调,而非读取全局状态
- 路由参数明确(通常一个参数对象比多个可选字段更清晰)
- 当一个 UI 片段不再频繁改动时,把它抽成可复用组件
评审者更关注的是输入/输出是否稳定,而不是布局细节如何变化。
What’s a quick pre-commit checklist for a safe UI iteration?
提交前做两分钟检查:
- 能否触发并检查 loading、empty、error、success,它们是否看起来可接受?
- 返回(back)是否回到预期位置?有没有奇怪的跳转?
- 本次变更是否只影响了一小组明确负责的文件?有没有顺手改动?
- 是否有临时标记或占位资源可能在以后导致问题?
如果你的工作流支持快照/回滚,重大布局重构前请先保存快照以便快速恢复。