<?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/en/tags/flutter/</link><description>Recent content in Flutter on Trevor's Blog</description><generator>Hugo</generator><language>en-US</language><lastBuildDate>Fri, 23 Oct 2026 14:31:26 +0800</lastBuildDate><atom:link href="http://mings.in/en/tags/flutter/index.xml" rel="self" type="application/rss+xml"/><item><title>AI Can Write Swift and Kotlin Now. So What's the Point of Cross-Platform?</title><link>http://mings.in/en/posts/flutter-ai-scale/</link><pubDate>Fri, 23 Oct 2026 14:31:26 +0800</pubDate><guid>http://mings.in/en/posts/flutter-ai-scale/</guid><description>On September 10, Shopify&amp;rsquo;s head of mobile published a post.</description><content:encoded><![CDATA[<p>On September 10, Shopify&rsquo;s head of mobile, Mustafa Ali, published a post.</p>
<p>The title was blunt: native is now the future of mobile at Shopify.</p>
<p>Six years ago they went all-in on React Native. Last January, Ali wrote a retrospective saying RN had a bright future. Now they are reversing course, and the stated reason is one thing: coding agents have crushed the cost of writing everything twice.</p>
<p>The switch worked out. Six engineers took Shop App from proof of concept to the app store in 12 weeks. iOS cold start improved 23%, Android 50%.</p>
<p><img alt="Migration diagram from Shopify&rsquo;s engineering blog" loading="lazy" src="/images/flutter-ai-scale/shopify-native.png"></p>
<p>But the most valuable line in that post is not a number.</p>
<p>Ali writes that an agent can rewrite the code in seconds, then spend minutes working out whether it actually works. Simulator control is too slow, so getting state means going through the accessibility tree or taking screenshots.</p>
<p>His words: if the model cannot quickly verify its own work, it does not matter how good the model is.</p>
<p>That complaint made me read two posts side by side. The Shopify piece from September 10 and the Flutter team&rsquo;s response forty days later are arguing about the same thing.</p>
<h2 id="the-bottleneck-is-not-generation-it-is-verification">The bottleneck is not generation, it is verification</h2>
<p>The Flutter post went up on October 20, by Carlos Mendonça and Emma Twersky. Its title is just as blunt: AI makes it easy to migrate, Flutter makes it easy to scale. It exists to answer the question Shopify threw down.</p>
<p>The whole piece rests on one claim. Code generation stopped being the bottleneck a while ago. Verification is the bottleneck now.</p>
<p>Two numbers back it up. 73% of developers say AI makes them more productive when starting a task. 46% do not trust AI&rsquo;s debugging accuracy without a human checking it.</p>
<p>Squeeze from both sides and the blocker sits on the human side.</p>
<h2 id="the-bill-for-three-steps">The bill for three steps</h2>
<p>The team published a comparison table putting native and Flutter next to each other across the three verification steps.</p>
<p><img alt="The same table rearranged in Chinese, values and wording unchanged" loading="lazy" src="/images/flutter-ai-scale/verify-table.png"></p>
<p>Code-to-running latency: native takes 2 to 5 minutes per cycle. Build, link, archive, install on the simulator. Four debug rounds burn twenty-odd minutes.</p>
<p>Flutter stays under 200 milliseconds. Hot reload injects the change into a running Dart VM and keeps state.</p>
<p>Automated tests: native walks the full simulator lifecycle and a background emulator. Flutter&rsquo;s headless widget tests run in an isolated environment in under 500 milliseconds.</p>
<p>The UI and state-check row is where it gets interesting.</p>
<p>Native screenshots the simulator and hands the image to a vision model to guess at the layout. Probabilistic, and slow. Flutter goes through MCP to query the Dart VM Service and DevTools, reading the widget and render object trees directly. Deterministic, structured.</p>
<p>Chain the three and you have a complete loop. Every step queues on the native side. On Flutter a round trip is milliseconds.</p>
<h2 id="harness-beats-prompt">Harness beats prompt</h2>
<p>Fast verification is one thing. Getting the agent to actually use it is another. The team ran an internal experiment called Project One Shot, letting an agent fix a production bug in camera_android_camerax on its own.</p>
<p>The plugin manages Android CameraX lifecycles and surface texture binding, the hairiest kind of Flutter code there is.</p>
<p>Given vague instructions like watch out for memory leaks, the agent failed every time.</p>
<p>It only worked with a deterministic harness. AST analysis over MCP, package-level domain skills, enforced test-driven guardrails. Reproduce with a failing test, write the smallest fix, finish with the full test suite and leak checks.</p>
<p>No room for the agent to freestyle. Every step has its acceptance criteria nailed down.</p>
<p>Three things shipped alongside. An official Dart and Flutter agent plugin bundles an MCP server with ten Flutter and fourteen Dart skills, so the agent can reach the Dart Analyzer and Widget Inspector directly.</p>
<p>A dedicated agent inside Antigravity audits accessibility issues and submits fixes that match existing conventions. Plus an Agent Skills CLI built with Serverpod: one dart run skills@get installs the skills a package ships with into your agent.</p>
<p>The benchmark numbers: Gemini 3.5 Flash from 84.1% to 85.5%, Gemini 3.1 Pro from 89.6% to 92.7%. Modest gains, run by the team on its own eval suite.</p>
<h2 id="where-this-actually-lands">Where this actually lands</h2>
<p>The post lists a set of deployment targets well beyond phones. Toyota and Lexus head units, the Xiaomi SU7 center console, Nubank&rsquo;s fintech app with a hundred million users, Universal Music.</p>
<p>The post does not push this angle. I think it is the most convincing part.</p>
<p>What these share is absurdly expensive verification.</p>
<p>Flashing a head unit takes tens of minutes. One production incident in a fintech app costs real money. Music app users tolerate zero jank.</p>
<p>In places like these, a minute off the verification loop is money and time you keep.</p>
<p>Look further out. Once agents writing code is normal, one codebase across platforms saves more than headcount. It saves time.</p>
<p>An agent produces a change, hot reload shows it on phone, desktop and web at once. That is an order of magnitude faster than compiling, packaging and installing per platform.</p>
<p>That is also why the Flutter team has been pushing into automotive, desktop and embedded.</p>
<p>The second half of cross-platform may not happen on phones.</p>
<h2 id="a-few-things-i-do-not-buy">A few things I do not buy</h2>
<p>The app store rating section does not hold up. It cites Apptopia&rsquo;s study of 201,000 apps. On Google Play, Flutter apps average 4.1 stars, and so do non-Flutter apps. On the App Store both sit at 4.0. The conclusion offered is that users do not care about the framework.</p>
<p>All that shows is that cross-platform did not hurt ratings. Flip it around and it also shows cross-platform did not help. Using a tie to argue for adoption is missing a step.</p>
<p>That 200 milliseconds is the time to inject a change. It excludes writing the code, thinking it through, switching windows.</p>
<p>It cannot change class structure either. Add a field or restructure the state tree and you need a restart.</p>
<p>The table measures code-to-running latency, which is an honest definition. Readers will take it as development being twenty times faster, and those are very different claims.</p>
<p>The enterprise examples have survivorship bias too.</p>
<p>Xiaomi SU7, Universal cutting 45% of its code, Toyota and Lexus, Nubank&rsquo;s hundred million users. All real.</p>
<p>No post ever covers the company that moved to cross-platform for two years and then tore it out. Arguments from these examples are half evidence.</p>
<p><img alt="The eight arguments the team published, enough to argue with in a team meeting" loading="lazy" src="/images/flutter-ai-scale/talking-points.png"></p>
<h2 id="how-to-do-the-cross-platform-math">How to do the cross-platform math</h2>
<p>Line up both sides first. Shopify&rsquo;s call: the cost of writing twice has been crushed, and what is left is not worth giving up native capabilities for. Flutter&rsquo;s call: coordination costs remain.</p>
<p>The example in the post is specific. Same checkout state machine, two instances of the same model. One writes Swift and drops stale events through an async stream. One writes Kotlin and queues events through a reactive flow.</p>
<p>Both pass local unit tests on day one. Weeks later Android checkout conversion drops three points, because under network jitter the retry timing differs.</p>
<p><img alt="From the original post: parallel agents do not remove coordination cost, they introduce nondeterministic drift" loading="lazy" src="/images/flutter-ai-scale/parallel-agent-challenge.png"></p>
<p>Both positions hold. They are doing different arithmetic.</p>
<p>I also do not think this pivot is evidence that cross-platform is dead.</p>
<p>This is a company with hundreds of screens, over a thousand engineers, and mobile as a core investment. At that scale, rebuilding for 23% faster cold start and a better crash rate pencils out.</p>
<p>For five people maintaining a mid-sized app, the same move probably does not.</p>
<p>What decides whether you go cross-platform is team size, how many years you will maintain it, and how long your verification chain is. AI changes none of those.</p>
<p>Here is a question worth arguing about instead. If agents really do get a verification cycle down to a few hundred milliseconds, how much longer does native-feels-better hold up.</p>
<p>I would like to hear what is actually on your desk. Is your project single-platform, dual, or cross? What drove that call: performance, headcount, or a boss who said we are going native?</p>
<p>One more question: do you think Compose Multiplatform has a shot? It is cross-platform too, arrived years after Flutter, but it stands on half the Kotlin and Android ecosystem.</p>
<p>Drop a comment. I will pick a few representative ones and write them up as a follow-up.</p>
<p>Both source posts are here. Twenty minutes total.</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>Streaming Chat in Flutter: What Three Pitfalls Actually Cost</title><link>http://mings.in/en/posts/flutter-stream/</link><pubDate>Sat, 17 Oct 2026 10:34:47 +0800</pubDate><guid>http://mings.in/en/posts/flutter-stream/</guid><description>Anyone writing an AI chat client runs into this: the model does not hand you the answer in one go. It dribbles out token by token, wedging tool calls, code blocks and stretches of thinking in between. The user staring at the screen is really watching a tree grow while someone keeps rearranging it.</description><content:encoded><![CDATA[<h2 id="one-reply-streams-for-twenty-minutes-and-the-ui-has-to-grow-with-it">One reply streams for twenty minutes, and the UI has to grow with it</h2>
<p>Anyone writing an AI chat client runs into this: the model does not hand you the answer in one go. It dribbles out token by token, wedging tool calls, code blocks and stretches of thinking in between. The user staring at the screen is really watching a tree grow while someone keeps rearranging it.</p>
<p>The GetPullRequest team wrote a long post recently about how they built this in Flutter. The sharpest line is about Flutter&rsquo;s official GenUI package:</p>
<blockquote>
<p>It only drew the UI after the model&rsquo;s full response had arrived and been parsed.</p>
</blockquote>
<p>Which means it waits for the whole answer to arrive and be parsed before drawing anything. For a normal Q&amp;A, waiting a few seconds is fine. For an agent doing twenty minutes of work, it is not: the tool card should have appeared long ago and you are still looking at a spinner.</p>
<p>I reproduced the key scenarios from that post on macOS and measured them. Three pitfalls here, each with a number you can produce yourself.</p>
<h2 id="pitfall-one-re-parsing-markdown-on-every-token">Pitfall one: re-parsing Markdown on every token</h2>
<p>The most naive approach: a slice of text arrives, you throw the accumulated full text at a Markdown parser, get the node tree, render it. Looks like the obvious thing to do. The cost is that you re-parse what has already been parsed, every single time.</p>
<p>I took an 886-character agent reply as a sample: prose, a three-row table, a dart code block, a two-level nested list. I sliced it into 222 four-character chunks to simulate streaming. Here is how the three approaches compare.</p>
<table>
	<thead>
			<tr>
					<th>Approach</th>
					<th>Total time</th>
					<th>Notes</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Full re-parse on every chunk</td>
					<td>28 ms</td>
					<td>222 parses, slowest single parse 0.62 ms</td>
			</tr>
			<tr>
					<td>Re-parse only inside the last, still-growing block</td>
					<td>4 ms</td>
					<td>Completed blocks parsed once</td>
			</tr>
			<tr>
					<td>Parse once after everything arrives</td>
					<td>0.148 ms</td>
					<td>The cost of the GenUI approach</td>
			</tr>
	</tbody>
