<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>DartWay Blog</title>
        <link>https://dartway.dev/blog</link>
        <description>Full-stack Dart in production: engineering writing, case breakdowns, framework releases.</description>
        <lastBuildDate>Thu, 01 Oct 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>Copyright © 2026 DartWay.</copyright>
        <item>
            <title><![CDATA[Shopify leaves React Native: what it means for Flutter]]></title>
            <link>https://dartway.dev/blog/shopify-leaves-react-native</link>
            <guid>https://dartway.dev/blog/shopify-leaves-react-native</guid>
            <pubDate>Thu, 01 Oct 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Shopify moved its apps to React Native in 2020 and is now going back to Swift and Kotlin, because agents made a second codebase cheap. How they did it, what the numbers leave out, and why Flutter developers should not worry.]]></description>
            <content:encoded><![CDATA[<p>Shopify went all-in on React Native in 2020, finished moving its apps last year —
and in September 2026 announced it is going back to native Swift and Kotlin. This
is not our stack, but the reason is coding agents, and agents change what matters
when you pick a framework at all. I build a fullstack Dart framework, and I already
know I will not be the one writing apps on it: agents will. That changes the price
of every technical choice, and Shopify is a clear case of it.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-happened">What happened<a href="https://dartway.dev/blog/shopify-leaves-react-native#what-happened" class="hash-link" aria-label="Direct link to What happened" title="Direct link to What happened" translate="no">​</a></h2>
<p>In 2020 Shopify bet on React Native: one codebase, lower costs. In January 2025
its head of mobile wrote that the future of React Native was bright. On 10
September 2026 the same person published
<a href="https://shopify.engineering/back-to-native" target="_blank" rel="noopener noreferrer" class="">Native is now the future of mobile at Shopify</a>.
The Shop app is already rebuilt and in the stores; the main Shopify app — more than
300 screens, widgets, an Apple Watch app, Siri Shortcuts — ships later this year.</p>
<p>They do not say React Native was a mistake. They say it was the right choice in
2020 and the conditions have changed.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-native-gave-them">What native gave them<a href="https://dartway.dev/blog/shopify-leaves-react-native#what-native-gave-them" class="hash-link" aria-label="Direct link to What native gave them" title="Direct link to What native gave them" translate="no">​</a></h2>
<p>From the <a href="https://shopify.engineering/shop-app-migration" target="_blank" rel="noopener noreferrer" class="">Shop app migration post</a>:</p>















































<table><thead><tr><th></th><th>React Native</th><th>Native</th><th>Change</th></tr></thead><tbody><tr><td>Cold start, Android</td><td>4433 ms</td><td>2233 ms</td><td>−50%</td></tr><tr><td>Cold start, iOS</td><td>3200 ms</td><td>2466 ms</td><td>−23%</td></tr><tr><td>App size, Android</td><td>293 MB</td><td>184 MB</td><td>−37%</td></tr><tr><td>App size, iOS</td><td></td><td></td><td>+1 MB</td></tr><tr><td>Crash-free sessions</td><td>99.5%</td><td>99.95%</td><td>10× fewer crashing sessions</td></tr><tr><td>Android release build time</td><td></td><td></td><td>−75%</td></tr></tbody></table>
<p>Six core engineers, twelve weeks from proof of concept to the stores. The proof of
concept itself took one engineer one week.</p>
<p>The advantages of native were always known. They just always looked small — a
slightly faster start, a slightly smaller build, a few fewer platform bugs — next
to the cost of building everything twice.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-now">Why now<a href="https://dartway.dev/blog/shopify-leaves-react-native#why-now" class="hash-link" aria-label="Direct link to Why now" title="Direct link to Why now" translate="no">​</a></h2>
<p>Keeping two codebases used to mean keeping two full development teams. Now agents
write most of the code, and they can also take on the supporting work: checking
that a feature behaves the same on both platforms, comparing, verifying. Shopify
puts it this way:</p>
<blockquote>
<p>Native still means building and maintaining software on two platforms, that cost
has not disappeared. What changed is that agents can now do enough of the
implementation, translation, testing, and review work that it's no longer the
deciding factor it was in 2020.</p>
</blockquote>
<p>The same thing is happening everywhere. With agents you can port almost any
library to your own stack, or rewrite a whole framework, and do it fast. That
shifts what counts as a good or a bad technical decision.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="how-they-did-it">How they did it<a href="https://dartway.dev/blog/shopify-leaves-react-native#how-they-did-it" class="hash-link" aria-label="Direct link to How they did it" title="Direct link to How they did it" translate="no">​</a></h2>
<p>The interesting part is not the decision but how you trust a rewrite of this size
to agents at a company where any mistake costs real money. They did not just tell a
model to rewrite the app. They built a pipeline.</p>
<p><strong>Logic separated from UI.</strong> Business logic was split from the interface so cleanly
that it runs headless, on a desktop machine. Agents tested it through a CLI and
console tests — inspect the app's state, navigate, perform actions — in
milliseconds, without starting an emulator. The UI was verified separately: the
new screens were compared by screenshot against the running React Native app at
the same checkpoints. Their tool for this, Tardis, also captured analytics events
in both apps so agents could compare event names, counts and payloads.</p>
<p><strong>Small checkpoints.</strong> Their system, Helix, cuts a screen into small ordered slices
that can be reviewed in minutes. Each slice has to prove its behaviour with tests,
match the running app in a visual review, survive two adversarial code reviewers
and get a human's approval before it is committed. Review feedback is remembered,
so the loop needs less help as the migration goes on.</p>
<p><strong>Where agents fell short.</strong> Even inside that pipeline, the generated code brought
duplication, architectural drift and performance problems. Shopify is explicit that
native expertise remained essential — this only worked because they had a team of
native experts reviewing it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-numbers-leave-out">What the numbers leave out<a href="https://dartway.dev/blog/shopify-leaves-react-native#what-the-numbers-leave-out" class="hash-link" aria-label="Direct link to What the numbers leave out" title="Direct link to What the numbers leave out" translate="no">​</a></h2>
<p>A few things did not fit into the video:</p>
<ul>
<li class=""><strong>The comparison is against the old React Native architecture.</strong> Commenters
pointed out that nobody knows how the New Architecture would have measured —
and moving to it was a large refactor Shopify faced anyway.</li>
<li class=""><strong>It was a port, not a new product.</strong> Agents had a working app to read and to
compare against. Shopify says agents worked best exactly when they had an
existing implementation; building something new does not speed up the same way.</li>
<li class=""><strong>Two platforms is now permanent.</strong> Feature parity "at all times", two reviews,
two releases — and no over-the-air updates past the store.</li>
</ul>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="should-flutter-developers-worry">Should Flutter developers worry?<a href="https://dartway.dev/blog/shopify-leaves-react-native#should-flutter-developers-worry" class="hash-link" aria-label="Direct link to Should Flutter developers worry?" title="Direct link to Should Flutter developers worry?" translate="no">​</a></h2>
<p>I do not think so — I am quite sure not.</p>
<p>This is one more example of agents making a little more possible. The biggest
tech companies always ran native, with parallel teams for each platform, and never
went cross-platform. Shopify is a large company, but not that large; agents let it
work to the standards of companies with stronger development teams. It is the same
shift as a small business now being able to build its own app, which used to take a
mid-sized one. The cost of reaching the same result drops, at every level.</p>
<p>And rewriting is a pain in any case. For most companies it simply is not worth it:
it is far better to put the effort into the product — new features, higher quality
— than to chase the last 1% of bugs or shave megabytes off a build. The gains
Shopify got matter much less to most teams.</p>
<p>So I do not expect this to move the job market much, and Flutter even less. In my
view Flutter is well ahead of React Native, so it suffers less from trends like
this one.</p>
<p>What I do take from it is the same lesson that changed my own work: I recently
rewrote my framework quite radically, precisely because I came to trust agents
with very large tasks done in short time. Is anyone around you talking about moving
from one stack to another with agents?</p>
<hr>
<p><em>The video version, in Russian, is <a href="https://youtu.be/sbbvqOYI0Ko" target="_blank" rel="noopener noreferrer" class="">on YouTube</a>.</em></p>]]></content:encoded>
            <category>Ecosystem</category>
        </item>
        <item>
            <title><![CDATA[Dart MCP: what it actually gives a coding agent]]></title>
            <link>https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent</link>
            <guid>https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent</guid>
            <pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[The MCP server has been built into the Dart SDK since July 2025. Half of it repeats what an agent already does through the terminal; the other half works with the running app. What that is worth, and what it costs in context.]]></description>
            <content:encoded><![CDATA[<p>An MCP server has been built into the Dart SDK since July 2025, and only recently
have people started actually using it. MCP is the protocol that lets a coding
agent call a server for information or actions. For Dart that means the agent is
no longer limited to the terminal — it can reach into your running app.</p>
<p>We connected it to Claude Code on a demo app and checked what it gives in
practice.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="connecting-it">Connecting it<a href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent#connecting-it" class="hash-link" aria-label="Direct link to Connecting it" title="Direct link to Connecting it" translate="no">​</a></h2>
<p>One command:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">claude mcp add --transport stdio dart -- dart mcp-server --enable all</span><br></div></code></pre></div></div>
<p>Without <code>--enable all</code>, half of the tools are switched off. Under the hood it goes
through the Dart Tooling Daemon — the same thing DevTools uses for debugging. The
next agent session starts the server and receives the list of tools.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-the-agent-could-already-do">What the agent could already do<a href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent#what-the-agent-could-already-do" class="hash-link" aria-label="Direct link to What the agent could already do" title="Direct link to What the agent could already do" translate="no">​</a></h2>
<p>The analyzer, tests, <code>dart fix</code>, formatting, searching pub.dev. All useful, and all
things an agent does without the server — through the terminal and the web. In
the demo it went to search pub.dev through the tool; it would have found the
package just as well without it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-is-actually-new-the-running-app">What is actually new: the running app<a href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent#what-is-actually-new-the-running-app" class="hash-link" aria-label="Direct link to What is actually new: the running app" title="Direct link to What is actually new: the running app" translate="no">​</a></h2>
<p>This is the part worth having:</p>
<ul>
<li class=""><strong>runtime errors</strong> from the live app, straight to the agent;</li>
<li class=""><strong>the widget tree</strong>, as the app has it right now;</li>
<li class=""><strong>taps and text input</strong> through Flutter Driver — tap, enter text, wait, get an
offset, take a screenshot;</li>
<li class=""><strong>evaluating an expression</strong> inside the running app — the agent can print a
value on the spot instead of finding the place in the code and adding a print;</li>
<li class=""><strong>hot reload</strong> after a fix.</li>
</ul>
<p>In the demo the agent pulled the runtime errors, found a layout overflow, fixed it
and hot-reloaded — without being shown where the problem was. Without MCP it
could not have seen those offsets and would have been guessing.</p>
<p>The obvious use is the one that scales. You have built a large app and want every
screen checked. Normally you find each overflow by eye, then describe it: "in this
widget, overflow". With the server the agent walks the screens itself and collects
the layout errors. Or you click through yourself and the agent gathers the errors
through MCP.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-it-costs">What it costs<a href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent#what-it-costs" class="hash-link" aria-label="Direct link to What it costs" title="Direct link to What it costs" translate="no">​</a></h2>
<p>The descriptions of all 24 tools land in the context of <strong>every</strong> session and are
reread on every step of the agent. A task takes anywhere from twenty to a few
hundred steps, and every one of them carries the whole list — cached, but still
read, and still sitting in the context the agent has to reason within.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="verdict">Verdict<a href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent#verdict" class="hash-link" aria-label="Direct link to Verdict" title="Direct link to Verdict" translate="no">​</a></h2>
<p>For a tool that matters on the occasional layout bug, that price is paid all the
time. So in DartWay it will not be on by default. We will add it to the framework
as an on-demand tool — connected for a run of visual scenarios or for fixing
layout bugs, and not present in everyday development sessions. Keeping it in
every session is overkill.</p>
<hr>
<p><em>The video version, in Russian, is <a href="https://youtu.be/KRDMVBS_rig" target="_blank" rel="noopener noreferrer" class="">on YouTube</a>.</em></p>]]></content:encoded>
            <category>Ecosystem</category>
            <category>Engineering</category>
        </item>
        <item>
            <title><![CDATA[Serverpod 4 and App Studio: a review from someone who ships with agents]]></title>
            <link>https://dartway.dev/blog/serverpod-4-review</link>
            <guid>https://dartway.dev/blog/serverpod-4-review</guid>
            <pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Full-stack hot reload, Postgres without Docker, an MCP server, offline sync, shared models, a cloud and a desktop studio. What each one is, and how much it matters when most of the code is written by agents in the background.]]></description>
            <content:encoded><![CDATA[<p>Serverpod 4 came out in September, and with it App Studio. We have moved our own
framework off Serverpod, but for fullstack Dart it remains a solid choice — so
here is the release, feature by feature, with what each one is and what it is
worth in practice.</p>
<p>The lens matters, so it goes first. Most of our development no longer happens in
front of a running app. One session holds the conversation about the work; it
starts separate agent sessions for development and for code review; several tasks
run at once, in the background; and the result gets looked at on a test server a
couple of times a day. Several of Serverpod 4's headline features are built for a
different picture — a developer at a desk with the whole stack running locally.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="full-stack-hot-reload">Full-stack hot reload<a href="https://dartway.dev/blog/serverpod-4-review#full-stack-hot-reload" class="hash-link" aria-label="Direct link to Full-stack hot reload" title="Direct link to Full-stack hot reload" translate="no">​</a></h2>
<p>The database, the server and the Flutter app come up with one command. An edit
anywhere — from a model to a screen — is picked up live, code generation
included, and the app keeps its state. No more four terminals and restarting
things by hand.</p>
<p><strong>Worth it?</strong> Convenient, and it barely touches how we work. With agents running
in parallel there is often no local stack up at all: changes go to a test server
and are checked there, and in most cases that is enough. Bringing a stack up was
never the hard part.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="postgres-without-docker-and-an-mcp-server">Postgres without Docker, and an MCP server<a href="https://dartway.dev/blog/serverpod-4-review#postgres-without-docker-and-an-mcp-server" class="hash-link" aria-label="Direct link to Postgres without Docker, and an MCP server" title="Direct link to Postgres without Docker, and an MCP server" translate="no">​</a></h2>
<p>For local development there is now a built-in Postgres, so Docker is optional. And
the running app exposes an MCP server: your coding agent can reach into the live
stack — logs, migrations, the running Flutter app.</p>
<p><strong>Worth it?</strong> Useful on the occasional tricky bug. It does not solve a problem we
actually have: the agent already sees the logs, and in practice it rarely lacks
information to work out what is going on. (We looked at the same idea in Dart's
own MCP server — <a class="" href="https://dartway.dev/blog/dart-mcp-what-it-gives-an-agent">a separate post</a>.)</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="offline-and-a-database-on-the-client">Offline and a database on the client<a href="https://dartway.dev/blog/serverpod-4-review#offline-and-a-database-on-the-client" class="hash-link" aria-label="Direct link to Offline and a database on the client" title="Direct link to Offline and a database on the client" translate="no">​</a></h2>
<p>The same models and ORM now generate SQLite in Flutter, and a single line —
<code>database: sync</code> — turns on two-way sync with the server.</p>
<p><strong>Worth it?</strong> The right direction; DartWay is heading there too. It is marked
experimental for now, and it will take time to mature.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="future-calls-and-shared-models">Future calls and shared models<a href="https://dartway.dev/blog/serverpod-4-review#future-calls-and-shared-models" class="hash-link" aria-label="Direct link to Future calls and shared models" title="Direct link to Future calls and shared models" translate="no">​</a></h2>
<p>Background jobs are declared as ordinary Dart methods, and recurring ones are
supported. Models can live in a separate package shared by the server and the
Flutter app.</p>
<p><strong>Worth it?</strong> Sharing models between frontend and backend is a well-known pain,
and it was one of the reasons we left Serverpod. Now it is there, and it is a
sensible step.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="serverpod-cloud">Serverpod Cloud<a href="https://dartway.dev/blog/serverpod-4-review#serverpod-cloud" class="hash-link" aria-label="Direct link to Serverpod Cloud" title="Direct link to Serverpod Cloud" translate="no">​</a></h2>
<p>Deploy the backend, the database and the web app with one command, from $5 a
month. The framework is free and the business is hosting — a clear model.</p>
<p><strong>Worth it?</strong> Today an agent sets up infrastructure in a couple of prompts: a
server, Docker, the database. And if you do not understand how your server is
built, handing it to someone else is uncomfortable either way.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="app-studio">App Studio<a href="https://dartway.dev/blog/serverpod-4-review#app-studio" class="hash-link" aria-label="Direct link to App Studio" title="Direct link to App Studio" translate="no">​</a></h2>
<p>A desktop app with Flutter, Dart and Serverpod already inside. There is no AI of
its own: you plug in Claude Code, Cursor or any other agent. What comes out is an
ordinary project with its code in your hands — not a closed sandbox like Lovable.</p>
<p><strong>Worth it?</strong> Two reservations.</p>
<p>The first: Serverpod gives you a tool for each task, but it does not tie them
into an architecture. To build everything on that list well you still have to be
the architect of your app, or it turns into a mess. We ran into that many times
when we built on Serverpod directly, before DartWay existed.</p>
<p>The second matters more. Real development does not get stuck on setting up the
environment. A prototype or a first version is easy. It gets hard when there are
many screens and features that have to stay consistent with each other, and a
large set of requirements that must hold precisely. A chat window next to a
running app does not help with that: where do all the requirements live, and how
does it all stay in sync? That is the question that decides whether a real
product gets built, and App Studio does not answer it.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="verdict">Verdict<a href="https://dartway.dev/blog/serverpod-4-review#verdict" class="hash-link" aria-label="Direct link to Verdict" title="Direct link to Verdict" translate="no">​</a></h2>
<p>A good release of a good framework, and worth a look if you build fullstack Dart.
Most of it makes the local loop nicer; the hard problems of agent-driven
development — architecture, and keeping a large product consistent — are left
to you.</p>
<hr>
<p><em>The video version, in Russian, is <a href="https://youtu.be/HL9ja3EwXzY" target="_blank" rel="noopener noreferrer" class="">on YouTube</a>.</em></p>]]></content:encoded>
            <category>Ecosystem</category>
        </item>
        <item>
            <title><![CDATA[Flutter 3.47: what actually breaks, and what to do before November]]></title>
            <link>https://dartway.dev/blog/flutter-3-47-what-breaks</link>
            <guid>https://dartway.dev/blog/flutter-3-47-what-breaks</guid>
            <pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[material and cupertino left the SDK. Nothing breaks today, the migration is one command with two traps, and the most repeated claim about this release — that ChangeNotifier moved out — is not true yet.]]></description>
            <content:encoded><![CDATA[<p>Summer produced exactly one stable Flutter release. 3.45 and 3.46 never reached
stable at all, and 3.47 arrived on 12 August carrying 1358 commits on top of
3.44. The headline is not a feature. It is a repackaging: <strong><code>material</code> and
<code>cupertino</code> moved out of the SDK into separate packages.</strong></p>
<p>That sounds alarming and mostly is not — today. Here is what we found migrating a
real project from 3.44.0 to 3.47.2, and the one widely repeated claim about this
release that does not survive contact with the code.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="nothing-is-on-fire-today">Nothing is on fire today<a href="https://dartway.dev/blog/flutter-3-47-what-breaks#nothing-is-on-fire-today" class="hash-link" aria-label="Direct link to Nothing is on fire today" title="Direct link to Nothing is on fire today" translate="no">​</a></h2>
<p>We took a live project from 3.44.0 to 3.47.2 and ran it: <code>flutter analyze</code> clean,
tests green, the libraries still inside the SDK and not marked deprecated. The
deprecation is scheduled for the <strong>November release</strong>. That is when the clock
starts.</p>
<p>So this is a good month to migrate and a bad month to panic. The window exists
precisely so the migration is boring.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-migration-is-one-command-with-two-traps">The migration is one command, with two traps<a href="https://dartway.dev/blog/flutter-3-47-what-breaks#the-migration-is-one-command-with-two-traps" class="hash-link" aria-label="Direct link to The migration is one command, with two traps" title="Direct link to The migration is one command, with two traps" translate="no">​</a></h2>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token plain">dart fix --apply --code=migrate_design_widgets</span><br></div></code></pre></div></div>
<p><strong>Trap one: plain <code>dart fix --apply</code> will not do it.</strong> The fix is not in the
default set; without <code>--code=migrate_design_widgets</code> the command runs, reports
success, and changes nothing relevant. It is the kind of failure that reads as
"already migrated".</p>
<p><strong>Trap two: it writes <code>material_ui: any</code> into your <code>pubspec</code>.</strong> <code>any</code> means the
next breaking release of that package lands in your build without a word.
Replace it with a caret and a version the moment the fix finishes.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-claim-that-is-not-true-changenotifier-did-not-move">The claim that is not true: <code>ChangeNotifier</code> did not move<a href="https://dartway.dev/blog/flutter-3-47-what-breaks#the-claim-that-is-not-true-changenotifier-did-not-move" class="hash-link" aria-label="Direct link to the-claim-that-is-not-true-changenotifier-did-not-move" title="Direct link to the-claim-that-is-not-true-changenotifier-did-not-move" translate="no">​</a></h2>
<p>The most repeated line about this release is that <code>ChangeNotifier</code> and friends
were pulled out of the SDK into <code>package:listen</code>. In 3.47.2 that is not what
happened.</p>
<p>The classes are still in the SDK, and they are not marked deprecated. <code>listen</code>
is a <strong>second, independent implementation of the same names</strong> — not a new home
for the old ones. The two are separate types, and the compiler treats them that
way:</p>
<ul>
<li class="">hand a <code>listen</code> <code>ValueNotifier</code> to a Flutter <code>ValueListenableBuilder</code> and you
get a compile error, not a subtle runtime bug;</li>
<li class="">the same applies in reverse, and to every other pair with a shared name.</li>
</ul>
<p>Where <code>listen</code> earns its place is code that has no Flutter in it: a server, a
CLI, logic shared between the two. There the SDK types were never available and
now there is an equivalent. Inside a Flutter app, switching to it today buys
nothing and costs you type compatibility with every widget that expects the SDK
version.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="primary-constructors-will-not-turn-themselves-on">Primary constructors will not turn themselves on<a href="https://dartway.dev/blog/flutter-3-47-what-breaks#primary-constructors-will-not-turn-themselves-on" class="hash-link" aria-label="Direct link to Primary constructors will not turn themselves on" title="Direct link to Primary constructors will not turn themselves on" translate="no">​</a></h2>
<p>Dart 3.13 ships primary constructors, and they are behind a language version
gate. Without <code>sdk: ^3.13.0</code> in <code>pubspec.yaml</code> you get <code>experiment_not_enabled</code>
even on the right SDK — the toolchain is correct and the project is not asking.</p>
<p>Worth saying plainly, though: the value of saving constructor boilerplate drops
a long way once the boilerplate is not written by a human. If your constructors
are generated, this is a readability change, not a productivity one.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-rest">The rest<a href="https://dartway.dev/blog/flutter-3-47-what-breaks#the-rest" class="hash-link" aria-label="Direct link to The rest" title="Direct link to The rest" translate="no">​</a></h2>
<p>Impeller by default on desktop, iOS 15 and macOS 12 as the new floors, and the
Wasm work — all real, none of it urgent, and none of it changes what you do this
month.</p>
<p><strong>What to actually do before November:</strong> run the migration on a branch, pin
<code>material_ui</code> properly, and leave <code>ChangeNotifier</code> alone.</p>
<hr>
<p><em>Checked by migrating a live project from 3.44.0 to 3.47.2 — <code>before/</code> and
<code>after/</code> each build and pass their tests on their own SDK. The video walkthrough
is <a href="https://youtu.be/kNXMVEj_7bA" target="_blank" rel="noopener noreferrer" class="">on YouTube</a>.</em></p>]]></content:encoded>
            <category>Ecosystem</category>
            <category>Engineering</category>
        </item>
        <item>
            <title><![CDATA[One account, two doors: what phone-or-email login really costs]]></title>
            <link>https://dartway.dev/blog/one-account-two-doors</link>
            <guid>https://dartway.dev/blog/one-account-two-doors</guid>
            <pubDate>Sat, 05 Sep 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Letting users sign in with either a phone number or an email looks like a one-line change. It is a data-model change, and the bill arrives as duplicate accounts you cannot merge.]]></description>
            <content:encoded><![CDATA[<p>Letting people sign in with <strong>either</strong> a phone number or an email looks like a
one-line change: widen the lookup, accept both. We shipped it last week in a live
project, and the one-line change turned out to be a data-model change with a bill
attached.</p>
<p>Here is what it actually costs, in the order the costs show up.</p>
<!-- -->
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-lookup-was-never-the-problem">The lookup was never the problem<a href="https://dartway.dev/blog/one-account-two-doors#the-lookup-was-never-the-problem" class="hash-link" aria-label="Direct link to The lookup was never the problem" title="Direct link to The lookup was never the problem" translate="no">​</a></h2>
<p>The framework stored a single <code>userIdentifier</code> column and matched it by strict
equality. Ask it to find a profile by phone <em>or</em> email and there is no seam to
extend — not because the query is hard, but because authentication itself requires
that column to exist. Remove it and the core fails at startup.</p>
<p>So the first fix is not "search two fields". It is <strong>giving the application a say
in how a user is found</strong>:</p>
<div class="language-dart codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#F8F8F2;--prism-background-color:#282A36"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-dart codeBlock_bY9V thin-scrollbar" style="color:#F8F8F2;background-color:#282A36"><code class="codeBlockLines_e6Vv"><div class="token-line" style="color:#F8F8F2"><span class="token class-name">DwAuthConfig</span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  findUserProfileByIdentifier</span><span class="token punctuation" style="color:rgb(248, 248, 242)">:</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">(</span><span class="token plain">session</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"> identifier</span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token plain"> </span><span class="token keyword" style="color:rgb(189, 147, 249);font-style:italic">async</span><span class="token plain"> </span><span class="token punctuation" style="color:rgb(248, 248, 242)">{</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">    </span><span class="token comment" style="color:rgb(98, 114, 164)">// the app decides what "the same person" means</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain">  </span><span class="token punctuation" style="color:rgb(248, 248, 242)">}</span><span class="token punctuation" style="color:rgb(248, 248, 242)">,</span><span class="token plain"></span><br></div><div class="token-line" style="color:#F8F8F2"><span class="token plain"></span><span class="token punctuation" style="color:rgb(248, 248, 242)">)</span><span class="token punctuation" style="color:rgb(248, 248, 242)">;</span><br></div></code></pre></div></div>
<p>The column stops being mandatory, the identifier arrives already normalised, and
the framework stops pretending it knows what identity means in your domain. Three
obligations move to the application, and the docs say so out loud — a resolver that
silently does the wrong thing is worse than no resolver.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="searching-two-fields-fixes-nothing-on-its-own">Searching two fields fixes nothing on its own<a href="https://dartway.dev/blog/one-account-two-doors#searching-two-fields-fixes-nothing-on-its-own" class="hash-link" aria-label="Direct link to Searching two fields fixes nothing on its own" title="Direct link to Searching two fields fixes nothing on its own" translate="no">​</a></h2>
<p>This is the part that surprised us, and it is the reason this post exists.</p>
<p>A user who signed up by phone has an <strong>empty email column</strong>. Searching both fields
finds nothing new, because there is nothing in the second field. The lookup is
correct and useless at the same time.</p>
<p>To fill that column you need a way to attach a second channel to an existing
account — which means <code>addAuthProvider</code> and <code>changeIdentifier</code> have to actually
work. In our case both threw <code>UnimplementedError</code>, and there was no screen for it
in any mockup. Until that exists, <strong>signing in from the other channel keeps quietly
creating a second account</strong>, exactly as before.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-duplicates-are-already-there">The duplicates are already there<a href="https://dartway.dev/blog/one-account-two-doors#the-duplicates-are-already-there" class="hash-link" aria-label="Direct link to The duplicates are already there" title="Direct link to The duplicates are already there" translate="no">​</a></h2>
<p>Then comes the part you cannot postpone. We went to add unique indexes on <code>phone</code>
and <code>email</code> and found there were <strong>no unique indexes at all</strong> — not on the new
columns, not on the old <code>userIdentifier</code>. Two rows with the same value were already
legal, and the lookup resolved to whichever one the database felt like returning.</p>
<p>You cannot add the index until the duplicates are merged. And the duplicates exist
precisely <em>because</em> login from a second channel used to create a new account. The
bug and its cleanup are the same object.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="merging-is-an-event-not-an-action">Merging is an event, not an action<a href="https://dartway.dev/blog/one-account-two-doors#merging-is-an-event-not-an-action" class="hash-link" aria-label="Direct link to Merging is an event, not an action" title="Direct link to Merging is an event, not an action" translate="no">​</a></h2>
<p>Account merge is irreversible: one profile absorbs another, and afterwards nobody
can reconstruct what came from where. So we modelled it as a <code>ProfileMerge</code> <strong>event</strong>
rather than a method call — who merged, whom, and what moved. An action leaves the
database consistent and the story gone; an event leaves both.</p>
<h2 class="anchor anchorTargetStickyNavbar_Vzrq" id="what-we-would-tell-ourselves-a-week-ago">What we would tell ourselves a week ago<a href="https://dartway.dev/blog/one-account-two-doors#what-we-would-tell-ourselves-a-week-ago" class="hash-link" aria-label="Direct link to What we would tell ourselves a week ago" title="Direct link to What we would tell ourselves a week ago" translate="no">​</a></h2>
<ul>
<li class="">"Login by phone or email" is not an auth-screen task. It is a <strong>profile identity</strong>
task, and the screen is the last five percent.</li>
<li class="">Ship the <strong>attach-a-second-channel</strong> flow in the same release. Without it, the
feature is a lookup that finds nothing.</li>
<li class="">Check for unique indexes <strong>before</strong> you promise anything. Ours were missing for the
entire life of the project, and nobody noticed because nothing enforced them.</li>
<li class="">Verify on a live database with a real round trip — phone signup, email attach,
email login into the same account, one row in the table. We had 23 server tests
including that pass; the reading-the-code version of this article would have been
wrong in two places.</li>
</ul>
<p>The framework side landed in <code>dartway</code> core <code>0.12.1</code> — the auth resolver and a
working <code>changeIdentifier</code>. If you are running the earlier version, the migration is
the interesting part, not the API.</p>]]></content:encoded>
            <category>Engineering</category>
            <category>Case breakdown</category>
        </item>
    </channel>
</rss>