<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Flutter on Trevor's Blog</title><link>http://hitrevor.com/tags/flutter/</link><description>Recent content in Flutter on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Fri, 13 Nov 2026 08:11:41 +0800</lastBuildDate><atom:link href="http://hitrevor.com/tags/flutter/index.xml" rel="self" type="application/rss+xml"/><item><title>同一段 C 代码，Flutter 和 KMP 各踩五个坑</title><link>http://hitrevor.com/posts/interop/</link><pubDate>Fri, 13 Nov 2026 08:11:41 +0800</pubDate><guid>http://hitrevor.com/posts/interop/</guid><description>Flutter 官方博客在 2026 年 11 月 12 日发了篇公告，作者 Mariam Hasnany，说 FFIgen 的 Obj-C 支持和 JNIgen 都转正了。博客原文写道，Pigeon 底层一直在用这两套机制，只是现在才把它们摆到台面上。</description><content:encoded><![CDATA[<p>Flutter 官方博客在 2026 年 11 月 12 日发了篇公告，作者 Mariam Hasnany，说 FFIgen 的 Obj-C 支持和 JNIgen 都转正了。博客原文写道，Pigeon 底层一直在用这两套机制，只是现在才把它们摆到台面上。</p>
<p>我当天下午就去试。</p>
<p>第一步就卡住：最新版 ffigen 装不进我的工程。</p>
<p>这不是官方说错了什么。转正说的是这套方案成熟了，跟我这台机器上的 Flutter 版本能不能装最新包，是两件事。但这两件事叠在一起，恰好是所有人上手时会遇到的第一堵墙。</p>
<h2 id="公告说-stable装的时候报错了">公告说 stable，装的时候报错了</h2>
<p>我本机是 Flutter 3.41.4，Dart 3.11.1。</p>
<p>pubspec 里写 ffigen 23.0.0，pub get 直接失败，报的是一长串依赖推导：ffigen 22 以上依赖 code_assets，code_assets 依赖 hooks，hooks 依赖 record_use，record_use 要 meta 1.19.0 以上，而 Flutter SDK 把 meta 钉死在 1.17.0，六个包串成一条链，断在第四环，报错信息整整一屏，末行才写着降级建议。</p>
<p>链条到这儿就断了。降级建议是装 ffigen 21。</p>
<p>我把版本改成 21，秒过。</p>
<p>pub.dev 上 ffigen 的版本页写着 23.0.0，发布日期 10 月 6 日。能装和最新，是两回事。</p>
<p>所以第一件事不是「怎么用」，是「你得先确认自己能不能装」。</p>
<h2 id="那就退一个版本">那就退一个版本</h2>
<p>ffigen 21 能不能覆盖这次公告说的能力，我没法从版本号上判断。官方描述指出的是这套方案的设计取舍，通篇没提最低版本要求。</p>
<p>但我要测的东西不需要 JNIgen，也不需要绑 Obj-C。我要的是一个最基础的场景：一段 C 代码，两边各自绑一遍，看谁更省事。</p>
<p>C 就够。C 是两边唯一没有分歧的交集。</p>
<h2 id="先写一段两边都能用的-c">先写一段两边都能用的 C</h2>
<p>我写了 34 行 C 加 17 行头文件，四个函数，故意挑了四种数据形状。</p>
<p>一个纯计算，传入数组返回和，用来量调用开销。</p>
<p>一个返回结构体，结构体里带一个 64 字节的内联字符数组。这是最容易出事的形状。</p>
<p>一个往调用方给的缓冲区里写字符串。谁分配内存，两边态度不一样。</p>
<p>一个返回 const char 指针。</p>
<p>这四个函数在 C 层先跑通，输出 sum=10、value=-7 negative、filled:42、interop-lab 1.0。后面两边绑完，必须复现同一组值，一个字符都不能差。</p>
<p><img alt="一段 C 代码，两条绑定路径" loading="lazy" src="/images/interop/fig1.png"></p>
<h2 id="flutter-这条路一个-yaml-出全部绑定">Flutter 这条路：一个 yaml 出全部绑定</h2>
<p>Flutter 侧我只写了 15 行配置和 34 行调用代码。</p>
<p>配置就是一个 yaml，告诉 ffigen 头文件在哪、输出到哪个 dart 文件、哪些函数要、哪些结构体要。</p>
<p>跑起来 0.76 秒，生成 457 行 Dart。</p>
<p>然后 dart run，四条调用全部正确。</p>
<p>但这里有个细节值得说。</p>
<p>457 行里，真正跟我的接口有关的只有 23 行。剩下 434 行是 Darwin 系统头的产物，因为我的头文件 include 了 stdint.h，ffigen 一路展开，把 pthread、mbstate_t 这些全生成了出来，连同它们依赖的一大串内部结构。</p>
<p>能读。但没什么用。</p>
<p><img alt="手写行数与生成代码" loading="lazy" src="/images/interop/fig2.png"></p>
<h2 id="kmp-这条路四行-def-加一个任务">KMP 这条路：四行 def 加一个任务</h2>
<p>KMP 侧要三样东西。</p>
<p>一个 def 文件，四行：头文件路径、编译选项、链接选项、包名。</p>
<p>一个 gradle 配置，二十二行：定义 macosArm64 目标、创建 cinterop、指定 def 文件。</p>
<p>还有 commonMain 里四条 expect 声明，以及 macosArm64Main 里对应的四条 actual 实现，四十二行。</p>
<p>cinterop 生成 1.4 秒。链接可执行文件，debug 2.8 秒，release 5.7 秒。</p>
<p>跑起来的结果跟 C 层、跟 Dart 侧完全一致。</p>
<p>四个函数、三套实现、同一组输出。这一步过了，后面的对比才有意义。如果连值都对不上，比构建时间就是瞎比。</p>
<h2 id="构建kmp-慢四倍产物小十倍">构建：KMP 慢四倍，产物小十倍</h2>
<p>生成绑定，Flutter 0.76 秒，KMP 1.4 秒。</p>
<p>编译到能跑，Dart AOT 1.3 秒，KMP release 5.7 秒。KMP 慢四倍多。</p>
<p>但产物反过来。Dart 编译出的可执行文件 5.3 MB，KMP 的 kexe 只有 532 KB。</p>
<p>十倍。这不是谁优化得好，是运行时要不要整个装进去。Dart 那个二进制里带着整个 Dart 运行时，KMP 只带用到的部分。</p>
<p>KMP 的 debug 版是 1.3 MB，也不大，但那是因为它没优化，不是因为小得漂亮。</p>
<p><img alt="构建耗时与产物体积" loading="lazy" src="/images/interop/fig3.png"></p>
<h2 id="运行普通调用打平一碰结构体就拉开">运行：普通调用打平，一碰结构体就拉开</h2>
<p>各跑两万次取均值。</p>
<p>普通函数调用，Dart 0.095 微秒，KMP 0.102 微秒，差 7%，基本是噪声。结构体返回，Dart 0.146 微秒，KMP 0.349 微秒，差 2.4 倍。</p>
<p>四类调用全套走一遍，Dart 56 微秒，KMP 129 微秒。</p>
<p>启动更快的是 KMP，22 毫秒三次一致。Dart 是 25、25、29。</p>
<p>所以差距不是全局的。它只在特定形状上冒头：你传数组、传指针、拿字符串，两边一样快。你一让它按值返回结构体，Kotlin 那边就要多付一次 CValue 的拷贝。</p>
<p><img alt="release 对照下的调用开销" loading="lazy" src="/images/interop/fig4.png"></p>
<h2 id="debug-差点让我写错结论">debug 差点让我写错结论</h2>
<p>这里差点翻车。</p>
<p>我第一次量 KMP，用的是 debug 构建。结构体返回那一项，9.817 微秒，而 Dart 侧同一项 0.146 微秒，我当时的草稿已经写下「差 67 倍」。</p>
<p>后来想，debug 是不优化的，拿它跟 AOT 比不成立。换成 release 重新量，同一项变成 0.349 微秒。</p>
<p>28 倍的差距，全在优化开关上。</p>
<p>要是当时就那么发出去了，结论会完全反过来，而且看起来特别有说服力。</p>
<h2 id="五个坑对五个坑">五个坑对五个坑</h2>
<p>Flutter 这边五个。</p>
<p>第一，最新版 ffigen 装不上，被 meta 版本钉死，只能退到 21。</p>
<p>第二，配置里的 entry-points 写相对路径，ffigen 不报错，静默生成一个空绑定文件。我一开始看到 229 字节的输出，还以为是自己配置写错了。改成绝对路径才正常。</p>
<p>第三，结构体里的内联 char 数组，ffigen 生成的是 Array 类型，没有 cast 方法，要取字符串只能逐字节读。</p>
<p>第四，ffi 包里的 Char 和 Utf8 是两种扩展类型，互相之间得来回转。</p>
<p>第五，457 行生成代码里 434 行跟你无关。</p>
<p>KMP 这边也是五个。</p>
<p>第一，首次构建时 cinterop 和编译的任务顺序会撞车，第一次编译必然报符号找不到，第二次就好了。</p>
<p>第二，整个文件要 opt-in ExperimentalForeignApi，方法上逐个标不如文件头标一次。</p>
<p>第三，ByteVar、IntVar 在 kotlinx.cinterop 里，不在 platform.posix 里。我第一版从 posix 导入，全红。</p>
<p>第四，按值返回的结构体在 Kotlin/Native 里是 CValue，不能直接点字段，必须 useContents。</p>
<p>第五，链接完运行时报 dyld 找不到 dylib，得给 DYLD_LIBRARY_PATH 或者自己加 rpath。</p>
<p><img alt="两边各五个坑" loading="lazy" src="/images/interop/fig5.png"></p>
<h2 id="两边坑的区别在哪">两边坑的区别在哪</h2>
<p>数量一样，性质不一样。</p>
<p>Flutter 那五个里，有两个是静默的：空绑定不报错，版本墙只在 pub get 那一步拦你一次，你写完代码跑不通，得自己去猜哪里空。KMP 那五个全是编译期报错，一个都躲不过去，但也一个都不用猜，报错凶，改起来反而快。这个差别来自两边绑定的时机：一个在运行期找符号，一个在链接期定死。</p>
<p>如果你更怕「程序不报错但结果不对」，Flutter 这边的坑更难受。如果你更怕反复调构建，KMP 这边更折腾。</p>
<h2 id="该选哪条">该选哪条</h2>
<p>如果目标只有一个平台，而且在 Apple 生态里，KMP 的 cinterop 更省事。绑定在编译期就定死，产物体积小十倍，运行时开销也没输。</p>
<p>但要跨平台，尤其是要落到 Android、Windows、JS 上，cinterop 那条路就断了。它只在 Kotlin/Native 支持的 target 上存在，一到 JVM 就得换成 JNI 那一套。</p>
<p>Flutter 的 ffigen 反而是通用的，同一个 dylib、so、dll，换平台只换加载路径。</p>
<p>这也是我做完之后最想说的一点：这两个东西不是同一层的替代品。cinterop 是 Kotlin/Native 的编译器能力，ffigen 是跨平台的绑定生成器。比快慢没意义，比的是「你的项目形态让哪条路走得通」。</p>
<p>至于那个版本墙，等你哪天想试最新的 ffigen，先看看自己 Flutter 是哪一版，别像我一样先写 pubspec 再发现装不上。</p>
]]></content:encoded></item><item><title>AI 都能写 Swift 和 Kotlin 了，跨端图什么</title><link>http://hitrevor.com/posts/flutter-ai-scale/</link><pubDate>Fri, 23 Oct 2026 14:31:26 +0800</pubDate><guid>http://hitrevor.com/posts/flutter-ai-scale/</guid><description>9 月 10 日，Shopify 移动端负责人 Mustafa Ali 发了篇博客。</description><content:encoded><![CDATA[<p>9 月 10 日，Shopify 移动端负责人 Mustafa Ali 发了篇博客。</p>
<p>标题很直白，原生现在是 Shopify 移动端的未来。</p>
<p>六年前他们 all-in React Native。去年 1 月，Ali 还写过复盘，说 RN 前途光明。现在收回去，理由一条，coding agent 把写两遍的成本打下来了。</p>
<p>转过去的效果不差。Shop App 六个工程师，12 周从概念验证做到上架。iOS 冷启动快 23%，Android 快 50%。</p>
<p><img alt="Shopify 官方博客里的迁移示意图" loading="lazy" src="/images/flutter-ai-scale/shopify-native.png"></p>
<p>但这段话里最值钱的不是数字。</p>
<p>Ali 自己写，agent 几秒能改完代码，却要花好几分钟才测得出结果。因为模拟器控制太慢，只能靠无障碍树或截图去拿状态。</p>
<p>他的原话是，如果模型没法快速验证自己的工作，模型多好都不管用。</p>
<p>这句抱怨，让我把两篇东西并排读了一遍。Shopify 9 月 10 日这篇，和 Flutter 团队四十天后的回应，争的其实是同一件事。</p>
<h2 id="瓶颈不在生成在验证">瓶颈不在生成，在验证</h2>
<p>Flutter 团队那篇 10 月 20 日发，作者是 Carlos Mendonça 和 Emma Twersky。标题也直白，AI 让迁移变简单，Flutter 让扩张变简单。它要回答的，就是 Shopify 抛出来的那个问题。</p>
<p>全文压在一个判断上，代码生成早就不是瓶颈，验证才是。</p>
<p>两个数据撑着这个判断。73% 的开发者说用 AI 开始任务时更有产出。46% 的人不信任 AI 在没有人工检查时的调试准确性。</p>
<p>两头一夹，卡点在人这边。</p>
<h2 id="三个环节的账">三个环节的账</h2>
<p>官方给了张对照表，把原生和 Flutter 放在一起比验证的三个环节。</p>
<p><img alt="按官方原表重排的中文对照，三行数值与表述未改动" loading="lazy" src="/images/flutter-ai-scale/verify-table.png"></p>
<p>代码到运行的延迟，原生 2 到 5 分钟一次。构建、链接、归档、装模拟器，四轮调试就耗掉 20 多分钟。</p>
<p>Flutter 200 毫秒以内。热重载把改动注入运行中的 Dart VM，状态还留着。</p>
<p>自动化测试，原生要走完整的模拟器生命周期和后台 emulator。Flutter 的 headless widget test 在隔离环境里跑，500 毫秒以内。</p>
<p>界面和状态检查这条最见功夫。</p>
<p>原生截模拟器图喂给视觉模型猜布局，概率性的、慢。Flutter 走 MCP 查 Dart VM Service 和 DevTools，直接读 Widget 树和 RenderObject 树，确定的、结构化的。</p>
<p>三段串起来是一条完整的回路。原生那边每走一段都得排队，Flutter 这边一个来回就几毫秒。</p>
<h2 id="harness-比-prompt-值钱">harness 比 prompt 值钱</h2>
<p>验证快是一回事，怎么让 agent 用上它，是另一回事。官方给了个内部实验，Project One Shot，让 agent 自主修 camera_android_camerax 里的生产 bug。</p>
<p>这插件管着 Android CameraX 的生命周期和 surface texture 绑定，Flutter 里最复杂的那类代码。</p>
<p>给泛泛的指令，比如注意别内存泄漏，agent 全失败。</p>
<p>换成确定性的 harness 才成。AST 分析走 MCP、包级别的 domain skill、强制的测试驱动 guardrails，先用一个失败的测试复现，再写最小修复，收尾跑通全套测试和泄漏检查。</p>
<p>就是不给 agent 自由发挥的空间，把每一步的验收条件钉死。</p>
<p>配套放出三样东西。官方 Dart 和 Flutter agent 插件，把 MCP server 和 10 个 Flutter、14 个 Dart 技能打包，agent 能直接访问 Dart Analyzer 和 Widget Inspector。</p>
<p>Antigravity 里的专用 agent，自动审无障碍问题并提交符合习惯的修复。还有个和 Serverpod 一起做的 Agent Skills CLI，一条 dart run skills@get 把依赖包带的 skill 装进 agent。</p>
<p>基准数据是 Gemini 3.5 Flash 从 84.1% 到 85.5%，Gemini 3.1 Pro 从 89.6% 到 92.7%。涨幅不大，是官方自己拿评估套件跑出来的。</p>
<h2 id="这些能力落到什么地方">这些能力落到什么地方</h2>
<p>官方那篇列了一串落地位置，比手机远得多。丰田和雷克萨斯的车机、小米 SU7 的中控、Nubank 一亿用户的金融 App、Universal 音乐。</p>
<p>文章没往这个方向推。我倒觉得这是它最有说服力的一段。</p>
<p>这些场景的共同点是验证成本高得离谱。</p>
<p>车机刷一次固件几十分钟。金融 App 一次线上事故就是真金白银。音乐 App 的用户对卡顿零容忍。</p>
<p>在这种地方，验证回路短一分钟，都是实打实的钱和时间。</p>
<p>再往远看一点。agent 写代码变成常态之后，一份代码跑多端省的不只是人力，还有时间。</p>
<p>agent 生成一版，热重载到手机、桌面、Web 同时看效果，比一个平台一个平台地编译、打包、装模拟器快一个数量级。</p>
<p>这也是 Flutter 团队这两年往车机、桌面、嵌入式使劲的原因。</p>
<p>跨端的下半场，可能不在手机里。</p>
<h2 id="有几点我不是很同意">有几点我不是很同意</h2>
<p>应用商店评分那段其实经不起推敲。原文引了 Apptopia 对 201,000 个应用的统计。Google Play 上 Flutter 应用平均 4.1 星，非 Flutter 也是 4.1 星。App Store 上都是 4.0 星，结论是用户不关心框架。</p>
<p>这数据只能证明跨端没拖累评分。翻过来也说明它没带来优势。</p>
<p>拿一个打平的结果去论证应该选它，差着一层。</p>
<p>热重载那个 200 毫秒，说的是注入改动的时间，不含改代码、想清楚、切窗口。</p>
<p>它也改不了类的结构。加字段、改状态树还得重启。</p>
<p>官方表里比的是代码到运行的延迟，这个定义是诚实的。但读者容易读成开发速度是原生 20 倍，两件事差得远。</p>
<p>企业案例也有幸存者偏差。</p>
<p>小米 SU7、Universal 砍掉 45% 代码、丰田和雷克萨斯、Nubank 一亿用户，都是真的。</p>
<p>但没有一篇文章会写某公司迁到跨端两年后又拆回去。拿这些论证架构选择，只能算半个证据。</p>
<p><img alt="官方给出的八条论点，拿去团队里吵架够用" loading="lazy" src="/images/flutter-ai-scale/talking-points.png"></p>
<h2 id="跨端的账该怎么算">跨端的账该怎么算</h2>
<p>先把两家的账摆平。Shopify 的判断是，写两遍的成本被 agent 打下来了，剩下那点不值得为了省它放弃原生能力。Flutter 的判断是，协调成本还在。</p>
<p>原文举的例子很具体。同一个结账状态机，同一个模型开两个分身，一个写 Swift 用异步 stream 丢过期事件，一个写 Kotlin 用 reactive flow 把事件排队。</p>
<p>两套实现第一天都过了本地单测。几周后上线，Android 结账转化率掉三个点，原因是网络抖动时重试时序不一样。</p>
<p><img alt="官方原图：并行 agent 不会消除协调成本，而是引入非确定性 drift" loading="lazy" src="/images/flutter-ai-scale/parallel-agent-challenge.png"></p>
<p>两种说法都成立，算的是不同的账。</p>
<p>Shopify 这次转向，我也不觉得该当成&quot;跨端已死&quot;的证据。</p>
<p>它是几百个屏幕、上千工程师、把移动端当核心业务在投的公司。这个体量下为了 23% 的冷启动和崩溃率的改善去重建，账算得过来。</p>
<p>换成五个人维护一个中等规模的 App，同样的事就未必划算。</p>
<p>决定你要不要跨端的，是团队规模、维护年限，还有验证链路的长度。这几样 AI 一个都改不了。</p>
<p>倒是另一个问题值得吵一吵，如果 agent 真能把一轮验证压到几百毫秒，那原生体验更好这条理由，还能撑多久。</p>
<p>想听你们手上的真实情况。你的项目现在是单端、双端还是跨端？当初选它，主导因素是性能、人力，还是那句老板说要原生？</p>
<p>顺带问一句，你觉得 Compose Multiplatform 有戏么？同样是跨端，它比 Flutter 晚进场好几年，但背后站着 Kotlin 和 Android 那半个生态。</p>
<p>评论区说两句，我挑几个有代表性的整理成下篇。</p>
<p>两篇原文都在这儿，加起来二十分钟。</p>
<p><a href="https://shopify.engineering/back-to-native">Native is now the future of mobile at Shopify</a></p>
<p><a href="https://flutter.dev/blog/ai-makes-it-easy-to-migrate-flutter-makes-it-easy-to-scale">AI makes it easy to migrate. Flutter makes it easy to scale.</a></p>
]]></content:encoded></item><item><title>Flutter 流式聊天：三个坑的实测代价</title><link>http://hitrevor.com/posts/flutter-stream/</link><pubDate>Sat, 17 Oct 2026 10:34:47 +0800</pubDate><guid>http://hitrevor.com/posts/flutter-stream/</guid><description>写 AI Chat 客户端的人都绕不过这件事：模型的回答不是一次性给你的，它一个 token 一个 token 往外吐，中间还要插工具调用、插代码块、插一段思考。用户盯着屏幕，其实是在看一棵树一边长一边被人重新摆位置。</description><content:encoded><![CDATA[<h2 id="一条回复流二十分钟界面得跟着长">一条回复流二十分钟，界面得跟着长</h2>
<p>写 AI Chat 客户端的人都绕不过这件事：模型的回答不是一次性给你的，它一个 token 一个 token 往外吐，中间还要插工具调用、插代码块、插一段思考。用户盯着屏幕，其实是在看一棵树一边长一边被人重新摆位置。</p>
<p>GetPullRequest 那个团队最近写了篇长文，讲他们怎么在 Flutter 里把这件事做出来。最扎人的一句是说 Flutter 官方的 GenUI 包：</p>
<blockquote>
<p>It only drew the UI after the model&rsquo;s full response had arrived and been parsed.</p>
</blockquote>
<p>意思是它非得等整段回答收完、解析完才画界面。普通问答等几秒还行，agent 一轮干二十分钟就不行了：工具卡早就该出来了，你还在看菊花。</p>
<p>我顺着这篇帖子在 macOS 上把几个关键场景复现了一遍，量了数。这篇讲三个坑，每个坑都配一个能跑出来的数字。</p>
<h2 id="坑一每个-token-都重解析一遍-markdown">坑一：每个 token 都重解析一遍 Markdown</h2>
<p>最朴素的写法是：来一片文本，把累积起来的全文丢给 Markdown 解析器，拿到节点树再渲染。看起来天经地义，代价是每次都在解析「已经有过的内容」。</p>
<p>我拿一段 886 字符的 agent 回复做样本，里面有散文、一张三行表格、一个 dart 代码块、两层嵌套列表，按 4 字符一片切成 222 片，模拟流式到达。三种做法对比如下。</p>
<table>
	<thead>
			<tr>
					<th>做法</th>
					<th>总耗时</th>
					<th>说明</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>每片都全量重解析</td>
					<td>28 毫秒</td>
					<td>解析 222 次，单次最慢 0.62 毫秒</td>
			</tr>
			<tr>
					<td>只在正在增长的最后一块内重解析</td>
					<td>4 毫秒</td>
					<td>已完成的块只解析一次</td>
			</tr>
			<tr>
					<td>收完再一次性解析</td>
					<td>0.148 毫秒</td>
					<td>就是 GenUI 那种做法的成本</td>
			</tr>
	</tbody>