</table>
<p>28 milliseconds does not sound like much, but it is not spread evenly across 222 chunks. The first 50 chunks average 0.046 ms, the last 50 average 0.199 ms. It gets more expensive as you go, because the accumulated text gets longer. This is quadratic work: for an 886-character reply, the naive approach processes about 98K characters, 111 times the original.</p>
<p>Real-length answers make it obvious. Same approach, reply length doubles, time goes up two to three times:</p>
<pre tabindex="0"><code>886 chars / 222 chunks: 16 ms
1774 chars / 444 chunks: 61 ms
3550 chars / 888 chunks: 229 ms
7102 chars / 1776 chunks: 899 ms
</code></pre><p>A frame budget is 16.7 ms. By the 3550-character tier, just parsing the tail eats several milliseconds, and add layout and paint on top and dropped frames are inevitable.</p>
<p><img alt="Three parsing strategies for the same reply" loading="lazy" src="/images/flutter-stream/fig1.png"></p>
<p>The post&rsquo;s fix is block-level incrementality: completed blocks parse once, only the growing one is re-parsed, and &ldquo;notify the UI on every chunk&rdquo; becomes &ldquo;coalesce notifications into one microtask.&rdquo;</p>
<h2 id="line-by-line-parsing-shreds-the-structure">Line-by-line parsing shreds the structure</h2>
<p>There is a sneakier pitfall, tied to a product requirement: letting users select and comment on individual lines. If your render layer is one widget per line and each line is parsed separately, Markdown structure falls apart.</p>
<p>The same content, parsed line by line, produces this tag sequence:</p>
<pre tabindex="0"><code>[p, p, p, p, p, pre, p, p, pre, ul, li, pre, pre]
</code></pre><p>Parsed in chunks it produces this:</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>Compare the counts. The table: 0 tables when parsed line by line (a four-row table turns into five lines of pipe-separated text), 1 when parsed in chunks. The code block: 4 pre tags line by line (it should be one unit), 1 in chunks. The nested list: only 1 li line by line (it should be three levels), 3 in chunks.</p>
<p>The nested list case is the easiest to miss. In Markdown, four leading spaces mean &ldquo;this is code,&rdquo; so the moment the second level of a list gets parsed on its own it turns into a code block, shown on screen as a grey box with a bare minus sign in front of it.</p>
<p><img alt="Structural difference between line-by-line and chunk parsing" loading="lazy" src="/images/flutter-stream/fig2.png"></p>
<p>One more basic pitfall while we are here: the markdown package only parses tables with the extension explicitly enabled. The default <code>Document()</code> yields 0 tables; switch to <code>ExtensionSet.gitHubWeb</code> and you get 1.</p>
<p>And one that is specific to streaming: a chunk can break in the middle of three backticks. When I fed that half-finished text to the parser, the one or two trailing backticks became literal characters in the body, so two lonely <code>`</code> pop up on screen and only turn into a proper code block when the next chunk arrives. And if it happens to be a complete triple backtick with no content yet, you immediately get an empty code box.</p>
<h2 id="pitfall-two-you-scroll-up-it-yanks-you-back-to-the-bottom">Pitfall two: you scroll up, it yanks you back to the bottom</h2>
<p>The chat list is reversed (newest at the bottom, offset 0 is the bottom). The user scrolls up 300 pixels to reread history. A new token arrives, and if the code does no checking and just scrolls to the bottom, the view is dragged back 300 pixels by force.</p>
<p>Measured: offset jumps from 300 to 0, the viewport is pulled 300 pixels. The paragraph being read vanishes.</p>
<p>Change it to &ldquo;only auto-scroll when within 200 pixels of the bottom&rdquo; and at that same offset of 300, the offset stays at 300 when new content arrives. The user is left alone.</p>
<p>The second source of jitter is card expansion. A card above the viewport grows from 100 pixels to 400, visible content shifts 300 pixels, and whatever was under your finger drifts away. The fix is to measure the height difference first, then after that frame&rsquo;s layout, move the scroll position back by the same distance. Measured: compensating 300 pixels leaves 0.0 pixels of residual anchor drift.</p>
<p>The post also mentions the reverse case: expanding is easy, collapsing is hard, because Flutter has already adjusted the scroll position before you get a chance to fix it. You have to remember the position from before the change.</p>
<h2 id="pitfall-three-treating-one-long-reply-as-one-widget">Pitfall three: treating one long reply as one widget</h2>
<p>The first two pitfalls live at the text layer. The third is at the list layer. The old way stuffs one reply into a single Column with tens or hundreds of blocks inside. The problem is that as soon as Flutter sees any part of that widget on screen, it has to build the entire Column.</p>
<p>Measured with 500 blocks:</p>
<table>
	<thead>
			<tr>
					<th>Rendering</th>
					<th>First render</th>
					<th>Blocks actually built</th>
					<th>One update</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Whole reply in one Column</td>
					<td>232 ms</td>
					<td>500</td>
					<td>30 ms, rebuilds 500</td>
			</tr>
			<tr>
					<td>One block per row with ListView.builder</td>
					<td>24 ms</td>
					<td>14 (only what is visible)</td>
					<td>Rebuilds only the changed row</td>
			</tr>
	</tbody>
</table>
<p>232 ms is fourteen frames. It janks the first time you open it. A 30 ms update also drops two frames. Split into blocks, the first render builds only 14 and is an order of magnitude faster.</p>
<p><img alt="Two ways to render 500 blocks" loading="lazy" src="/images/flutter-stream/fig3.png"></p>
<p>Then there is notification frequency. Twenty chunks arrive in one event loop. Call <code>notifyListeners()</code> on each and the UI is asked to refresh 20 times. Coalesce them into one microtask with a small flag and you get 1 notification. Same result on screen, nineteen copies of wasted work gone.</p>
<p>The line from that post I agree with most, and the conclusion of all three sections:</p>
<blockquote>
<p>the widget that owns a stream should be the smallest widget that shows it.</p>
</blockquote>
<p>Which means: whoever owns the stream owns the listening. Do not let one big thing listen on your behalf.</p>
<h2 id="how-to-read-these-numbers">How to read these numbers</h2>
<p>The caveat first: these were measured in a headless <code>flutter test</code> environment, counting CPU time for parsing, building and layout, excluding GPU rasterization. So read them as magnitudes and relative differences, not as real-device frame rates. On a real device you also pay for font shaping and image decoding, so it only gets worse.</p>
<p>The three pitfalls are three facets of one thing: all of them pay repeatedly for content already processed. Full re-parsing re-parses. One widget for a whole reply rebuilds. Notify-per-chunk refreshes. What needs fixing is not three pieces of code but one habit.</p>
<p>One unavoidable piece of context: many people reach for flutter_markdown and hand Markdown to the official package, but it is unmaintained on pub.dev. The last release stopped at 0.7.7+1 in May 2025, and the package page points to flutter_markdown_plus as the successor. On streaming rendering, the official path is currently broken, and the community and every client vendor are writing their own.</p>
<p>How do you handle streaming Markdown? Chunk-by-chunk parsing or full re-layout? Tell me in the comments.</p>
]]></content:encoded></item><item><title>Genkit Dart 1.0: The AI Toolbox Is Finally Complete</title><link>http://mings.in/en/posts/genkit-dart/</link><pubDate>Sun, 11 Oct 2026 10:17:07 +0800</pubDate><guid>http://mings.in/en/posts/genkit-dart/</guid><description>Adding AI to Flutter used to mean two roads: stuff the model key into the app and call it directly, or hand-assemble requests with http. The first is handing over the keys; the second has you hand-crafting a shoddy framework by day three, patching in retries, streaming, tool calls and structured output one at a time.</description><content:encoded><![CDATA[<h2 id="starting-point-dart-never-had-a-decent-ai-framework">Starting point: Dart never had a decent AI framework</h2>
<p>Adding AI to Flutter used to mean two roads: stuff the model key into the app and call it directly, or hand-assemble requests with <code>http</code>. The first is handing over the keys; the second has you hand-crafting a shoddy framework by day three, patching in retries, streaming, tool calls and structured output one at a time.</p>
<p>On October 8, Genkit Dart shipped 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>This post isn&rsquo;t chasing the news cycle. It answers two questions: what 1.0 actually did, and what Dart developers can take away from it.</p>
<h2 id="one-api-four-model-providers">One API, four model providers</h2>
<p>Gemini, Claude, OpenAI, and every OpenAI-compatible service go through the same interface. Providers register as plugins, so swapping models doesn&rsquo;t touch business 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">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>For developers in China, the more practical part is the &ldquo;OpenAI-compatible&rdquo; bit: services like DeepSeek get a first-class handle in the <code>genkit_openai</code> plugin. <code>deepSeek.model('deepseek-flash')</code> is the entire config — no assembling a baseUrl by hand and pretending to be OpenAI.</p>
<h2 id="flow-typed-ai-functions-that-can-also-be-your-server">flow: typed AI functions that can also be your server</h2>
<p>Google defines a flow as strongly typed, observable functions. My read: wrap AI logic into a function whose input and output both have types.</p>
<p>Types are written once in Dart with <code>schemantic</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="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>The server uses it to constrain model output, and Flutter uses the same type to parse the result. The hand-written <code>jsonDecode</code> plus <code>as Map</code> layer in between can be deleted wholesale:</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">// already a Recipe here
</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>A flow can also be mounted as an HTTP endpoint directly, with the client calling it through <code>defineRemoteAction</code> and the same schema shared:</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>On the wire it&rsquo;s <code>{&quot;data&quot;: {...}}</code> in, <code>{&quot;result&quot;: {...}}</code> out. For Flutter developers that means one type serves as both the interface doc and the parser.</p>
<h2 id="three-placements-you-decide-where-the-key-lives">Three placements, you decide where the key lives</h2>
<p>This is the most substantive thing in 1.0, I think. Google states outright that AI logic can live in three places:</p>
<ul>
<li>Directly in Flutter, good for prototypes and bring-your-own-key scenarios</li>
<li>On a Dart server, keeping sensitive prompts on the backend, with Flutter calling through <code>defineRemoteAction</code></li>
<li>AI logic stays in Flutter, model requests go through a lightweight Genkit backend that protects the key and handles auth</li>
</ul>
<p>The line from the announcement is worth copying down: <code>never embed private API keys in a published client app</code>. We used to embed keys in clients because reality forced us to. Now there&rsquo;s a legitimate way out.</p>
<h2 id="tools-interrupts-middleware">Tools, interrupts, middleware</h2>
<p>Tools let the model call your Dart functions. For actions that genuinely need a human nod, a tool can return <code>.interrupt(...)</code>, suspending the generation loop until the client confirms:</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>The approval lives inside the tool, so the model can&rsquo;t route around it. Google&rsquo;s words: <code>Putting the approval check inside the tool guarantees that the model can't bypass it.</code> That is far more reliable than prompt engineering to keep the model in line.</p>
<p>On middleware, automatic retries, dynamically loading skills from <code>SKILL.md</code>, tool approval allowlists — all one line of <code>use: [...]</code> on <code>generate</code>. To write your own cross-cutting logic, <code>defineGenerateMiddleware</code> leaves the door open.</p>
<h2 id="prompts-and-production-observability">Prompts and production observability</h2>
<p>Dotprompt keeps templates, model config and input/output schemas in <code>.prompt</code> files, auto-loads from the <code>prompts/</code> directory, and you call it like a function in 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>Once you&rsquo;re live, <code>genkit_otel</code> exports traces, token usage and latency per OpenTelemetry&rsquo;s GenAI semantic conventions, plugging straight into whatever observability backend you already run.</p>
<h2 id="two-still-in-the-experimental-zone">Two still in the experimental zone</h2>
<p>Stateful agents and generative UI. The first is <code>defineAgent</code>, folding model, tools, system instructions and session state into one definition, with sessions surviving restarts via a session store. The second is <code>genkit_a2ui</code>: the agent stops emitting only text and starts emitting components like date pickers, forms and confirmation cards, which Flutter renders incrementally into native widgets.</p>
<p>Both import from <code>package:genkit/experimental.dart</code>. Try them early without carrying the stable-release baggage.</p>
<h2 id="what-developers-actually-take-away">What developers actually take away</h2>
<p>First, four kinds of glue you no longer write: multi-vendor adaptation, structured output parsing, streaming, and tool calls with retries. All four used to be rewritten for every model you onboarded. Now they&rsquo;re in the framework, and the code goes from &ldquo;rewrite it every time&rdquo; to &ldquo;it&rsquo;s there when you plug in&rdquo;.</p>
<p>Second, two things moved to the right place. The key moved from the client back to the server — that&rsquo;s the security account. The schema moved from docs into code — that&rsquo;s the interface contract. Anyone who&rsquo;s done this once knows what those two save you is months of manual work later.</p>
<p>Third, adoption can be incremental. Start by using it as &ldquo;a library that can call models&rdquo;, add flows when you need them, and only then think about agents. Nothing demands you swap your architecture in one go.</p>
<p>I ran all four scenarios on my machine, all hitting a real model (DeepSeek), not paper reasoning:</p>
<pre tabindex="0"><code>[flow]   typed output → 番茄炒蛋（2 人份）| 6 steps / 15 min / 1812ms
[tool]   finishReason=interrupted → stop after restart
[client] remote response → 葱油拌面（1人份） | 5 steps | 2686ms
curl  →  {&#34;data&#34;:{...}} in, {&#34;result&#34;:{...}} out
</code></pre><p><img loading="lazy" src="/images/genkit-dart/fig-map.png"></p>
<h2 id="two-hurdles-before-you-start">Two hurdles before you start</h2>
<p>Now the down side. 1.0 has an invisible threshold: it requires Dart SDK 3.12 or above. My machine had 3.11.1, so <code>dart pub add genkit</code> quietly installed 0.16.1 for me — no error, no warning. After you upgrade the SDK there&rsquo;s a second one: the code generator was split into a separate <code>schemantic_builder</code> in 1.0, and if you miss it <code>build_runner</code> prints <code>wrote 0 outputs</code> — again, zero errors.</p>
<p>Two self-check commands, take them:</p>
<pre tabindex="0"><code>dart pub deps --style=compact | grep &#34;^- genkit&#34;   # see the version you actually got
ls lib/*.g.dart                                    # check whether codegen really hit disk
</code></pre><h2 id="what-i-havent-verified">What I haven&rsquo;t verified</h2>
<p>Being honest: the Dev UI (<code>genkit start</code>, needs <code>genkit-cli</code> from npm), middleware, how Dotprompt actually behaves at runtime, <code>genkit_otel</code> exports, and the two experimental ones — I didn&rsquo;t run any of those locally this round. What&rsquo;s written above is the official documentation&rsquo;s account.</p>
<p>Worth mentioning: the official example is already in your local cache, at <code>~/.pub-cache/hosted/pub.dev/genkit-1.0.0/example/http_server_example.dart</code>. Streaming, auth and custom routing are all in there. Read it before you start and you save half a day.</p>
<p>If your Dart project were getting its first piece of AI logic, would you put it in the app or on the server? Comments welcome.</p>
]]></content:encoded></item><item><title>Flutter 1.7.8 Stable Released</title><link>http://mings.in/en/posts/flutter-1-7-8/</link><pubDate>Tue, 09 Jul 2019 09:00:00 +0800</pubDate><guid>http://mings.in/en/posts/flutter-1-7-8/</guid><description>Flutter 1.7.8 stable is out, fixing a batch of bugs.</description><content:encoded><![CDATA[<p>The latest stable Flutter 1.7.8 is out, and the headline fix is <a href="https://github.com/flutter/flutter/issues/18494">the bug that kept a single APK from supporting both 32-bit and 64-bit</a>. No more of that hack to package for the domestic market. Lovely.</p>
<p>A partial list of the other fixes:</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>