BLOG · #Engineering

参考方案:用 Flutter 做一个行情与自选 MVP

给小团队的一套具体、可直接开工的架构:建什么、按什么顺序建、刻意不建什么,以及每个重家伙究竟要等到哪个被测量到的信号出现才配被引入。

English version: A reference design: quotes and watchlist MVP in Flutter。这是交易类 App 架构系列的收尾篇。

前面每一篇都在论证一种机制。这一篇给出一个明确的方案:一个带搜索、自选和图表的行情应用,由两三个人建造,交易功能稍后再加。

“给出明确方案”这件事本身有意义。写“视情况而定”很容易,真正有用的产出是一个具体计划、得出它的推理,以及一份明确写出的“拒绝建造什么”。

诚实地写下约束

在选任何东西之前,先把总览里那六个量级,针对这个产品回答一遍:

约束我们的量级
新鲜度一秒一次行情,可见标的几百只
不可逆v1 没有,只读
目录约 15 万只标的,需离线搜索
平台iOS 加 Android,一个小团队
团队一个
威胁公开行情数据;用户账户在 v2

六条里有五条都很小。这就决定了架构的大部分,而且决定的方向是更少。

方案

WebSocket ──▶ 解码 (Dart) ──▶ FrameCoalescer ──▶ QuoteStore ──▶ 叶子组件
                                                       │
REST ──▶ 目录增量同步 ──▶ SQLite(打包快照)──▶ 搜索

传输。 一条 WebSocket 走实时行情,其余走 REST。在这个速率下 JSON 完全够用;二进制编码是以后的优化,不是起步时的决定。

接收。 一开始就在 UI isolate 上解码。每秒几百条消息,耗时是微秒级的。等性能剖析说该挪的那天再挪到 isolate,不要提前。

合并。 状态那篇里的 FrameCoalescer 放在套接字和 store 之间。即使速率不高,这一个也值得在第一天就写:它只有三十行,却能永久地让渲染成本与数据速率无关。

状态。 QuoteStore 里按标的分的 ValueNotifier,叶子各自订阅。页面级状态你想用什么都行,粒度纪律只在热路径上才要紧。

目录。 预构建的 SQLite 文件作为资源发布,首次运行拷贝,按版本增量同步,用带拼音和首字母列的 FTS 搜索,见离线那篇。

图表。 CustomPainter,静态层和实时层放在各自的重绘边界里。如果现成库够用就先用库;等你需要指标副图或画线工具时再自己写。

结构。 一个 package,目录清晰。不搞 monorepo,理由见下。

我们刻意不建什么

这份清单才是这个方案真正的内容。任何一项都可以后加,纪律在于顺序。

  • 不写原生代码,不用 FFI。 一个 1 Hz 的数据源,不足以支撑一条要你付出调用栈完整性和两套工具链的边界。
  • 不做自研二进制协议。 在报文体积或解析耗时被测量出问题之前,就用 WebSocket 上的 JSON。
  • 不做共享跨平台核心。 一份 Flutter 代码已经覆盖两个平台;共享领域层解决的是我们没有的问题。
  • 不做一功能一 package 的 monorepo。 一个团队一个应用,package 边界在这里是开销,不是强制力。
  • 不做 add-to-app。 没有可嵌入的宿主。
  • 不做证书固定。 数据是公开的,而证书固定是一项我们目前还兑现不了的运维承诺。把标准 TLS 做扎实。
  • 不做订单状态机。 因为没有订单。等它们到来时,那一篇就从选读变成必读。

每一行都是可以撤销的决定。写下来的意义正在于此:每一项都会有人提议,而答案应该是“还不到时候,以及什么会让我改变主意”。

建造顺序

第一阶段,目录与搜索。 发布快照,做出搜索,列出标的。此时还没有实时数据。这样做是为了在任何东西依赖它之前,先验证那个最大、最难回头的资产:数据库和它的同步。

第二阶段,实时行情。 WebSocket、合并器、按标的的 notifier、自选页。至此响应式链路端到端打通。从一开始就把带退避的重连和重新订阅写进去;事后补重连非常痛苦,因为它会牵动每一个订阅点。

第三阶段,图表。 走 REST 拉历史,自绘,两层。这是第一个光栅线程性能真正要紧的界面,也是你第一次该在中端设备上做性能剖析的地方。

第四阶段,账户与交易。 先做认证,然后是完整的订单可靠性设计:发送前落盘的幂等键、未知态、重连时对账。这一阶段会改变威胁模型,所以安全那篇从它的第一个提交起就适用。

一到三阶段没有不可逆动作,这正是它们可以快的原因。第四阶段是“谨慎”不再可选的地方,它值得一个更慢的节奏。

升级路径

这个方案真正的质量,不在于它现在能做什么,而在于它能在哪里生长而不必重写。每个重家伙都有明确的接缝和一个被测量到的触发条件:

元素插在哪里何时引入
解码用 isolate数据源接口之后UI 线程剖析显示解码已占用帧预算
二进制协议数据源内部的编解码器报文体积或解析耗时被测出问题
原生 / FFI 接收替换数据源实现Dart 里的合并跟不上了;在这个速率下大概永远不会
功能 package从目录里抽出来两个人反复冲突,或某一层需要隔离测试
共享领域核心抽出 domain/出现第二个不允许与之分叉的客户端
订单机制新模块第一个不可逆动作上线时

需要你今天就刻意设计的,是那些接缝:把数据源暴露为接口而不是一个具体的套接字类、目录访问放在仓储之后、业务规则不写进组件。这些现在都不花钱,而正是它们让未来的改动变成一次抽取,而不是一次重写。

唯一值得记住的事

如果读者只从这个系列里带走一个想法,那就是:在选择任何东西之前,先写下你的六个量级;然后每个重家伙都等到某次测量要求它的那天再引入。

大型架构并不更先进。它们是同样的约束在满量级下的样子,外加多年累积的迁移中间态。过早照抄它,意味着你付了它的协调成本,却完全没有它的规模。这是整个主题里最常见、也最昂贵的错误。

参考

API 与渲染器相关断言按 Flutter 3.47.0 / Dart 3.13(2026 年 8 月)核对。


上一篇:Flutter 工程结构 · 返回总览。