</table>
<p>28 毫秒听上去不多，但它不是平均分在 222 片上。开头的 50 片平均 0.046 毫秒，最后的 50 片平均 0.199 毫秒，越往后越贵，因为累积的文本越来越长。这就是个平方级的活：886 字符的回复，朴素做法一共处理了约 98K 字符，是原文的 111 倍。</p>
<p>换成真实长度的回答更明显。同一个做法，回复长度翻倍，耗时涨两三倍：</p>
<pre tabindex="0"><code>886 字符 / 222 片：16 ms
1774 字符 / 444 片：61 ms
3550 字符 / 888 片：229 ms
7102 字符 / 1776 片：899 ms
</code></pre><p>一帧的预算是 16.7 毫秒。等到 3550 字符那一档，光解析一次尾部就要吃掉好几毫秒，再叠加布局和绘制，掉帧是必然的。</p>
<p><img alt="同一段回复的三种解析策略" loading="lazy" src="/images/flutter-stream/fig1.png"></p>
<p>那篇帖子的解法是段落级增量：已完成的块只解析一次，只有正在长的那一块反复解析，同时把「每片都通知 UI 刷新」改成「一个微任务合并通知一次」。</p>
<h2 id="逐行解析会把结构打散">逐行解析会把结构打散</h2>
<p>还有个更隐蔽的坑，跟「让用户能按行点选评论」这个产品需求绑在一起：如果你的渲染层是一行一个 widget，每行单独送去解析，Markdown 的结构就散了。</p>
<p>同一段内容，逐行解析出来的标签序列是这样的：</p>
<pre tabindex="0"><code>[p, p, p, p, p, pre, p, p, pre, ul, li, pre, pre]
</code></pre><p>分块解析出来的是这样：</p>
<pre tabindex="0"><code>[p, table, thead, tr, th, th, tbody, tr, td, td, tr, td, td, pre, ul, li, li, li]
</code></pre><p>对一下数：表格逐行解析出 0 个 table（四行表格变成五行带竖线的文字），分块解析出 1 个；代码块逐行解析出 4 个 pre（本该是一个整体），分块解析出 1 个；嵌套列表逐行只剩 1 个 li（本该三层），分块解析出 3 个。</p>
<p>嵌套列表那条最容易漏。Markdown 里行首四个空格的意思是「这是代码」，所以第二层列表一旦被单独拿去解析，它就变成了一段代码块，屏幕上显示成一个灰块，前面还挂着一个裸的减号。</p>
<p><img alt="逐行解析与分块解析的结构差异" loading="lazy" src="/images/flutter-stream/fig2.png"></p>
<p>顺带一个更基础的坑：markdown 这个包的表格要显式开扩展，默认的 <code>Document()</code> 解析出来是 0 个 table，换成 <code>ExtensionSet.gitHubWeb</code> 才是 1 个。</p>
<p>还有个流式特有的：一片文本可能在三个反引号中间断掉。实测把这样半截的文本送去解析，尾部一个或两个反引号会直接变成正文里的字面字符，屏幕上先跳出两个孤零零的 <code>`</code>，下一片到了才变成一个正常代码块。而如果恰好是完整的三个反引号、内容还没来，会立刻出现一个空的代码框。</p>
<h2 id="坑二你往上翻它把你拽回底部">坑二：你往上翻，它把你拽回底部</h2>
<p>聊天列表是反着排的（最新在最下面，offset 0 就是底部）。用户往上翻 300 像素去回看历史，这时候新 token 到了，如果代码里什么都不判断、直接滚到底，画面会被生生拽回去 300 像素。</p>
<p>实测：offset 从 300 跳到 0，视野被拉动 300 像素。正在读的那段话直接消失。</p>
<p>改成「只有距底部 200 像素以内才自动滚」之后，同样是 offset 300 的状态，新内容到达时 offset 保持 300，不打扰用户。</p>
<p>第二个抖动来源是卡片展开。视口上方有一张卡片从 100 像素高变成 400 像素，可见内容整体位移 300 像素，手指下面的东西就漂走了。补偿办法是先量一次高度差，等这一帧排版完再把滚动位置挪回同样的距离：实测按 300 像素补偿后，锚点残余位移 0.0 像素。</p>
<p>那篇帖子还提到一个反向的坑：展开好办，折叠难办，因为 Flutter 在你来得及修之前已经把滚动位置调过了，你得记住变化之前的位置。</p>
<h2 id="坑三一条很长的回复当成一个-widget">坑三：一条很长的回复当成一个 widget</h2>
<p>前两个坑都在「文本」这一层，第三个在列表这一层。老写法是一条回复塞进一个 Column，里面是几十上百个块。问题是 Flutter 只要看到这个 widget 里有一部分在屏幕上，就得把整个 Column 建出来。</p>
<p>500 个块的实测对比：</p>
<table>
	<thead>
			<tr>
					<th>渲染方式</th>
					<th>首次渲染</th>
					<th>实际构建的块</th>
					<th>一次更新</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>整条回复一个 Column</td>
					<td>232 毫秒</td>
					<td>500 个</td>
					<td>30 毫秒，重建 500 个</td>
			</tr>
			<tr>
					<td>一行一块，用 ListView.builder</td>
					<td>24 毫秒</td>
					<td>14 个（只建可见的）</td>
					<td>只重建变化的那行</td>
			</tr>
	</tbody>
</table>
<p>232 毫秒相当于十四帧，第一次打开就卡一下；一次更新 30 毫秒也要掉两帧。改成分块之后，首次只建 14 个块，快了一个数量级。</p>
<p><img alt="500 个块的两种渲染方式" loading="lazy" src="/images/flutter-stream/fig3.png"></p>
<p>还有通知频率。20 个 chunk 在一个事件循环里到达，如果每个都调一次 <code>notifyListeners()</code>，UI 就被要求刷新 20 次；用一个小标志位把它们合并进一个微任务，通知次数变成 1 次。屏幕上看到的结果一样，白干的活少了十九份。</p>
<p>那篇帖子里我最认同的一句，也是这三节合起来的结论：</p>
<blockquote>
<p>the widget that owns a stream should be the smallest widget that shows it.</p>
</blockquote>
<p>翻译过来就是，谁的流谁负责，别让一个大家伙替你监听。</p>
<h2 id="这些数字怎么读">这些数字怎么读</h2>
<p>先交代口径：上面这些是在 <code>flutter test</code> 的无头环境里量的，量的是解析、构建、布局的 CPU 时间，不含 GPU 光栅化。所以看的是量级和相对差异，不等于真机帧率；真机上还会叠加字体排版、图片解码这些开销，只会更糟。</p>
<p>三个坑其实是同一件事的三个切面：都在为「已经处理过的内容」重复付钱。全量重解析在重复解析，整条回复一个 widget 在重复构建，每 chunk 通知一次在重复刷新。要修的也就不是三处代码，而是一个习惯。</p>
<p>顺带说个绕不开的现状：很多人第一反应是装 flutter_markdown 把 Markdown 交给官方包，但它在 pub.dev 上已经是停维护状态，最后一次发版停在 2025 年 5 月的 0.7.7+1，包页上指向了接手的 flutter_markdown_plus。流式渲染这块，官方那条路目前是断的，社区和各家客户端都在自己写。</p>
<p>你那边是怎么处理流式 Markdown 的，逐块解析还是整段重排？欢迎评论区聊聊。</p>
]]></content:encoded></item><item><title>Genkit Dart 1.0：AI 工具箱配齐了</title><link>http://hitrevor.com/posts/genkit-dart/</link><pubDate>Sun, 11 Oct 2026 10:17:07 +0800</pubDate><guid>http://hitrevor.com/posts/genkit-dart/</guid><description>Flutter 里加 AI 功能，过去无非两条路：把模型的 key 塞进 App 直接调，或者用 http 手拼请求。前者等于把钥匙交出去，后者写到第三天你会发现，自己在手搓一个拙劣的框架，重试、流式、工具调用、结构化输出，样样都得补。</description><content:encoded><![CDATA[<h2 id="起点dart-一直缺个趁手的-ai-框架">起点：Dart 一直缺个趁手的 AI 框架</h2>
<p>Flutter 里加 AI 功能，过去无非两条路：把模型的 key 塞进 App 直接调，或者用 <code>http</code> 手拼请求。前者等于把钥匙交出去，后者写到第三天你会发现，自己在手搓一个拙劣的框架，重试、流式、工具调用、结构化输出，样样都得补。</p>
<p>10 月 8 日 Genkit Dart 发了 1.0：</p>
<blockquote>
<p>Today, we&rsquo;re announcing Genkit Dart 1.0, the first stable, production-ready release of Google&rsquo;s open-source framework for building AI-powered features and agents in Dart.</p>
</blockquote>
<p>这篇不追热点，只回答两个问题：1.0 到底做了什么，Dart 开发者能从中拿走什么。</p>
<h2 id="一套-api通吃四家模型">一套 API，通吃四家模型</h2>
<p>Gemini、Claude、OpenAI，以及所有 OpenAI 兼容的服务，走同一个接口。供应商按插件注册，换模型不动业务代码：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">ai</span> <span class="o">=</span> <span class="n">Genkit</span><span class="p">(</span><span class="nl">plugins:</span> <span class="p">[</span><span class="n">googleAI</span><span class="p">(),</span> <span class="n">anthropic</span><span class="p">()]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">fromGemini</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">ai</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nl">model:</span> <span class="n">googleAI</span><span class="p">.</span><span class="n">gemini</span><span class="p">(</span><span class="s1">&#39;gemini-flash-latest&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nl">prompt:</span> <span class="n">prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">fromClaude</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">ai</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nl">model:</span> <span class="n">anthropic</span><span class="p">.</span><span class="n">model</span><span class="p">(</span><span class="s1">&#39;claude-sonnet-5-5&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">  <span class="nl">prompt:</span> <span class="n">prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>对国内开发者来说，更实际的是那句&quot;OpenAI 兼容&quot;：DeepSeek 这类服务在 <code>genkit_openai</code> 插件里有一等公民句柄，<code>deepSeek.model('deepseek-flash')</code> 就是全部配置，不用自己拼 baseUrl 假装成 OpenAI。</p>
<h2 id="flow带类型的-ai-函数还能当服务端">flow：带类型的 AI 函数，还能当服务端</h2>
<p>官方对 flow 的定义是 strongly typed, observable functions。我的理解：把 AI 逻辑包成一个输入输出都有类型的函数。</p>
<p>类型用 <code>schemantic</code> 在 Dart 里写一次：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="err">@</span><span class="n">Schema</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="kd">abstract</span> <span class="kd">class</span> <span class="nc">$RecipeRequest</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kt">String</span> <span class="kd">get</span> <span class="n">dish</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="kt">int</span> <span class="kd">get</span> <span class="n">servings</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>服务端拿它约束模型输出，Flutter 端拿同一份类型解析结果。中间那层手写的 <code>jsonDecode</code> 加 <code>as Map</code>，可以整段删掉：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">recipeFlow</span> <span class="o">=</span> <span class="n">ai</span><span class="p">.</span><span class="n">defineFlow</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">  <span class="nl">name:</span> <span class="s1">&#39;recipeFlow&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nl">inputSchema:</span> <span class="n">RecipeRequest</span><span class="p">.</span><span class="n">$schema</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nl">outputSchema:</span> <span class="n">Recipe</span><span class="p">.</span><span class="n">$schema</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="nl">fn:</span> <span class="p">(</span><span class="n">RecipeRequest</span> <span class="n">input</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">final</span> <span class="n">res</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">ai</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="nl">model:</span> <span class="n">deepSeek</span><span class="p">.</span><span class="n">model</span><span class="p">(</span><span class="s1">&#39;deepseek-flash&#39;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nl">prompt:</span> <span class="s1">&#39;给我一份 </span><span class="si">${</span><span class="n">input</span><span class="p">.</span><span class="n">servings</span><span class="si">}</span><span class="s1"> 人份的「</span><span class="si">${</span><span class="n">input</span><span class="p">.</span><span class="n">dish</span><span class="si">}</span><span class="s1">」的做法。&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nl">outputSchema:</span> <span class="n">Recipe</span><span class="p">.</span><span class="n">$schema</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">res</span><span class="p">.</span><span class="n">output</span><span class="o">!</span><span class="p">;</span>   <span class="c1">// 这里已经是 Recipe 类型
</span></span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">);</span>
</span></span></code></pre></div><p>flow 还能直接挂成 HTTP 端点，客户端用 <code>defineRemoteAction</code> 调它，schema 依然共用：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">router</span> <span class="o">=</span> <span class="n">GenkitRouter</span><span class="p">()..</span><span class="n">addAction</span><span class="p">(</span><span class="n">recipeFlow</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">server</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">router</span><span class="p">.</span><span class="n">serve</span><span class="p">(</span><span class="nl">port:</span> <span class="m">8123</span><span class="p">);</span>
</span></span></code></pre></div><p>线上协议是 <code>{&quot;data&quot;: {...}}</code> 进、<code>{&quot;result&quot;: {...}}</code> 出。对 Flutter 开发者来说，这意味着一份类型既当接口文档，又当解析器。</p>
<h2 id="三种放置方式key-摆在哪由你定">三种放置方式，key 摆在哪由你定</h2>
<p>这是我认为 1.0 最实在的一处。官方明说 AI 逻辑可以放三个位置：</p>
<ul>
<li>直接写在 Flutter 里，适合做原型和 bring-your-own-key 的场景</li>
<li>放在 Dart 服务端，敏感提示词留在后端，Flutter 端用 <code>defineRemoteAction</code> 调</li>
<li>AI 逻辑留在 Flutter，模型请求走一个轻量的 Genkit 后端，由它来保护 key 和做鉴权</li>
</ul>
<p>官方公告里那句提醒值得抄下来：<code>never embed private API keys in a published client app</code>。过去我们在客户端里嵌 key 是被现实逼的，现在有了正规出路。</p>
<h2 id="工具中断中间件">工具、中断、中间件</h2>
<p>工具让模型调你的 Dart 函数。真正需要人点头的动作，可以在工具里返回 <code>.interrupt(...)</code>，把生成循环挂起，等客户端确认后再恢复：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="nl">fn:</span> <span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">ctx</span><span class="p">)</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="n">ctx</span><span class="p">.</span><span class="n">resumed</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">.</span><span class="n">interrupt</span><span class="p">({</span><span class="s1">&#39;hotel&#39;</span><span class="o">:</span> <span class="n">input</span><span class="p">.</span><span class="n">hotelName</span><span class="p">,</span> <span class="s1">&#39;total&#39;</span><span class="o">:</span> <span class="n">input</span><span class="p">.</span><span class="n">totalPrice</span><span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">.</span><span class="n">response</span><span class="p">(</span><span class="n">confirmation</span><span class="p">.</span><span class="n">id</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span>
</span></span></code></pre></div><p>审批写在工具内部，模型绕不过去。官方原话是 <code>Putting the approval check inside the tool guarantees that the model can't bypass it.</code> 这一点比&quot;让模型守规矩&quot;的提示词工程靠谱。</p>
<p>中间件这边，自动重试、从 <code>SKILL.md</code> 动态加载技能、工具审批白名单，都是一行 <code>use: [...]</code> 挂到 <code>generate</code> 上的事。要写自己的横切逻辑，<code>defineGenerateMiddleware</code> 留了口子。</p>
<h2 id="提示词与生产可观测">提示词与生产可观测</h2>
<p>Dotprompt 把模板、模型配置、输入输出 schema 收在 <code>.prompt</code> 文件里，从 <code>prompts/</code> 目录自动加载，Dart 里当函数调：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">introPrompt</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">ai</span><span class="p">.</span><span class="n">prompt</span><span class="p">(</span><span class="s1">&#39;destinationIntro&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="kd">final</span> <span class="n">response</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">introPrompt</span><span class="p">({</span><span class="s1">&#39;destination&#39;</span><span class="o">:</span> <span class="s1">&#39;Kyoto&#39;</span><span class="p">});</span>
</span></span></code></pre></div><p>上线之后，<code>genkit_otel</code> 按 OpenTelemetry 的 GenAI 语义约定导出 trace、token 用量和延迟，直接接进你已有的观测后端。</p>
<h2 id="还在实验区的两个">还在实验区的两个</h2>
<p>有状态 agent 和生成式界面。前者用 <code>defineAgent</code> 把模型、工具、系统指令、会话状态收进一次定义，会话靠 session store 跨重启保住。后者是 <code>genkit_a2ui</code>：agent 不再只吐文本，而是吐日期选择器、表单、确认卡这类组件，Flutter 端增量渲染成原生 widget。</p>
<p>两个都从 <code>package:genkit/experimental.dart</code> 导入，可以提前试，不背稳定版的包袱。</p>
<h2 id="开发者实际能拿走什么">开发者实际能拿走什么</h2>
<p>第一是少写四类胶水：多厂商适配、结构化输出解析、流式传输、工具调用与重试。这四样以前每接一家模型就要重写一遍，现在都收进框架，代码从&quot;每次都重写&quot;变成&quot;接进来就有&quot;。</p>
<p>第二是两处位置挪对了。key 从客户端搬回后端，这是安全账；schema 从文档变成代码，这是接口契约。做过一遍就知道，这两样省掉的是后面几个月的手工活。</p>
<p>第三是能渐进接入。先把它当一个&quot;能调模型的库&quot;用，需要时再上 flow，最后才考虑 agent，不要求你一次把架构换掉。</p>
<p>我在本机把四个场景都跑了一遍，都是真打模型（DeepSeek）不是纸面推演：</p>
<pre tabindex="0"><code>[flow]   类型安全输出 → 番茄炒蛋（2 人份）| 6 步 / 15 分钟 / 1812ms
[tool]   finishReason=interrupted → restart 后 stop
[client] 远端返回 → 葱油拌面（1人份） | 5 步 | 2686ms
curl  →  {&#34;data&#34;:{...}} 进、{&#34;result&#34;:{...}} 出
</code></pre><p><img loading="lazy" src="/images/genkit-dart/fig-map.png"></p>
<h2 id="上手前要过的两道坎">上手前要过的两道坎</h2>
<p>成绩说完说得失。1.0 有个隐形门槛：要求 Dart SDK 3.12 以上。本机是 3.11.1，于是 <code>dart pub add genkit</code> 安静地给我装了 0.16.1，不报错、不警告。升完 SDK 还有第二道，代码生成器在 1.0 拆成了独立的 <code>schemantic_builder</code>，漏装它 <code>build_runner</code> 会打印 <code>wrote 0 outputs</code>，同样是零报错。</p>
<p>两条自检命令，抄走就行：</p>
<pre tabindex="0"><code>dart pub deps --style=compact | grep &#34;^- genkit&#34;   # 看真实拿到的版本
ls lib/*.g.dart                                    # 看代码生成有没有真落盘
</code></pre><h2 id="还有没验的">还有没验的</h2>
<p>诚实交代：Dev UI（<code>genkit start</code>，需要 npm 上的 <code>genkit-cli</code>）、中间件、Dotprompt 的运行效果、<code>genkit_otel</code> 的导出，以及实验区那两个，我这轮都没在本机跑，上面写的是官方文档口径。</p>
<p>值得一提的是，官方样例就在本地缓存里，<code>~/.pub-cache/hosted/pub.dev/genkit-1.0.0/example/http_server_example.dart</code>，流式、鉴权、自建路由都写在里面。动手之前先读它，能省半天。</p>
<p>如果你的 Dart 项目要接第一段 AI 逻辑，你会把它放在 App 里还是服务端？欢迎评论区聊聊。</p>
]]></content:encoded></item><item><title>Flutter 1.7.8 稳定版本发布</title><link>http://hitrevor.com/posts/flutter-1-7-8/</link><pubDate>Tue, 09 Jul 2019 09:00:00 +0800</pubDate><guid>http://hitrevor.com/posts/flutter-1-7-8/</guid><description>Flutter 1.7.8 稳定版本发布，修复了各种 bug</description><content:encoded><![CDATA[<p>Flutter 1.7.8 最新稳定版发布了, 最主要修复了在<a href="https://github.com/flutter/flutter/issues/18494">同一 APK 同时支持 32 位和 64 位的bug</a>， 这样就不用 使用这种黑科技来打包支持国内市场了，真香！！</p>
<p>修复的其他一些 bug 列表（部分）</p>
<h3 id="v176">v1.7.6</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/34684">#34684</a> The return type of the almost never used <code>ParentDataWidget.debugDescribeInvalidAncestorChain</code> method is now <code>Iterable&lt;DiagnosticsNode&gt;</code> instead of <code>String</code>. Existing uses can call <code>.join('\n')</code> on the return value if they need to continue to use a <code>String</code> instead of a <code>DiagnosticsNode</code>.</li>
</ul>
<h3 id="v174">v1.7.4</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/30069">#30069</a> Text inline widgets allows embedding arbitrary widgets inline with text. <code>TextSpan</code>has also been reworked to inherit from <code>InlineSpan</code>, in order to support <code>WidgetSpan</code>, which is used to define inline widgets. Existing uses of <code>TextSpan</code> should not assume the object is of type <code>TextSpan</code> anymore.</li>
</ul>
<h3 id="v171">v1.7.1</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/issues/33480">#33480</a> The <code>FadeInImage</code> widget no longer supports the <code>placeholderSemanticLabel</code>property.</li>
</ul>
<h3 id="v167">v1.6.7</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/issues/32374">#32374</a> The <code>Image</code> widget now supports two new properties: <code>frameBuilder</code>, and <code>loadingBuilder</code>, which enable callers to easily add a placeholder image which the image ios loading, add effects to the image once it loads (such as fading it in), and monitor the loading progress of the image (e.g. to show the user a loading indicator).</li>
</ul>
<h3 id="v163">v1.6.3</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/33148">#33148</a> ExpandIcon now uses Colors.white60 instead of Colors.white54 when dark theme is used (ThemeData.brightness is set to Brightness.dark).</li>
</ul>
<h3 id="v162">v1.6.2</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/32936">#32936</a> cleaned up the <code>ImageStream</code> listener API. <code>addListener()</code> and <code>removeListener()</code>now take an instance of <code>ImageStreamListener</code>, which contains references to the individual callbacks. See <a href="https://groups.google.com/forum/#!topic/flutter-announce/NWTszrEq9U0">the announcement</a> for more information.</li>
</ul>
<h3 id="v161">v1.6.1</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/32528">#32528</a> Tapping a modal bottom sheet no longer dismisses it by default.</li>
</ul>
<h3 id="v160">v1.6.0</h3>
<ul>
<li><a href="https://github.com/flutter/flutter/pull/30983">#30983</a> To support structured error messages <a href="https://github.com/flutter/flutter/issues/27327">#27327</a> the signature of InformationCollector and the context parameter of the FlutterErrorDetails constructor were changed. This should not impact you unless your package is attempting behave like part of the Flutter framework. See <a href="https://github.com/flutter/flutter/issues/31962#issuecomment-488882515">this comment</a> which discusses how to write code that is forwards and backwards compatible with this change.</li>
<li><a href="https://github.com/flutter/flutter/pull/31569">framework #31569</a>/<a href="https://github.com/flutter/engine/pull/8695">engine #8695</a> Rect and RRect are now const constructable and are backed by 64 bit doubles rather than 32 bit floats internally.</li>
<li><a href="https://github.com/flutter/flutter/pull/31917">framework #31917</a> &lt;- <a href="https://github.com/flutter/engine/pull/8796">engine #8796</a> &lt;- <a href="https://github.com/dart-lang/sdk/commit/1dd0f88c84">dart 1dd0f88c84</a> Fixes bug in StreamIterator which allowed constructor argument to be null. Also allowed await for on a null stream. This is now a runtime error.</li>
<li><a href="https://github.com/flutter/flutter/pull/30884">#30884</a> <code>TabController.animationController</code> changed to be <code>unbounded</code> to allow <code>DefaultTabController</code> to handle changes to number of tabs (after initial build).</li>
</ul>
<h3></h3>
]]></content:encoded></item></channel></rss>