<?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://mings.in/tags/flutter/</link><description>Recent content in Flutter on Trevor's Blog</description><generator>Hugo</generator><language>zh-CN</language><lastBuildDate>Sat, 17 Oct 2026 10:34:47 +0800</lastBuildDate><atom:link href="http://mings.in/tags/flutter/index.xml" rel="self" type="application/rss+xml"/><item><title>Flutter 流式聊天：三个坑的实测代价</title><link>http://mings.in/posts/flutter-stream/</link><pubDate>Sat, 17 Oct 2026 10:34:47 +0800</pubDate><guid>http://mings.in/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://mings.in/posts/genkit-dart/</link><pubDate>Sun, 11 Oct 2026 10:17:07 +0800</pubDate><guid>http://mings.in/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://mings.in/posts/flutter-1-7-8/</link><pubDate>Tue, 09 Jul 2019 09:00:00 +0800</pubDate><guid>http://mings.in/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>