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

快速的 Flutter UI 开发通常开局不错。你调整布局、增加按钮、移动字段,屏幕很快变得更好。但经过几轮修改后,速度会变成一堆没人愿意审查的改动,问题就出现了。
团队通常会遇到相同的失败模式:
一个主要原因是“一个大提示”的方法:描述整个功能,要求生成全部屏幕并接受大量输出。助手本想帮忙,但同时触及太多代码部分,使得改动混乱、难以审查、合并有风险。
可重复的循环通过强制清晰并限制影响范围来解决这个问题。不要一次性“构建整个功能”;改为重复做以下事情:挑选一个用户故事,生成能验证该故事的最小 UI 切片,只添加该切片所需的状态,然后为一条路径接入导航。每次迭代都足够小,易于审查,错误也容易回滚。
目标是提供一套实用工作流程,能把用户故事变成具体屏幕、状态处理与导航流,同时保持可控性。做好之后,你会得到模块化的 UI 片段、更小的 diff,以及在需求变更时更少的意外情况。
用户故事是写给人的,不是写给 widget 树的。在生成任何代码之前,把故事转换成一个小的 UI 规范,描述可见行为。“完成”应该是可测试的:关注用户能看到、能点击、并能确认的内容,而不是设计“感觉现代”与否。
让范围具体化的一个简单方法是把故事拆成四个桶:
如果故事仍然模糊,用简单语言回答这些问题:
提前添加约束可以指导每一个布局选择:主题基础(颜色、间距、排版)、响应式(先做手机纵向,再考虑平板宽度)以及可访问性的最低要求,比如点击目标大小、文本缩放可读性和图标的语义标签。
最后,决定哪些是稳定的、哪些是可变的,这样就不会让代码库发生频繁震荡。稳定项是其他功能所依赖的东西,如路由名、数据模型和现有 API。可变项更安全去迭代,例如布局结构、微文案和具体的 widget 组合。
示例:"作为用户,我可以在详情页把一项保存到收藏。" 一个可构建的 UI 规范可能是:
这些足够构建、审查并迭代,而不需要猜测。
小的 diff 并不是要求你慢下来。它们让每次 UI 改动都容易审查、容易撤销、不容易破坏。最简单的规则:每次迭代只做一个屏幕或一个交互。
在开始前先选好一个紧凑的切片。"给 Orders 屏添加空状态" 是个好切片。"重构整个 Orders 流程" 则不是。目标是做出一个队友一分钟内能看懂的 diff。
一个稳定的文件夹结构也能帮助你把改动控制在小范围内。简单的 feature-first 布局防止你把 widgets 和 routes 四处散落:
lib/
features/
orders/
screens/
widgets/
state/
routes.dart
保持 widget 小而可组合。当一个 widget 有明确的输入和输出时,你可以在不碰状态逻辑的情况下改变布局,也可以在不重写 UI 的情况下改变状态。优先使用接受普通值和回调的 widget,而不是直接依赖全局状态。
一个保持可审查的循环:
设定一个硬规则:每次变更都必须易于回退或隔离。迭代屏幕时避免顺手做大范围重构。如果你发现无关的问题,把它们记下来并在单独的提交中修复。
如果你的工具支持快照和回滚,把每个切片当作一个快照点。有些 vibe-coding 平台(例如 Koder.ai)包含快照与回滚功能,在尝试大胆 UI 变更时能让试验更安全。
另一个让早期迭代保持平静的习惯是:优先新增 widget,而不是修改共享组件。共享组件是小改动变成大 diff 的地方。
如果把思考和敲代码混在一起,快速的 UI 开发会出问题。先拿到清晰的 widget 树计划再生成代码。
先只要求生成 widget 树大纲。你想要的是 widget 名称、层级结构以及每一部分展示的内容。不要代码。这一阶段你可以发现缺失的状态、空屏或奇怪的布局选择,修改代价也很小。
要求组件职责分解。保持每个 widget 的单一关注点:一个 widget 渲染头部,另一个渲染列表,另一个处理空/错展示。如果将来需要状态,在此标注但先不用实现。
生成屏幕脚手架和无状态 widget。先从单文件屏幕开始,放占位内容和明确的 TODO。保持输入显式(构造函数参数),以便之后接入真实状态时无需重写树。
单独一轮处理样式与布局细节:间距、排版、主题和响应式行为。把样式作为独立的 diff 以便审查简单。
把约束放在最前面,阻止助手发明无法交付的 UI:
具体示例:用户故事是 "As a user, I can review my saved items and remove one." 先要求一个包含 app bar、带行的列表和空状态的 widget 树。然后请求像 SavedItemsScreen、SavedItemTile、EmptySavedItems 这样的分解。仅在确认后再生成用假数据填充的 stateless scaffold,最后再单独改样式(分割线、内边距和显眼的删除按钮)。
当每个 widget 都开始做决策时,UI 迭代就会失控。让 widget 保持“哑”:它们读取状态并渲染,而不是包含业务规则。
先用自然语言命名状态。大多数功能需要比“loading”和“done”更多的状态:
然后列出会改变状态的事件:点击、表单提交、下拉刷新、返回、重试和“用户编辑字段”。提前写这些可以避免后续的猜测。
为该功能选择一种状态处理方式并坚持使用。目标不是“最好的模式”,而是能带来一致的 diff。对于小屏幕来说,简单的 controller(ChangeNotifier 或 ValueNotifier)通常足够。把逻辑放在一个地方:
在加入代码前,用简单的英语写出状态转换。例如登录屏:\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,也要决定用户中途进入时发生什么:你能否加载缺失数据,还是应该重定向到安全的入口屏?\n\n还要决定哪些屏应返回结果。例如:Select Address 屏返回一个 addressId,结账页在接收后无需完整刷新即可更新。保持返回值简单且有类型,这样改动也更易审查。
编码前列出边缘情况:未保存更改(显示确认对话框)、需要鉴权(登录后暂停并恢复)、缺失或被删除的数据(显示错误并提供明确出口)。
快速迭代时真正的风险不是“UI 错了”,而是“无法审查的 UI”。如果队友无法看出改变了什么、为什么改、哪些保持不变,那么每次后续迭代都会更慢。
一个有帮助的规则:先锁定接口,然后允许内部移动。稳定公共 widget 的 props(输入)、简短的 UI 模型和路由参数。一旦这些被命名和类型化,你就可以在不破坏其他部分的情况下重塑 widget 树。
在生成代码前先要一个对 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 在一个地方展示逻辑变更。
最快速却最伤进度的方式是让助手一次改全局。如果一次提交同时触及布局、状态与导航,审查者无法判断出错的来源,回滚也会变得混乱。
更安全的习惯是:每次迭代只带一个意图——先成型 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 来“稳定”它。更干净的做法是用简单的占位符保留空间,同时保持行结构不变。
提交前做两分钟检查,确认用户价值并保护你免受意外回归。目标不是完美,而是确保本次迭代易于审查、易于测试并且易于回退。
阅读用户故事一次,然后在运行的应用上(或至少在简单的 widget 测试中)核对:
一个现实检查:如果你新增了订单详情屏,你应能(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 中需要解释的点)
这能降低助手在功能中途自创模式的概率。
选一个容易在代码审查时强制执行的“小”的定义。例如限制每次迭代修改的文件数,并把 UI 重构与行为改动分开。
一个简单规则集:\n\n- 每次迭代改动不超过 3–5 个文件\n- 每次迭代只新增一个 widget 或一个导航步骤\n- 不在同一循环中引入新的状态管理方案\n- 每次变更在进入下一次迭代前必须能编译并运行
添加检查点以便你能快速撤销错误步骤。至少在重大重构前打标签或保持本地检查点。如果你的工作流支持快照/回滚,请积极使用。
如果你想要一个能在聊天中端到端生成并精炼 Flutter 应用的工作流,Koder.ai 提供了一个规划模式,能在应用变更前帮助你审查计划与预期文件变动。
先写一个小而可测试的 UI 规范。用 3–6 行说明:\n\n- 会出现什么(关键控件/组件)\n- 点击会做什么(一个主要交互)\n- 加载/错误/空状态的表现\n- 如何在 30 秒内验证完成度\n\n然后只实现该切片(通常是一个屏幕 + 1–2 个小组件)。
把用户故事拆成四类:\n\n- Screens(屏幕): 哪些会改变,哪些保持不变\n- Components(组件): 哪些新控件出现,它们放在哪\n- States(状态): loading、empty、error、success(每种展示什么)\n- Events(事件): 点击、返回、重试、刷新、表单编辑\n\n如果你不能快速描述验收检查项,那说明故事对 UI 来说仍然太模糊,难以产出干净的差异。
先只生成widget 树大纲(名字 + 层级 + 每部分展示什么),不要生成代码。\n\n接着要求组件责任分解(每个 widget 负责什么)。\n\n只有在树和责任明确后再生成无状态的脚手架(构造函数参数明确的 stateless widgets),样式单独作为一轮变更。
把规则当做硬限制:每次迭代只做一件事。\n\n- 迭代 A:widget 树 / 布局\n- 迭代 B:状态接入\n- 迭代 C:导航接入\n\n如果一次提交同时改变布局、状态和路由,审查者无法判断哪个改动导致了问题,回滚也会变得混乱。
让 widget 保持“哑”的角色:它们应该渲染状态,而不是做业务决策。\n\n一个实用默认做法:\n\n- 创建一个 controller/view-model,负责事件与异步工作\n- 暴露一个单一状态对象(loading/empty/error/success)供 UI 渲染\n- UI 通过回调(retry、submit、toggle)通知 controller\n\n切记不要把异步调用放在 build() 中——那会在重建时重复触发。
在编码前用自然语言定义状态与转换。常见模式:\n\n- Loading: 显示 spinner / 骨架屏\n- Empty: 显示信息 + 动作(例如重试)\n- Error: 显示内联错误 + 重试\n- Success: 渲染内容\n\n然后列出推动状态变化的事件(刷新、重试、提交、编辑)。这样代码更容易与书面规则对照,评审时也更清晰。
为用户故事写一个小型“流图”:\n\n- Entry(入口): 用户从哪里进入(哪个 Tab、通知或 deep link)\n- Next(下一步): 主要的前进路径是什么\n- Cancel(取消): 放弃时去哪里\n- Back(返回): 返回时应保留或丢弃什么\n- Fallback(兜底): 必要数据缺失时的去向\n\n同时锁定在屏幕之间传递的参数(IDs、筛选条件、草稿数据),防止把上下文藏进全局单例。
采用 feature-first 的目录结构可以把变更限制在特性范围内。例如:\n\n- lib/features/<feature>/screens/\n- lib/features/<feature>/widgets/\n- lib/features/<feature>/state/\n- lib/features/<feature>/routes.dart\n\n把每次迭代集中在一个 feature 文件夹内,避免顺手做别处的重构。
一个简单规则:先稳定接口,再允许内部变化。\n\n- 公共 widget 的 props 保持小且有类型\n- 优先传递值 + 回调,而非读取全局状态\n- 路由参数明确(通常一个参数对象比多个可选字段更清晰)\n- 当一个 UI 片段不再频繁改动时,把它抽成可复用组件\n\n评审者更关注的是输入/输出是否稳定,而不是布局细节如何变化。
提交前做两分钟检查:\n\n- 能否触发并检查 loading、empty、error、success,它们是否看起来可接受?\n- 返回(back)是否回到预期位置?有没有奇怪的跳转?\n- 本次变更是否只影响了一小组明确负责的文件?有没有顺手改动?\n- 是否有临时标记或占位资源可能在以后导致问题?\n\n如果你的工作流支持快照/回滚,重大布局重构前请先保存快照以便快速恢复。