1 分钟

使用 Claude Code 进行 Flutter UI 迭代:实用工作流程

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

使用 Claude Code 进行 Flutter UI 迭代:实用工作流程

问题:快速的 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 树计划再生成代码。

  1. 先只要求生成 widget 树大纲。你想要的是 widget 名称、层级结构以及每一部分展示的内容。不要代码。这一阶段你可以发现缺失的状态、空屏或奇怪的布局选择,修改代价也很小。

  2. 要求组件职责分解。保持每个 widget 的单一关注点:一个 widget 渲染头部,另一个渲染列表,另一个处理空/错展示。如果将来需要状态,在此标注但先不用实现。

  3. 生成屏幕脚手架和无状态 widget。先从单文件屏幕开始,放占位内容和明确的 TODO。保持输入显式(构造函数参数),以便之后接入真实状态时无需重写树。

  4. 单独一轮处理样式与布局细节:间距、排版、主题和响应式行为。把样式作为独立的 diff 以便审查简单。

一个有效的提示模板

把约束放在最前面,阻止助手发明无法交付的 UI:

  • 目标设备(仅手机、也包括平板、方向)
  • 设计约束(Material 3、现有主题颜色、间距规则)
  • 导航期望(返回行为、deep link(若有))
  • 验收标准(必须可见和可点击的项)
  • 现有代码边界(必须保留的文件/组件名称约定)

具体示例:用户故事是 "As a user, I can review my saved items and remove one." 先要求一个包含 app bar、带行的列表和空状态的 widget 树。然后请求像 SavedItemsScreenSavedItemTileEmptySavedItems 这样的分解。仅在确认后再生成用假数据填充的 stateless scaffold,最后再单独改样式(分割线、内边距和显眼的删除按钮)。

在不膨胀 UI 代码的情况下加入状态处理

让迭代成为习惯
用基于聊天的 vibe-coding 工作流把 Web、Server 和移动应用端到端构建成习惯。

当每个 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(productIdorderId)、筛选条件(日期范围、状态)、草稿数据(部分填写的表单)。若跳过这一步,你会把状态塞到全局单例或为“查找”上下文而重建屏幕。

即便不在第一天就发布 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) 保持稳定。然后在屏幕内部迭代。布局稳定后,把界面拆成 AddressFormAddressSummarySaveBar

如果状态处理发生变化(例如校验从 widget 移到 CheckoutController),审查仍然清晰:UI 文件主要变化在渲染层,而 controller 在一个地方展示逻辑变更。

与 AI 助手迭代时常见错误与陷阱

轻松分享测试构建
准备好分享时,将你的应用放到自定义域名下。

最快速却最伤进度的方式是让助手一次改全局。如果一次提交同时触及布局、状态与导航,审查者无法判断出错的来源,回滚也会变得混乱。

更安全的习惯是:每次迭代只带一个意图——先成型 widget 树,然后接入状态,最后连接导航。

会造成杂乱代码的错误

一个常见问题是让生成的代码在每个屏幕上引入不同的模式。如果一个页面用 Provider,下一页用 setState,第三页又引入自定义 controller,整个应用会迅速变得不一致。挑一小套模式并强制执行它们。

另一个错误是在 build() 里直接做异步工作。快速 demo 时看起来没问题,但会在重建时重复调用,造成闪烁与难以追踪的 bug。把调用移到 initState()、view model 或专门的 controller 中,让 build() 专注于渲染。

命名是一个安静的陷阱。能编译但读起来像 Widget1data2temp 的代码会让未来重构变得痛苦。清晰的命名也能帮助助手在后续修改时产生更符合意图的代码。

防止最糟结果的护栏:\n\n- 每次迭代只改布局、状态或导航中的一个\n- 在功能范围内重用相同的状态模式\n- 不要在 build() 里做网络或数据库调用\n- 在添加更多功能前先重命名占位符\n- 优先提取 widget,而不是增加嵌套层级

嵌套陷阱

一个典型的视觉修复是不断加 ContainerPaddingAlignSizedBox 来达到预期效果。几轮下来,树会变得难以阅读。

如果一个按钮位置不对,先尝试删掉无用的包装、用单一父布局控件,或提取一个小部件并给它自己的约束,而不是不断套娃。

示例:结账页中总价在加载时抖动。助手可能会多包几层 widget 来“稳定”它。更干净的做法是用简单的占位符保留空间,同时保持行结构不变。

提交下一个 UI 迭代前的快速清单

提交前做两分钟检查,确认用户价值并保护你免受意外回归。目标不是完美,而是确保本次迭代易于审查、易于测试并且易于回退。

提交就绪清单

阅读用户故事一次,然后在运行的应用上(或至少在简单的 widget 测试中)核对:

  • widget 树是否匹配故事: 验收标准中的关键元素存在且可见。文本、按钮和空白都是有意安排的。
  • 所有状态都能触发: Loading、Error、Empty 状态不仅在代码里有注释,你能触发并看到它们(即使用临时 debug 标志)。
  • 导航与返回行为合理: 返回到预期屏幕、对话框能正常关闭、deep link(如用到)落点合理。
  • diff 保持小且有归属: 变更限定在少数组文件并有明确责任。没有顺手改动的重构。
  • 回滚干净: 回退此提交后,其他屏幕仍能编译运行。移除临时标志或占位资源以免日后出问题。

一个现实检查:如果你新增了订单详情屏,你应能(1)从列表打开它,(2)看到加载 spinner,(3)模拟错误,(4)看到空订单,以及(5)按返回回到列表且没有奇怪跳转。

如果你的工作流支持快照/回滚,在较大布局改动前先拍快照会很有帮助。一些平台(例如 Koder.ai)支持此类功能,能让你在不把主分支置于风险下的情况下更快迭代。

一个现实示例:三个迭代把用户故事变成屏幕

构建下一个 UI 切片
将单个用户故事转为可审阅的 Flutter 屏幕,结合聊天与明确计划。

用户故事:"作为购物者,我可以浏览商品、打开详情页、把商品保存到收藏,稍后查看我的收藏。" 目标是在三次小而可审查的步骤中把文字变成屏幕。

迭代 1: 只关注浏览列表屏。创建足够渲染的 widget 树,但不接真实数据:带 AppBarScaffold,一个占位行的 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)是否回到预期位置?有没有奇怪的跳转?
  • 本次变更是否只影响了一小组明确负责的文件?有没有顺手改动?
  • 是否有临时标记或占位资源可能在以后导致问题?

如果你的工作流支持快照/回滚,重大布局重构前请先保存快照以便快速恢复。

Related posts