<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jasper</title>
    <description>The latest articles on DEV Community by Jasper (@ianjasperrr).</description>
    <link>https://dev.to/ianjasperrr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3928284%2Fc0418c8f-090b-4f7a-b3a6-501f5f8e795c.png</url>
      <title>DEV Community: Jasper</title>
      <link>https://dev.to/ianjasperrr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ianjasperrr"/>
    <language>en</language>
    <item>
      <title>Flutter Deep Links for Beginners: A Production Setup Checklist</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Wed, 10 Jun 2026 01:37:37 +0000</pubDate>
      <link>https://dev.to/ianjasperrr/flutter-deep-links-for-beginners-a-production-setup-checklist-3pab</link>
      <guid>https://dev.to/ianjasperrr/flutter-deep-links-for-beginners-a-production-setup-checklist-3pab</guid>
      <description>&lt;p&gt;Marketing sent the email. The link opened the app store instead of the promo screen. Support tickets mentioned "the link worked on Android but not iPhone." QA could reproduce it only on a physical device, never in the simulator.&lt;/p&gt;

&lt;p&gt;If you are new to Flutter deep linking, that story probably sounds familiar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a deep link?&lt;/strong&gt; A URL that opens your app and navigates to a specific screen, not just the home screen. Example: &lt;code&gt;https://example.com/promo/123&lt;/code&gt; should open your app directly on the promo details page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why beginners struggle:&lt;/strong&gt; three separate systems must agree before that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your &lt;strong&gt;website&lt;/strong&gt; (verification files that prove you own the domain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS or Android native config&lt;/strong&gt; (entitlements, manifest entries)&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;Flutter router&lt;/strong&gt; (Dart code that maps the URL path to a screen)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deep links look simple in a demo. In production, one misconfigured file fails silently.&lt;/p&gt;

&lt;p&gt;We ship &lt;a href="https://spice-factory.ph/services#mobile-development" rel="noopener noreferrer"&gt;cross-platform mobile delivery&lt;/a&gt; with Flutter teams who need email, SMS, web, and ad campaigns to land on the right in-app screen. This post is the checklist we run before calling deep links done, with definitions for terms tutorials often skip. For platform behavior and Router vs Navigator handling, start with Flutter's &lt;a href="https://docs.flutter.dev/ui/navigation/deep-linking" rel="noopener noreferrer"&gt;Deep linking&lt;/a&gt; overview.&lt;/p&gt;




&lt;h2&gt;
  
  
  How a deep link works (mental model)
&lt;/h2&gt;

&lt;p&gt;When someone taps a link, this is the flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User taps&lt;/strong&gt; &lt;code&gt;https://example.com/promo/123&lt;/code&gt; in email, SMS, or a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phone checks&lt;/strong&gt; whether your app is allowed to handle that domain (verification files on your server + native app config).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OS opens the app&lt;/strong&gt; (if installed and verified) and passes the full URL to Flutter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your router reads the path&lt;/strong&gt; (&lt;code&gt;/promo/123&lt;/code&gt;) and shows the matching screen.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two terms you will see everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cold start:&lt;/strong&gt; the app was fully closed. The OS launches it and delivers the link shortly after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm start:&lt;/strong&gt; the app was in the background. The OS wakes it and passes a new link while it is already running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cold start is where most beginner bugs hide (auth not ready, router not listening yet).&lt;/p&gt;




&lt;h2&gt;
  
  
  Terms you'll see in every tutorial
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Plain meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Deep link&lt;/td&gt;
&lt;td&gt;URL that opens a specific in-app screen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Universal Link (iOS)&lt;/td&gt;
&lt;td&gt;HTTPS deep link verified by Apple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App Link (Android)&lt;/td&gt;
&lt;td&gt;Same idea on Android&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AASA&lt;/td&gt;
&lt;td&gt;Apple's domain verification file (&lt;code&gt;apple-app-site-association&lt;/code&gt;) on your website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;assetlinks.json&lt;/td&gt;
&lt;td&gt;Android's domain verification file on your website&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom URL scheme&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;myapp://&lt;/code&gt; shortcut; easy to demo locally, weak for email campaigns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intent filter&lt;/td&gt;
&lt;td&gt;Android manifest entry that says "this app handles these URLs"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Official setup guides for each platform are in References at the end.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Deep Links Matter (And When They Do Not)
&lt;/h2&gt;

&lt;p&gt;You do not need deep links on day one of every app. Add them when a URL in email, SMS, or ads should land on a specific in-app screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth the setup cost:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Passwordless login or magic links (login email opens the app to the confirm screen)&lt;/li&gt;
&lt;li&gt;Referral and invite flows (friend's invite link opens signup with code pre-filled)&lt;/li&gt;
&lt;li&gt;Push notification taps to specific content (notification opens the order detail screen)&lt;/li&gt;
&lt;li&gt;Email campaigns to product screens (newsletter link opens the sale page in-app)&lt;/li&gt;
&lt;li&gt;Web-to-app handoff from marketing sites (landing page link opens the app if installed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Often overbuilt early:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every blog URL mapping to an in-app WebView&lt;/li&gt;
&lt;li&gt;Complex deferred deep linking before v1 has retention data&lt;/li&gt;
&lt;li&gt;Custom URL schemes as the only path (see below)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope the first release. Ship Universal Links / App Links for two or three high-value paths, then expand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Custom Schemes vs Universal / App Links
&lt;/h2&gt;

&lt;p&gt;If you are new to Flutter deep linking, start with verified HTTPS links on a domain you own.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Custom URL scheme&lt;/td&gt;
&lt;td&gt;&lt;code&gt;myapp://promo/123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast to prototype&lt;/td&gt;
&lt;td&gt;Not verified; conflicts with other apps; email clients may block&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS Universal Links&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://example.com/promo/123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verified domain; opens app if installed&lt;/td&gt;
&lt;td&gt;Requires apple-app-site-association (AASA) file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android App Links&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://example.com/promo/123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verified domain; avoids disambiguation dialog&lt;/td&gt;
&lt;td&gt;Requires assetlinks.json&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Production default:&lt;/strong&gt; HTTPS links on a domain you control, with platform verification files served correctly. Keep a custom scheme only as fallback for legacy campaigns if needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beginner recommendation:&lt;/strong&gt; prototype with a custom scheme locally if that helps you learn routing. Ship real campaigns with Universal Links / App Links.&lt;/p&gt;

&lt;p&gt;Platform setup walkthroughs: &lt;a href="https://docs.flutter.dev/cookbook/navigation/set-up-universal-links" rel="noopener noreferrer"&gt;Set up universal links for iOS&lt;/a&gt; and &lt;a href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links" rel="noopener noreferrer"&gt;Set up app links for Android&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  iOS: Universal Links Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Associated Domains entitlement
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; tells iOS "this app is allowed to open links from example.com."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to click in Xcode:&lt;/strong&gt; Runner target → Signing &amp;amp; Capabilities → Associated Domains → add entries like (see Apple's &lt;a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" rel="noopener noreferrer"&gt;Supporting associated domains&lt;/a&gt; for entitlement and CDN details):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;applinks&lt;/span&gt;:&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;span class="n"&gt;applinks&lt;/span&gt;:&lt;span class="n"&gt;www&lt;/span&gt;.&lt;span class="n"&gt;example&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match every host you use in marketing links. Missing &lt;code&gt;www&lt;/code&gt; variants break half your campaigns.&lt;/p&gt;

&lt;h3&gt;
  
  
  apple-app-site-association (AASA)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; a JSON file on your server that proves you own the domain and lists which app may open which paths.&lt;/p&gt;

&lt;p&gt;Host at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://example.com/.well-known/apple-app-site-association
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Served over &lt;strong&gt;HTTPS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No redirects&lt;/strong&gt; on the AASA URL (common production bug)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Content-Type&lt;/code&gt; preferably &lt;code&gt;application/json&lt;/code&gt; (Apple is tolerant but avoid &lt;code&gt;text/html&lt;/code&gt; error pages)&lt;/li&gt;
&lt;li&gt;Include correct &lt;code&gt;appID&lt;/code&gt; (Team ID + bundle identifier) and &lt;code&gt;paths&lt;/code&gt; array&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Validate with Apple's CDN cache tool or third-party AASA validators after every deploy. File format and in-app handling: &lt;a href="https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app" rel="noopener noreferrer"&gt;Supporting universal links in your app&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutter handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; Dart code that receives the URL from iOS and calls your router to show the right screen.&lt;/p&gt;

&lt;p&gt;Packages commonly used: &lt;a href="https://pub.dev/packages/app_links" rel="noopener noreferrer"&gt;&lt;code&gt;app_links&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;uni_links&lt;/code&gt; (legacy), or routing packages with link APIs (&lt;code&gt;go_router&lt;/code&gt; deep link support). See Flutter's &lt;a href="https://docs.flutter.dev/ui/navigation/deep-linking" rel="noopener noreferrer"&gt;Deep linking&lt;/a&gt; for default handler behavior.&lt;/p&gt;

&lt;p&gt;If you use a third-party link plugin, opt out of Flutter's built-in handler: set &lt;code&gt;FlutterDeepLinkingEnabled&lt;/code&gt; to &lt;code&gt;NO&lt;/code&gt; in &lt;code&gt;Info.plist&lt;/code&gt;. Otherwise both handlers may fire and cause double navigation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Listen for initial link on cold start (&lt;code&gt;getInitialLink&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Subscribe to stream for warm start (&lt;code&gt;uriLinkStream&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Parse path and query params&lt;/li&gt;
&lt;li&gt;Navigate via your router (&lt;code&gt;go_router&lt;/code&gt;, &lt;code&gt;auto_route&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Cold start race:&lt;/strong&gt; routing may run before auth state loads. Queue the pending deep link until session is ready, then navigate once.&lt;/p&gt;




&lt;h2&gt;
  
  
  Android: App Links Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Intent filters in AndroidManifest.xml
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; tell Android which URLs should open your app instead of the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where to edit:&lt;/strong&gt; &lt;code&gt;android/app/src/main/AndroidManifest.xml&lt;/code&gt;, inside the &lt;code&gt;&amp;lt;activity&amp;gt;&lt;/code&gt; tag for &lt;code&gt;.MainActivity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For verified App Links, use &lt;code&gt;android:autoVerify="true"&lt;/code&gt; on intent filters with &lt;code&gt;https&lt;/code&gt; scheme, host, and pathPrefix or pathPattern (see &lt;a href="https://docs.flutter.dev/cookbook/navigation/set-up-app-links" rel="noopener noreferrer"&gt;Set up app links for Android&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Example concept (adjust to your package):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;intent-filter&lt;/span&gt; &lt;span class="na"&gt;android:autoVerify=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;action&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.action.VIEW"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.DEFAULT"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;category&lt;/span&gt; &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"android.intent.category.BROWSABLE"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;data&lt;/span&gt; &lt;span class="na"&gt;android:scheme=&lt;/span&gt;&lt;span class="s"&gt;"https"&lt;/span&gt; &lt;span class="na"&gt;android:host=&lt;/span&gt;&lt;span class="s"&gt;"example.com"&lt;/span&gt; &lt;span class="na"&gt;android:pathPrefix=&lt;/span&gt;&lt;span class="s"&gt;"/promo"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  assetlinks.json
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; proves your app is the official handler for URLs on your domain.&lt;/p&gt;

&lt;p&gt;Host at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://example.com/.well-known/assetlinks.json
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Include SHA-256 cert fingerprints for &lt;strong&gt;release&lt;/strong&gt; and &lt;strong&gt;upload&lt;/strong&gt; keys. If this is your first Play Store release, get the SHA-256 from Play Console → App integrity, not your local debug key. Play App Signing uses Google-held keys; fetch fingerprints from Play Console. Format and hosting rules: &lt;a href="https://developer.android.com/training/app-links/configure-assetlinks" rel="noopener noreferrer"&gt;Configure assetlinks.json&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run (see &lt;a href="https://developer.android.com/training/app-links/verify-applinks" rel="noopener noreferrer"&gt;Verify Android App Links&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell pm get-app-links com.example.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; this command shows whether Android trusts your app for that domain. Confirm &lt;code&gt;verified&lt;/code&gt; status on a release build, not debug-only.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutter parity
&lt;/h3&gt;

&lt;p&gt;Same listener pattern as iOS. If you use a third-party link plugin, set &lt;code&gt;flutter_deeplinking_enabled&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; in &lt;code&gt;AndroidManifest.xml&lt;/code&gt; (same opt-out as &lt;code&gt;FlutterDeepLinkingEnabled&lt;/code&gt; on iOS). Test on physical devices: emulators miss many intent resolution edge cases.&lt;/p&gt;




&lt;h2&gt;
  
  
  Flutter Router Integration (go_router Example)
&lt;/h2&gt;

&lt;p&gt;The incoming URL path must match a route you defined in Dart. Example: URL &lt;code&gt;https://example.com/promo/123&lt;/code&gt; → path &lt;code&gt;/promo/123&lt;/code&gt; → route &lt;code&gt;/promo/:id&lt;/code&gt; → &lt;code&gt;PromoScreen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Conceptual flow with &lt;code&gt;go_router&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GoRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;routes:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HomeScreen&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="n"&gt;GoRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;path:&lt;/span&gt; &lt;span class="s"&gt;'/promo/:id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// path from https://example.com/promo/123&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;PromoScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire incoming URIs to &lt;code&gt;router.go('/promo/$id')&lt;/code&gt; or equivalent after normalizing host and path. Official cookbooks use &lt;code&gt;go_router&lt;/code&gt;; see the &lt;a href="https://pub.dev/documentation/go_router/latest/topics/Deep%20linking-topic.html" rel="noopener noreferrer"&gt;go_router deep linking topic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalize paths:&lt;/strong&gt; marketing may send trailing slashes, UTM query strings, or mixed casing. Strip tracking params before route matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guard routes:&lt;/strong&gt; if &lt;code&gt;/account/orders/:id&lt;/code&gt; requires login, redirect to sign-in with return path stored, then resume after auth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backend and Web Fallback URLs
&lt;/h2&gt;

&lt;p&gt;Deep links often start on the web. Plan three outcomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;App installed:&lt;/strong&gt; open app to target screen (Universal / App Link)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App not installed:&lt;/strong&gt; show mobile web equivalent or store landing page&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop click:&lt;/strong&gt; responsive web page, not a broken custom scheme&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your web server should serve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AASA and assetlinks files&lt;/li&gt;
&lt;li&gt;Smart app banner meta tags optional on iOS Safari&lt;/li&gt;
&lt;li&gt;Consistent URL structure between web routes and in-app routes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backend teams own path conventions. If web uses &lt;code&gt;/promo/123&lt;/code&gt; but Flutter expects &lt;code&gt;/promotions?id=123&lt;/code&gt;, campaigns will fail despite valid platform files. Align &lt;a href="https://spice-factory.ph/services#system-development" rel="noopener noreferrer"&gt;backend and API integration for web products&lt;/a&gt; with mobile routing early.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing on Real Devices
&lt;/h2&gt;

&lt;p&gt;Simulator-only testing misses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Email client in-app browsers (Gmail, Outlook)&lt;/li&gt;
&lt;li&gt;SMS link handling&lt;/li&gt;
&lt;li&gt;Chrome Custom Tabs vs default browser on Android&lt;/li&gt;
&lt;li&gt;iOS pasteboard and long-press "Open in App" behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test matrix (minimum):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;iOS physical&lt;/th&gt;
&lt;th&gt;Android physical&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Notes app pasted URL&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email tap&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SMS tap&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile Safari / Chrome address bar&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start (app killed)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warm start (background)&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Log parsed URI and final route in debug builds during QA week. Before physical QA, run &lt;a href="https://docs.flutter.dev/tools/devtools/deep-links" rel="noopener noreferrer"&gt;Validate deep links (DevTools)&lt;/a&gt; to catch manifest, AASA, and assetlinks misconfigurations early.&lt;/p&gt;




&lt;h2&gt;
  
  
  When it breaks (and what you'll see)
&lt;/h2&gt;

&lt;p&gt;These bugs often look like Flutter routing problems but are usually config issues on the website or native side.&lt;/p&gt;

&lt;h3&gt;
  
  
  AASA served with redirect
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; link always opens Safari instead of your app.&lt;/p&gt;

&lt;p&gt;CDN or www redirect breaks iOS verification. AASA URL must return 200 directly. Apple caches AASA via its CDN (&lt;a href="https://developer.apple.com/documentation/xcode/supporting-associated-domains" rel="noopener noreferrer"&gt;Supporting associated domains&lt;/a&gt;), so fixes may not propagate to devices immediately after deploy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrong Team ID or bundle ID in AASA
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; iOS works in local dev builds but never in TestFlight or App Store builds.&lt;/p&gt;

&lt;p&gt;Links open Safari instead of app with no obvious error.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debug keystore fingerprint in assetlinks.json only
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; works when you run from Android Studio, fails after installing from Play Store.&lt;/p&gt;

&lt;p&gt;Works on dev builds, fails on Play Store builds. See &lt;a href="https://developer.android.com/training/app-links/troubleshoot" rel="noopener noreferrer"&gt;Troubleshoot App Links&lt;/a&gt; for uppercase SHA-256, redirect, and Play App Signing mismatches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Query param stripping by email ESP
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; link opens the app but lands on the wrong screen or home screen.&lt;/p&gt;

&lt;p&gt;Use path params for critical IDs when possible; treat query as optional metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Router navigates before auth
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; user lands on login screen; promo or order screen never appears.&lt;/p&gt;

&lt;p&gt;User lands on protected screen, bounced to login, loses deep link context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple listeners
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; screen flashes or navigates twice.&lt;/p&gt;

&lt;p&gt;Two plugins subscribing cause double navigation or race crashes. One ownership layer for incoming links.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Release Checklist
&lt;/h2&gt;

&lt;p&gt;Work top to bottom: website files → native config → Flutter router → device tests.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Domain hosts valid AASA and assetlinks.json without redirects&lt;/li&gt;
&lt;li&gt;[ ] All marketing hosts (&lt;code&gt;www&lt;/code&gt;, apex) covered in entitlements and intent filters&lt;/li&gt;
&lt;li&gt;[ ] Release signing fingerprints in assetlinks.json (Play App Signing aware)&lt;/li&gt;
&lt;li&gt;[ ] Flutter router maps every campaign path to a screen&lt;/li&gt;
&lt;li&gt;[ ] Auth-gated routes queue and resume deep links&lt;/li&gt;
&lt;li&gt;[ ] Web fallback pages exist for app-not-installed case&lt;/li&gt;
&lt;li&gt;[ ] Tested cold/warm start on physical iOS and Android&lt;/li&gt;
&lt;li&gt;[ ] Tested from email and SMS, not only pasted URLs&lt;/li&gt;
&lt;li&gt;[ ] Analytics events on deep link open and conversion funnel&lt;/li&gt;
&lt;li&gt;[ ] Runbook for marketing: approved URL format doc shared with growth team&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A deep link is a URL that opens a &lt;strong&gt;specific screen&lt;/strong&gt;, not just your app icon.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;verified HTTPS links&lt;/strong&gt; (Universal Links / App Links) for real campaigns, not custom schemes alone.&lt;/li&gt;
&lt;li&gt;Your &lt;strong&gt;website&lt;/strong&gt;, &lt;strong&gt;native config&lt;/strong&gt;, and &lt;strong&gt;Flutter router&lt;/strong&gt; must all agree on the same paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AASA&lt;/strong&gt; (iOS) and &lt;strong&gt;assetlinks.json&lt;/strong&gt; (Android) must be served correctly on every campaign host, with no redirects.&lt;/li&gt;
&lt;li&gt;Test on &lt;strong&gt;real devices&lt;/strong&gt; from email and SMS, not only simulators; cold start is where most first-time bugs show up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which broke last on your project: iOS verification, Android autoVerify, or Flutter routing after cold start?&lt;/p&gt;

&lt;p&gt;If deep links are in scope for your next release, &lt;a href="https://spice-factory.ph/contact" rel="noopener noreferrer"&gt;scoping a mobile product with deep link requirements&lt;/a&gt; early keeps platform files, router design, and campaign URLs on the same checklist before store submission.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>deeplink</category>
      <category>routes</category>
      <category>applink</category>
    </item>
    <item>
      <title>State Management in Production Flutter Apps: What Actually Held Up at Scale</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Thu, 28 May 2026 00:07:57 +0000</pubDate>
      <link>https://dev.to/ianjasperrr/state-management-in-production-flutter-apps-what-actually-held-up-at-scale-5bdd</link>
      <guid>https://dev.to/ianjasperrr/state-management-in-production-flutter-apps-what-actually-held-up-at-scale-5bdd</guid>
      <description>&lt;p&gt;State management rarely feels urgent on week one of a Flutter project.&lt;/p&gt;

&lt;p&gt;Screens come together fast. &lt;code&gt;setState&lt;/code&gt; works. Provider or Riverpod gets wired up in an afternoon. Demos look great in the simulator.&lt;/p&gt;

&lt;p&gt;Then month four or five hits. A second developer joins. You add offline-friendly flows, push notifications, and deeper API integration. Navigation stacks get taller. The same bug shows up on two different screens. Suddenly the state choices you made early are everywhere, and changing them feels expensive.&lt;/p&gt;

&lt;p&gt;I've been shipping Flutter apps on &lt;a href="https://spice-factory.ph/services/mobile-development" rel="noopener noreferrer"&gt;cross-platform mobile work&lt;/a&gt; for clients who need store-ready iOS and Android builds, not just a polished prototype. These are the patterns that held up in production, and the ones that did not.&lt;/p&gt;

&lt;p&gt;This is not a framework ranking. It is what we saw once real users, real releases, and real teammates entered the picture.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why State Management Decisions Show Up Late in Flutter Projects
&lt;/h2&gt;

&lt;p&gt;Flutter makes it easy to defer architectural decisions. Widgets compose quickly. Hot reload hides how tangled things are becoming. Async work, platform channels, and auth flows often land after the first sprint demo.&lt;/p&gt;

&lt;p&gt;By the time pain shows up, state is spread across parent widgets, inherited notifiers, and ad-hoc service singletons. Refactoring feels risky because nobody is sure which screen owns which piece of data.&lt;/p&gt;

&lt;h3&gt;
  
  
  The symptoms we noticed first
&lt;/h3&gt;

&lt;p&gt;These showed up before anyone said "we picked the wrong library":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screens that were fast to build but painful to change&lt;/li&gt;
&lt;li&gt;Duplicate API calls after navigation pop/push cycles&lt;/li&gt;
&lt;li&gt;Bug fixes that resurfaced in a different widget tree branch&lt;/li&gt;
&lt;li&gt;QA reports that only reproduced on one platform build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is unique to Flutter. It is what happens when UI state, domain state, and API-driven data get mixed without clear boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  What "production scale" actually meant for us
&lt;/h3&gt;

&lt;p&gt;For our client apps, production scale was not millions of users on day one. It usually meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More features shipping after the first store release&lt;/li&gt;
&lt;li&gt;More developers touching the same modules&lt;/li&gt;
&lt;li&gt;More edge cases around slow networks, stale tokens, and partial offline behavior&lt;/li&gt;
&lt;li&gt;Post-MVP iteration on a codebase that still had to pass App Store and Google Play review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is when state management stops being a tutorial topic and starts being a delivery constraint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five Lessons From Real Flutter Client Apps
&lt;/h2&gt;

&lt;p&gt;These lessons came from shipping and maintaining Flutter projects, not from comparing packages in isolation. Your stack may differ. The trade-offs probably will not.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. setState and local state are fine until they are not
&lt;/h3&gt;

&lt;p&gt;Local state still makes sense for isolated UI: toggles, form field focus, animation controllers, short-lived modal flows. We still use it in those cases.&lt;/p&gt;

&lt;p&gt;It became a problem when business logic crept into &lt;code&gt;StatefulWidget&lt;/code&gt; classes. Fetching data, handling errors, and coordinating navigation from one screen's &lt;code&gt;setState&lt;/code&gt; block made that screen the hidden owner of behavior other features needed later.&lt;/p&gt;

&lt;p&gt;We also paid for it in testing. Widgets that mixed layout, side effects, and API calls were harder to exercise on both iOS and Android CI builds. Splitting view logic from data flow, even in small steps, made regressions easier to catch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb we use now:&lt;/strong&gt; if another screen might need this data within two sprints, local &lt;code&gt;setState&lt;/code&gt; is probably too local.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Provider got us moving; Riverpod and Bloc earned their keep later
&lt;/h3&gt;

&lt;p&gt;Early in MVP delivery, &lt;strong&gt;Provider&lt;/strong&gt; was often enough. The team could move quickly, dependencies were familiar, and the learning curve stayed low. That matters when you are trying to reach a first release on a fixed timeline.&lt;/p&gt;

&lt;p&gt;As features accumulated, we leaned more on &lt;strong&gt;Riverpod&lt;/strong&gt; or &lt;strong&gt;Bloc&lt;/strong&gt; in modules where async boundaries and testability mattered most. Riverpod's explicit providers and overrides helped us reason about dependencies in larger apps. Bloc's event/state separation made complex flows (auth refresh, paginated lists, multi-step forms) easier to trace in code review.&lt;/p&gt;

&lt;p&gt;We did not migrate everything at once. Mixed patterns are fine if they are &lt;strong&gt;intentional&lt;/strong&gt;: one primary approach per feature folder, documented in a short README or ADR note.&lt;/p&gt;

&lt;p&gt;A pattern we reuse for list screens looks like this (Riverpod example):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ordersProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FutureProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;autoDispose&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchOrders&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The point is not the syntax. It is that loading, success, and failure have a predictable home instead of living inside a widget's &lt;code&gt;initState&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;See the &lt;a href="https://riverpod.dev/" rel="noopener noreferrer"&gt;Riverpod documentation&lt;/a&gt; and &lt;a href="https://bloclibrary.dev/" rel="noopener noreferrer"&gt;Bloc library docs&lt;/a&gt; when you want deeper references. We treat them as tools, not identity.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Global state is rarely the whole answer
&lt;/h3&gt;

&lt;p&gt;It is tempting to put "the app state" in one global store and call it done. That worked until features had different lifecycles, permissions, and refresh rules.&lt;/p&gt;

&lt;p&gt;What helped us more was scoping state by feature and separating layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UI state:&lt;/strong&gt; expanded panels, selected tabs, scroll position&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain state:&lt;/strong&gt; cart contents, draft form values, in-progress job status&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote data:&lt;/strong&gt; API responses cached with clear invalidation rules&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A repository layer became the stable seam for backend integration. Widgets and notifiers depended on repositories, not raw HTTP clients scattered through the tree. When an endpoint changed, we fixed one place instead of five screens.&lt;/p&gt;

&lt;p&gt;Global singletons still exist in our apps (session, environment config, analytics). They are just not where feature logic lives.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Error, loading, and empty states need a first-class plan
&lt;/h3&gt;

&lt;p&gt;Happy-path-only UI is the fastest way to ship a demo and the slowest way to stabilize a production app.&lt;/p&gt;

&lt;p&gt;We lost time to bugs that were really missing state models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infinite spinners when an API returned an empty list&lt;/li&gt;
&lt;li&gt;Stale data shown after a failed refresh&lt;/li&gt;
&lt;li&gt;Retry buttons that fired duplicate requests&lt;/li&gt;
&lt;li&gt;Error messages that disappeared on navigation and never came back&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once we modeled async work as explicit phases (loading, success, empty, error), tests got useful and support tickets dropped. &lt;code&gt;AsyncValue&lt;/code&gt; in Riverpod and sealed state classes in Bloc both push you in that direction. Even with Provider, a small wrapper type for &lt;code&gt;Resource&amp;lt;T&amp;gt;&lt;/code&gt; or similar beats treating &lt;code&gt;null&lt;/code&gt; as "still loading."&lt;/p&gt;

&lt;p&gt;Offline-adjacent behavior does not require a full offline-first architecture on day one. It does require deciding what the UI should show when the network is slow or unavailable, before users report it in reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Tests and onboarding broke our early "clever" patterns
&lt;/h3&gt;

&lt;p&gt;Clever abstractions age badly when only one person understands them.&lt;/p&gt;

&lt;p&gt;We had listeners buried in widget trees, magic &lt;code&gt;context.read&lt;/code&gt; calls after async gaps, and "temporary" global notifiers that never left. Refactors broke silently. New developers copied the wrong pattern because it was the fastest path to green builds.&lt;/p&gt;

&lt;p&gt;What helped onboarding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictable folder layout (&lt;code&gt;features/orders/data&lt;/code&gt;, &lt;code&gt;features/orders/presentation&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;One documented state approach per feature&lt;/li&gt;
&lt;li&gt;Widget tests for UI edge cases, integration tests for critical flows (login, checkout, submit)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests cost more to maintain, but they paid off on auth and payment paths where widget tests alone missed navigation regressions. The &lt;a href="https://docs.flutter.dev/testing/integration-tests" rel="noopener noreferrer"&gt;Flutter integration testing docs&lt;/a&gt; are worth reading before you promise coverage you cannot sustain.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Would Do Differently on the Next Flutter App
&lt;/h2&gt;

&lt;p&gt;If we could replay the first six weeks of a typical client project, we would spend more time on boundaries and less time debating which package "wins" on Reddit.&lt;/p&gt;

&lt;p&gt;State management is a team contract. The library is just how you enforce it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-launch checklist we use now
&lt;/h3&gt;

&lt;p&gt;Before we call architecture "good enough" for a store release, we walk through this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope state by &lt;strong&gt;feature&lt;/strong&gt;, not by screen count alone&lt;/li&gt;
&lt;li&gt;Define &lt;strong&gt;async contracts&lt;/strong&gt; early: loading, error, empty, success (and when to retry)&lt;/li&gt;
&lt;li&gt;Pick &lt;strong&gt;one primary pattern per layer&lt;/strong&gt; and stick to it unless there is a written reason to diverge&lt;/li&gt;
&lt;li&gt;Document &lt;strong&gt;where state lives&lt;/strong&gt; before the team doubles in size&lt;/li&gt;
&lt;li&gt;Plan for &lt;strong&gt;post-MVP growth&lt;/strong&gt; before the first App Store or Play Store submission&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that blocks an MVP. It prevents the MVP from becoming a rewrite trigger at month six.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Working on production Flutter apps changed how we think about state. Tutorials optimize for clarity in isolation. Client work optimizes for change over time: new endpoints, new teammates, new store builds, new platform quirks.&lt;/p&gt;

&lt;p&gt;We are still learning. Riverpod, Bloc, and Provider will keep evolving. The constant is paying attention to boundaries, async behavior, and what the next developer will assume when they open the repo.&lt;/p&gt;

&lt;p&gt;What broke first in your Flutter apps once you moved past the demo stage? Curious which lesson maps to your experience.&lt;/p&gt;

&lt;p&gt;If you are planning a Flutter MVP, &lt;a href="https://spice-factory.ph/contact" rel="noopener noreferrer"&gt;scoping a mobile MVP&lt;/a&gt; early (state boundaries, async contracts, store readiness) saves pain later.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Flutter 3.44 Highlights From Google I/O 2026: What's New and What Matters</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Thu, 21 May 2026 02:54:10 +0000</pubDate>
      <link>https://dev.to/ianjasperrr/flutter-344-highlights-from-google-io-2026-whats-new-and-what-matters-g6f</link>
      <guid>https://dev.to/ianjasperrr/flutter-344-highlights-from-google-io-2026-whats-new-and-what-matters-g6f</guid>
      <description>&lt;p&gt;I tuned into the &lt;a href="https://www.youtube.com/live/3TfGKugPlpE" rel="noopener noreferrer"&gt;What's new in Flutter&lt;/a&gt; session at Google I/O 2026 expecting a standard release walkthrough. Flutter 3.44 felt bigger than that. The team framed it around scaling to more users on more devices, and the demos backed it up: agentic hot reload, generative UI, Hybrid Composition++ on Android, Swift Package Manager as the iOS and macOS default, and Flutter running in a 2026 Toyota RAV4 infotainment system.&lt;/p&gt;

&lt;p&gt;If you're wondering what actually matters in this release, the short answer is that 3.44 pushes Flutter further into AI-assisted development, embedded deployments, and platform-native integration, without treating those as side experiments. Most of what landed here is production-oriented.&lt;/p&gt;

&lt;p&gt;I've been following Flutter releases with our team at &lt;a href="https://spice-factory.ph/" rel="noopener noreferrer"&gt;Spice Factory Philippines&lt;/a&gt;, and this one stood out because it connects everyday app work to where the ecosystem is clearly heading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ecosystem Growth by the Numbers at Google I/O 2026
&lt;/h2&gt;

&lt;p&gt;The session opened with a simple idea: Flutter is everywhere, everyday, built by everyone, for everyone.&lt;/p&gt;

&lt;p&gt;On stage, they backed that up with numbers that are hard to ignore. The pub.dev ecosystem hit over &lt;strong&gt;1.3 billion package downloads&lt;/strong&gt; in the last 30 days alone. Flutter is now the &lt;strong&gt;second most popular mobile SDK&lt;/strong&gt; on both major app stores, with &lt;strong&gt;1.5 million monthly developers&lt;/strong&gt;, up 50% in a year. This release cycle alone landed &lt;strong&gt;972 commits from 178 contributors&lt;/strong&gt;, including &lt;strong&gt;61 first-time contributors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That context matters. 3.44 is not a collection of niche experiments. It reads like a release built for teams shipping real products across mobile, desktop, web, and embedded hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI and Developer Experience: Agentic Hot Reload, GenUI, and DevTools
&lt;/h2&gt;

&lt;p&gt;This was the part of the session that felt most immediately useful for day-to-day work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agentic Hot Reload&lt;/strong&gt; is the headline feature here. Through the Dart and Flutter MCP server, coding agents can now automatically find and connect to your running app, then hot reload after UI changes. Prompt your agent to tweak a screen, and you see the result without manual setup. They also hardened dependency search for agents and consolidated MCP tool definitions to cut token costs.&lt;/p&gt;

&lt;p&gt;Alongside that, &lt;strong&gt;Dart and Flutter Agent Skills&lt;/strong&gt; give agents step-by-step, task-oriented guidance for things like integration tests and localization setup. If you already use Cursor or similar tools, this is worth trying on your next Flutter task.&lt;/p&gt;

&lt;p&gt;On the product side, &lt;strong&gt;GenUI&lt;/strong&gt; (built on the open A2UI protocol) stood out in the demos. Instead of AI responses as walls of markdown, agents compose real Flutter UI on the fly. The Hatcha event-planning demo and apps like Finnish it show what that looks like in practice. Li-Te Cheng from Google DeepMind also shared practical takeaways from the Gemini App's Visual Layout experiment: lean on opinionated frameworks for consistency, use an "AI critic" loop for reliability, and balance speed with templates when you need control.&lt;/p&gt;

&lt;p&gt;Behind the scenes, DevTools got snappier (WASM by default), and Widget Previews reduce IDE memory usage by up to 50% by leaning on the Dart Analysis Server. Small wins, but the kind that make long sessions less painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Platform Updates for Android, iOS, Desktop, and Embedded
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Android: Hybrid Composition++ and AGP 9 Kotlin Changes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Hybrid Composition++ (HCPP)&lt;/strong&gt; solves a long-standing Platform Views tradeoff: frame rate vs fidelity. It delegates compositing to the Android OS using Vulkan and &lt;code&gt;SurfaceControl&lt;/code&gt;, which means smoother scrolling, better touch input, and reliable &lt;code&gt;SurfaceView&lt;/code&gt; support. It is opt-in for now via &lt;code&gt;--enable-hcpp&lt;/code&gt; or a manifest flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta-data&lt;/span&gt;
  &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"io.flutter.embedding.android.EnableHcpp"&lt;/span&gt;
  &lt;span class="na"&gt;android:value=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flutter also reads &lt;strong&gt;display corner radii&lt;/strong&gt; from Android hardware through &lt;code&gt;MediaQuery&lt;/code&gt;, which helps on aggressively rounded screens.&lt;/p&gt;

&lt;p&gt;One heads-up for Android teams: &lt;strong&gt;AGP 9 built-in Kotlin&lt;/strong&gt; means manually applying the Kotlin Gradle plugin can break builds. If you maintain plugins, the migration guide requires a minimum Flutter constraint of &lt;strong&gt;3.44&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  iOS and macOS: Swift Package Manager by Default
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Swift Package Manager is now the default&lt;/strong&gt; for iOS and macOS. The CLI migrates your Xcode project automatically. Plugins still on CocoaPods trigger a fallback with a warning, so check your dependency tree. Apple is also moving toward requiring &lt;strong&gt;UIScene&lt;/strong&gt; lifecycle support, so migrate before enforcement catches you off guard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Desktop and Embedded: Canonical, Toyota RAV4, and LG webOS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Canonical&lt;/strong&gt; is now the lead maintainer for Flutter Desktop (Linux, Windows, macOS). Experimental &lt;strong&gt;multi-window&lt;/strong&gt; APIs are progressing on the main channel: tooltips, popup windows on macOS, and separate dialog windows.&lt;/p&gt;

&lt;p&gt;The embedded demos got the crowd reaction. Flutter powers the &lt;strong&gt;2026 Toyota RAV4&lt;/strong&gt; multimedia system, and &lt;strong&gt;LG's webOS SDK&lt;/strong&gt; (with hot reload, Riverpod, Firebase plugins, and more) is coming soon for big-screen targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Material and Cupertino Decoupling From the Core Framework
&lt;/h2&gt;

&lt;p&gt;Material and Cupertino libraries are &lt;strong&gt;frozen&lt;/strong&gt; in the core framework as of 3.44. They will move to standalone packages (&lt;code&gt;material_ui&lt;/code&gt; and &lt;code&gt;cupertino_ui&lt;/code&gt;) with independent versioning in a future release.&lt;/p&gt;

&lt;p&gt;Practically, that means design system updates can ship on their own cadence instead of waiting for the next Flutter SDK. If you are planning a Material 3 migration or heavy Cupertino customization, keep an eye on the decoupling tracking issue and start thinking about package-based imports early.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Upgrade to Flutter 3.44: Migration Checklist
&lt;/h2&gt;

&lt;p&gt;If you want to try the highlights without reading every release note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;flutter upgrade&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Test &lt;strong&gt;HCPP&lt;/strong&gt; if your app embeds native Android views (maps, web views, etc.)&lt;/li&gt;
&lt;li&gt;Audit your &lt;strong&gt;Android Gradle/Kotlin&lt;/strong&gt; setup for AGP 9 compatibility&lt;/li&gt;
&lt;li&gt;Check iOS plugins for &lt;strong&gt;SwiftPM&lt;/strong&gt; support&lt;/li&gt;
&lt;li&gt;Experiment with &lt;strong&gt;Agentic Hot Reload&lt;/strong&gt; if you use an AI coding agent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full breakdown, see the &lt;a href="https://medium.com/flutter/whats-new-in-flutter-3-44-b0cc1ad3c527" rel="noopener noreferrer"&gt;official Flutter 3.44 release post&lt;/a&gt; and the &lt;a href="https://dart.dev/blog/announcing-dart-3-12" rel="noopener noreferrer"&gt;Dart 3.12 release notes&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways From the What's New in Flutter Session
&lt;/h2&gt;

&lt;p&gt;The session made one thing clear: Flutter is not just iterating on widgets anymore. It is positioning for agentic development workflows, generative UI, and deployments from car dashboards to smart TVs.&lt;/p&gt;

&lt;p&gt;For our team, Agentic Hot Reload and the Android HCPP improvements are the first things worth testing on active projects. The Material/Cupertino decoupling is the longer-term architectural shift to watch.&lt;/p&gt;

&lt;p&gt;Which 3.44 change are you trying first? Curious what stands out for other teams shipping Flutter in production.&lt;/p&gt;

&lt;p&gt;If you want to see how we approach multi-platform app development for real client work, you can check what we do here: &lt;a href="https://spice-factory.ph/" rel="noopener noreferrer"&gt;https://spice-factory.ph/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>flutter</category>
      <category>mobile</category>
      <category>news</category>
    </item>
  </channel>
</rss>
