<?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: Yevhen Beshkarov</title>
    <description>The latest articles on DEV Community by Yevhen Beshkarov (@ybeshkarov).</description>
    <link>https://dev.to/ybeshkarov</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4149771%2Fa62933f9-d0ef-42c0-996f-f4cc6d54bb58.jpeg</url>
      <title>DEV Community: Yevhen Beshkarov</title>
      <link>https://dev.to/ybeshkarov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ybeshkarov"/>
    <language>en</language>
    <item>
      <title>Flutter Firebase Analytics screen tracking with go_router</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Sat, 24 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/flutter-firebase-analytics-screen-tracking-with-gorouter-1iij</link>
      <guid>https://dev.to/ybeshkarov/flutter-firebase-analytics-screen-tracking-with-gorouter-1iij</guid>
      <description>&lt;p&gt;The usual advice for Flutter Firebase Analytics screen tracking is to give &lt;code&gt;FirebaseAnalyticsObserver&lt;/code&gt; to &lt;code&gt;GoRouter&lt;/code&gt;. In an app with tabs, it misses screens. Going back to a tab the user has already opened logs nothing, and neither does closing a page that was opened over the tabs. A &lt;code&gt;go()&lt;/code&gt; to a nested route logs every parent page as well, under names like &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt;. I checked this on go_router 18.1.1 and firebase_analytics 13.0.0, with Flutter 3.44.2 and 3.47.4. A listener on the router delegate logs one screen view for each screen the user sees, in about 30 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FirebaseAnalyticsObserver logs with go_router tabs
&lt;/h2&gt;

&lt;p&gt;The usual setup:&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;observers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FirebaseAnalyticsObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;analytics:&lt;/span&gt; &lt;span class="n"&gt;FirebaseAnalytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&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="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My test app has two tabs in a &lt;code&gt;StatefulShellRoute&lt;/code&gt;: &lt;code&gt;/home&lt;/code&gt;, which has a child route &lt;code&gt;details&lt;/code&gt;, and &lt;code&gt;/settings&lt;/code&gt;. It also has a &lt;code&gt;/profile&lt;/code&gt; page that opens over the tabs on the root navigator, a nested route &lt;code&gt;/a/b/c&lt;/code&gt;, and &lt;code&gt;/item/:id&lt;/code&gt;, which is named &lt;code&gt;item&lt;/code&gt;. A fake &lt;code&gt;FirebaseAnalytics&lt;/code&gt; recorded every call to &lt;code&gt;logScreenView&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the user does&lt;/th&gt;
&lt;th&gt;What the observer logs&lt;/th&gt;
&lt;th&gt;What it should log&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The app starts on Home&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens the Settings tab for the first time&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/settings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/settings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goes back to the Home tab&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens the Settings tab again&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/settings&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go('/home/details')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;details&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home/details&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goes back to Home&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;push('/profile')&lt;/code&gt; over the tabs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/profile&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/profile&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Goes back from Profile&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go('/a/b/c')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/a/b/c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go('/item/42')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;item&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;item&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Users move between tabs all the time, so the reports undercount the screens behind the tabs. They also count parent pages that nobody saw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why FirebaseAnalyticsObserver misses tab switches
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;FirebaseAnalyticsObserver&lt;/code&gt; is a &lt;code&gt;NavigatorObserver&lt;/code&gt;. It logs a screen view when a navigator pushes, replaces or pops a route, and at no other time. Each branch of a &lt;code&gt;StatefulShellRoute&lt;/code&gt; has a navigator of its own, and that navigator stays alive in an &lt;code&gt;IndexedStack&lt;/code&gt; while the user is on another tab. The first visit to a tab builds its navigator, which pushes the first page, so that visit is logged. Going back to the tab only changes which navigator is visible. No navigator pushes or pops anything.&lt;/p&gt;

&lt;p&gt;Closing &lt;code&gt;/profile&lt;/code&gt; pops it from the root navigator, and the observer then logs the route below it. That route is the page of the shell route. go_router names a page after the &lt;code&gt;name&lt;/code&gt; of its route, or after its &lt;code&gt;path&lt;/code&gt; when there is no name. A shell route has neither, so the observer gets &lt;code&gt;null&lt;/code&gt; as the name and logs nothing.&lt;/p&gt;

&lt;p&gt;The same goes for a page that your own &lt;code&gt;pageBuilder&lt;/code&gt; returns. Tabs often use &lt;code&gt;NoTransitionPage(child: ...)&lt;/code&gt;, and that page has no name unless you pass one. With such a page on the Home tab, the observer never logged Home, not even when the app started. Pass &lt;code&gt;name: state.name ?? state.path&lt;/code&gt; to the page, and it is logged like the others.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go('/a/b/c')&lt;/code&gt; builds the whole chain. go_router puts a page on the navigator for each of &lt;code&gt;/a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt; and &lt;code&gt;c&lt;/code&gt;, so that back works, and the navigator reports a push for each of them. The names come from &lt;code&gt;path&lt;/code&gt;, and the &lt;code&gt;path&lt;/code&gt; of a child route is its own segment, not the full location. That is also why &lt;code&gt;details&lt;/code&gt; lost its &lt;code&gt;/home/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't add an observer to each branch
&lt;/h2&gt;

&lt;p&gt;Before go_router 17.0.0, the observers of &lt;code&gt;GoRouter&lt;/code&gt; didn't hear the navigators of shell routes, so guides from that time give each &lt;code&gt;ShellRoute&lt;/code&gt; or &lt;code&gt;StatefulShellBranch&lt;/code&gt; observers of its own. Since 17.0.0, shell routes pass their navigation to the observers of &lt;code&gt;GoRouter&lt;/code&gt; by default, through &lt;code&gt;notifyRootObserver&lt;/code&gt;. With one observer on &lt;code&gt;GoRouter&lt;/code&gt; and another on each branch, every screen in a tab is logged twice. Opening the app on Home and then the Settings tab logged this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home, /home, /settings, /settings

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep only the observer on &lt;code&gt;GoRouter&lt;/code&gt;, or pass &lt;code&gt;notifyRootObserver: false&lt;/code&gt; to the shell route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log screen views from the go_router delegate
&lt;/h2&gt;

&lt;p&gt;The router knows what the user sees, whichever navigator it is in. &lt;code&gt;GoRouter.routerDelegate&lt;/code&gt; notifies its listeners each time its configuration changes, and switching tabs counts. Its &lt;code&gt;currentConfiguration&lt;/code&gt; ends with the page on top. This listener logs that page:&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="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:firebase_analytics/firebase_analytics.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/services.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:go_router/go_router.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;/// Logs a screen view each time the user sees another screen: another&lt;/span&gt;
&lt;span class="c1"&gt;/// page on top of any navigator of the router, or the same page at&lt;/span&gt;
&lt;span class="c1"&gt;/// another path.&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;trackScreenViews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GoRouter&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FirebaseAnalytics&lt;/span&gt; &lt;span class="n"&gt;analytics&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="n"&gt;LocalKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;routerDelegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addListener&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;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;routerDelegate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentConfiguration&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;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lastOrNull&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;shown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;ImperativeRouteMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;matches&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="n"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// a pushed page&lt;/span&gt;
      &lt;span class="n"&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="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// The error screen. Its location can hold anything, such as a token.&lt;/span&gt;
      &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;return&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;screen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pageKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;screen&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;screen&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;route&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="n"&gt;shown&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;analytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logScreenView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;screenName:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;catchError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;error&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="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Screen view not logged: &lt;/span&gt;&lt;span class="si"&gt;$error&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nl"&gt;test:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="n"&gt;PlatformException&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;Call it once, right after you create the router, and leave out &lt;code&gt;observers&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;trackScreenViews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FirebaseAnalytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same steps with the listener:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What the user does&lt;/th&gt;
&lt;th&gt;What the listener logs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The app starts on Home&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/home&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Opens the Settings tab, then Home, then Settings&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/settings&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/settings&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;go('/home/details')&lt;/code&gt;, then goes back&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/home/details&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;push('/profile')&lt;/code&gt;, then goes back&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/profile&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;go('/a/b/c')&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/a/b/c&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;go('/item/42')&lt;/code&gt;, then &lt;code&gt;go('/item/43')&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;item&lt;/code&gt;, &lt;code&gt;item&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;go('/home?tab=2')&lt;/code&gt; while on Home&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Screen names, duplicates and the error screen
&lt;/h2&gt;

&lt;p&gt;The listener compares the page on top and its path with the last ones it logged. A pushed page gets a page key of its own, so pushing the same location twice logs it twice: the user saw it twice. &lt;code&gt;/item/42&lt;/code&gt; and &lt;code&gt;/item/43&lt;/code&gt; share a page key in go_router, and the path tells them apart. A change of the query alone, such as a filter, logs nothing.&lt;/p&gt;

&lt;p&gt;A screen is named after its route, or after the full path pattern, such as &lt;code&gt;/item/:id&lt;/code&gt;, when the route has no name. The location itself would make every item a screen of its own in the reports, and a location can hold an email address or a token. Since the listener reads routes rather than pages, a page from a &lt;code&gt;pageBuilder&lt;/code&gt; needs no name.&lt;/p&gt;

&lt;p&gt;When no route matches, the configuration is an error, and the location that failed can hold anything. The listener logs nothing for the error screen and forgets the last screen, so the screen the user goes back to is logged again.&lt;/p&gt;

&lt;p&gt;Nothing waits for &lt;code&gt;logScreenView&lt;/code&gt;, so a &lt;code&gt;PlatformException&lt;/code&gt; would go to the handler of uncaught errors. The listener prints it instead, as &lt;code&gt;FirebaseAnalyticsObserver&lt;/code&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SMF does Flutter Firebase Analytics screen tracking
&lt;/h2&gt;

&lt;p&gt;I ran into this while building &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;SMF&lt;/a&gt;, a free, open-source Flutter project generator. In every app with go_router and the &lt;a href="https://doc.saymyframe.com/modules/firebase-analytics" rel="noopener noreferrer"&gt;Firebase Analytics module&lt;/a&gt;, the router tells listeners about each screen the user sees, and the module logs a screen view for each one, with no code in the app. Tests that run in the generated apps check that listeners hear about each screen once, tab switches included.&lt;/p&gt;

&lt;p&gt;Firebase has a trap on iOS too, in the build phase that uploads symbols to Crashlytics: &lt;a href="https://saymyframe.com/blog/flutterfire-upload-crashlytics-symbols-error" rel="noopener noreferrer"&gt;how to fix the flutterfire upload-crashlytics-symbols error&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>firebase</category>
      <category>gorouter</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>SMF 0.6: Flutter push notifications and social sign-in</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Tue, 20 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/smf-06-flutter-push-notifications-and-social-sign-in-4n40</link>
      <guid>https://dev.to/ybeshkarov/smf-06-flutter-push-notifications-and-social-sign-in-4n40</guid>
      <description>&lt;p&gt;Say My Frame (SMF) generates Flutter apps from independent modules. &lt;a href="https://pub.dev/packages/smf_flutter_cli" rel="noopener noreferrer"&gt;Version 0.6 is out on pub.dev&lt;/a&gt;. It adds Flutter push notifications with Firebase Cloud Messaging, local notifications, and sign-in with Google and Apple. At the end of &lt;a href="https://saymyframe.com/blog/smf-0-5-flutter-sign-in" rel="noopener noreferrer"&gt;the post about 0.5&lt;/a&gt; these were next on my list. Some of the setup lives in the Apple Developer account and the Firebase console, where no command can reach, so 0.6 also comes with a guide for those steps, click by click.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create my_app &lt;span class="nt"&gt;-m&lt;/span&gt; home,onboarding,sign_in,google_sign_in,apple_sign_in,firebase_messaging,bottom_tabs,material_theme,gen_l10n,get_it,bloc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-6-flutter-push-notifications%2Fsmf-create.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-6-flutter-push-notifications%2Fsmf-create.gif" alt="smf create in a terminal asks for the app name and the modules, among them Google sign-in, Sign in with Apple, the onboarding and the notifications, adds the modules that the chosen ones need, such as go_router, the settings screen and Firebase Authentication, asks who may use the app without an account, and generates a Flutter app with an onboarding, a start screen, sign-in with email, Google and Apple, tabs at the bottom, a theme, two languages, notifications, get_it and BLoC" width="720" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recording answers the questions of &lt;code&gt;smf create&lt;/code&gt; instead of passing &lt;code&gt;-m&lt;/code&gt;. It picks local notifications and leaves push messages out, so the app needs no APNs key. Either way, SMF adds what the chosen modules need and says why.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw94zqt3uaffmigli613c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw94zqt3uaffmigli613c.png" alt="The app that smf create generates, on a phone: the onboarding, the sign-in with Google, Apple and an email address, the start screen, and the account screen in the dark theme and in Ukrainian" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As before, what you get is a regular Flutter project. It has no SMF package in its dependencies, and SMF never touches it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Flutter push notifications and local notifications after smf create
&lt;/h2&gt;

&lt;p&gt;Two new modules handle notifications, each for a role of its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;local_notifications&lt;/code&gt; shows a notification now or at about a time to come, on flutter_local_notifications. It tells the app of each tap, including the tap that launched the app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_messaging&lt;/code&gt; gives the app the address of the device for its backend, which is the registration token of Firebase Cloud Messaging. It also gives the app the push messages that arrive there and the taps on them. A message that arrives while the app is open shows as a notification too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Push messages lean on the notifications for the permission and for showing a message while the app is open, so &lt;code&gt;firebase_messaging&lt;/code&gt; brings &lt;code&gt;local_notifications&lt;/code&gt; with it. An app can have local notifications without push messages, and then it needs no Firebase project.&lt;/p&gt;

&lt;p&gt;The app asks for nothing when it starts. In an app with an onboarding, one page of it asks for the permission to show notifications. The settings screen gets a row that shows On or Off, asks while the app hasn't asked yet, and opens the settings of the system after that. On Android 12 and older, where apps get the permission without asking, the onboarding skips that page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucy8nglt7erqrrzcvbvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fucy8nglt7erqrrzcvbvm.png" alt="Three screens of the generated app: the page of the onboarding " width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://doc.saymyframe.com/guides/notifications" rel="noopener noreferrer"&gt;notifications guide&lt;/a&gt; shows how code schedules a notification, reads the address and listens to messages and taps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sign-in with Google and Apple in Flutter
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;google_sign_in&lt;/code&gt; adds sign-in with a Google account on Android and iOS, and &lt;code&gt;apple_sign_in&lt;/code&gt; adds Sign in with Apple on iOS, so on Android its button doesn't show. The sign-in screen of &lt;code&gt;sign_in&lt;/code&gt; shows "Continue with Google" and "Continue with Apple" above the form with the email address, in the theme and in both languages of the app. Firebase Authentication signs the user in with these credentials, links them to an account, and deletes such an account.&lt;/p&gt;

&lt;p&gt;Two rules of the App Store shaped this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An iOS app with Google sign-in also needs a way to sign in that the App Store takes as its equivalent, such as Sign in with Apple (Guideline 4.8). &lt;code&gt;smf create&lt;/code&gt; warns of an app with Google and without Apple.&lt;/li&gt;
&lt;li&gt;An app with Sign in with Apple revokes the tokens of Apple when the user deletes the account (Guideline 5.1.1(v)). The generated app asks Apple again before the deletion, and Firebase revokes the tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After &lt;code&gt;flutterfire configure&lt;/code&gt;, &lt;code&gt;smf create&lt;/code&gt; offers to run a script that enables Google in the Firebase project, registers the SHA-1 and SHA-256 of your debug key and writes the URL scheme of the iOS app. Apple has no such command. The &lt;a href="https://doc.saymyframe.com/guides/sign-in" rel="noopener noreferrer"&gt;sign-in guide&lt;/a&gt; covers both ways to sign in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steps you do by hand, click by click
&lt;/h2&gt;

&lt;p&gt;Some setup can't be done from a terminal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the App ID of a paid Apple team, which push messages and Sign in with Apple need, since a free team can't sign the app with them;&lt;/li&gt;
&lt;li&gt;the APNs key that Firebase needs to send messages to iPhones;&lt;/li&gt;
&lt;li&gt;the Apple provider and its key in Firebase;&lt;/li&gt;
&lt;li&gt;the SHA of your release keys;&lt;/li&gt;
&lt;li&gt;Google Analytics in the Firebase project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I checked for APIs before writing a guide. Firebase takes an APNs key only in its console, and the API of App Store Connect can't create such a key either.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;smf create&lt;/code&gt; ends with one line that says how many steps your app needs, and one link:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finish the setup by hand in 5 steps:
  https://doc.saymyframe.com/setup?steps=app_entry.ios_app_id,firebase_auth.apple_provider,firebase_auth.apple_revocation,firebase_auth.google_release_sha,firebase_messaging.apns_key

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page behind the link shows only those steps, in their order, with screenshots of the Apple Developer account, the Firebase console and Xcode. Each step has a checkbox, so you can come back later and see where you stopped. The README of the app lists the same steps in its section "Finish the setup". In terminals that support it, the link is clickable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Firebase Cloud Messaging and Apple did on real devices
&lt;/h2&gt;

&lt;p&gt;The tests of the modules run the real Firebase packages over a stand-in for their platform side. Before the release I went through push messages, notifications and sign-in by hand on an iPhone simulator, on Android emulators with Android 12 and 17, and against a real Firebase project. A few things there were not what I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the first start, the iPhone simulator had no address for about 13 seconds. The app picks it up when it arrives, so code that sends the address to a backend listens for it instead of reading it once.&lt;/li&gt;
&lt;li&gt;An Android emulator that I restored from a snapshot received no messages at all until a cold boot. Nothing was wrong with the app.&lt;/li&gt;
&lt;li&gt;On iOS, firebase_messaging also answers when a local notification is about to show. Unless the app sets the options of foreground notifications, that answer hides the notification while the app is open. The generated service sets them, so a notification shows once.&lt;/li&gt;
&lt;li&gt;Deleting an account that signed in with Apple, before the key of Apple was in Firebase, failed with "Code flow is not enabled for Apple". The generated app tells the user that this is not available, keeps the account, and shows you the fix in debug mode. With the key in place, the account was deleted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more problem broke every release build on Android, and it had nothing to do with notifications. &lt;code&gt;shared_preferences_android&lt;/code&gt; 2.5.0 came out during this work, and with it a release build crashed before its first frame. I wrote &lt;a href="https://saymyframe.com/blog/shared-preferences-android-release-build-crash" rel="noopener noreferrer"&gt;a separate post about that crash&lt;/a&gt;. Since then, CI also starts a release build on an Android emulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sixteen roles and twenty-two modules, checked in running apps
&lt;/h2&gt;

&lt;p&gt;0.5 had thirteen roles and eighteen modules. 0.6 has sixteen and twenty-two. The new roles are the notifications, the push messages and the onboarding screen, which lets modules add pages such as the one that asks for the permission. The new modules are &lt;code&gt;local_notifications&lt;/code&gt;, &lt;code&gt;firebase_messaging&lt;/code&gt;, &lt;code&gt;google_sign_in&lt;/code&gt; and &lt;code&gt;apple_sign_in&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When I wrote this, CI generated 59 apps from the modules of the CLI and ran &lt;code&gt;flutter analyze&lt;/code&gt; and &lt;code&gt;flutter test&lt;/code&gt; in each. It also configures the app with every module for a real Firebase project and starts it on an Android emulator and on an iOS simulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMF 0.6 can't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Firebase is the only provider of push messages and of sign-in.&lt;/li&gt;
&lt;li&gt;There is no networking and no in-app purchases yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;Subscriptions with a paywall are next on my list. There are no dates. SMF is a side project, and I'd rather ship things when they work.&lt;/p&gt;

&lt;p&gt;If you have a module in mind, tell me which one on &lt;a href="https://saymyframe.com/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or in &lt;a href="https://github.com/saymyframe/smf_flutter_cli/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 0.5
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--strict&lt;/code&gt; now stops on a warning about what the app is made of, such as a destination in an app without a main navigation. Each such warning has a code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;firebase_auth&lt;/code&gt; needs the Firebase CLI 15.34.0 or newer. That version no longer adds a web app to a Firebase project that already has apps.&lt;/li&gt;
&lt;li&gt;An app gets the fallback start screen only when no route can start it.&lt;/li&gt;
&lt;li&gt;The module API of &lt;code&gt;smf_contracts&lt;/code&gt; changed: modules can add code to the &lt;code&gt;AppDelegate&lt;/code&gt;, entitlements, ways to sign in and steps by hand. A module written for 0.5 needs an update.&lt;/li&gt;
&lt;li&gt;Apps generated by 0.5 stay as they are.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://github.com/saymyframe/smf_flutter_cli/releases/tag/smf_flutter_cli-v0.6.0" rel="noopener noreferrer"&gt;release notes of SMF 0.6.0 on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Flutter push notifications with smf create
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMF generates Android and iOS apps for Flutter 3.44 or newer and Dart 3.12 or newer, and it is tested on macOS, Linux and Windows. The &lt;a href="https://doc.saymyframe.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; starts with installation and your first app, the &lt;a href="https://doc.saymyframe.com/guides/notifications" rel="noopener noreferrer"&gt;notifications guide&lt;/a&gt; goes from the permission to push messages, and the code is &lt;a href="https://github.com/saymyframe/smf_flutter_cli" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Stars there are welcome, but only if you actually like it.&lt;/p&gt;

</description>
      <category>release</category>
      <category>firebase</category>
      <category>ios</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Fix shared_preferences crashing Flutter release builds</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Sun, 18 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/fix-sharedpreferences-crashing-flutter-release-builds-16n3</link>
      <guid>https://dev.to/ybeshkarov/fix-sharedpreferences-crashing-flutter-release-builds-16n3</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Update, 22 October 2026:&lt;/strong&gt; the bug is fixed in &lt;code&gt;shared_preferences_android&lt;/code&gt; 2.5.2. If you added the pin or the keep rules from the workarounds below, remove them. Then run &lt;code&gt;flutter pub upgrade shared_preferences_android&lt;/code&gt; and check that &lt;code&gt;pubspec.lock&lt;/code&gt; has 2.5.2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On 17 October 2026, &lt;code&gt;shared_preferences_android&lt;/code&gt; 2.5.0 came out, and Flutter apps that use &lt;code&gt;shared_preferences&lt;/code&gt; stopped starting in Android release builds. The app stays on the splash screen, and logcat has a &lt;code&gt;NoSuchMethodError&lt;/code&gt;. Debug and profile builds work, and so does iOS, so a CI that builds only debug stays green. This shared_preferences release build crash comes from R8, which renames Kotlin classes that the new version looks up by name. The fix is out in &lt;code&gt;shared_preferences_android&lt;/code&gt; 2.5.2, so upgrade the package. If you can't upgrade yet, go back to the previous version or add six keep rules to the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  NoSuchMethodError in SharedPreferencesAsyncApiRegistrar.getAll
&lt;/h2&gt;

&lt;p&gt;With &lt;code&gt;SharedPreferencesAsync&lt;/code&gt; or &lt;code&gt;SharedPreferencesWithCache&lt;/code&gt;, the first call to the preferences throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E flutter: Unhandled Exception: PlatformException(PlatformException, java.lang.NoSuchMethodError: no non-static method "Lio/flutter/plugins/sharedpreferences/SharedPreferencesAsyncApiRegistrar;.getAll(Ljava/util/List;Lio/flutter/plugins/sharedpreferences/SharedPreferencesPigeonOptions;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the legacy &lt;code&gt;SharedPreferences.getInstance()&lt;/code&gt;, the class in the message is &lt;code&gt;SharedPreferencesApiRegistrar&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PlatformException(PlatformException, java.lang.NoSuchMethodError: no non-static method "Lio/flutter/plugins/sharedpreferences/SharedPreferencesApiRegistrar;.getAll(Ljava/lang/String;Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the app opens the preferences in &lt;code&gt;main()&lt;/code&gt;, before &lt;code&gt;runApp()&lt;/code&gt;, the future fails and &lt;code&gt;runApp()&lt;/code&gt; never runs. The app keeps showing its splash screen, and no crash dialog comes up. An app that opens the preferences later fails on the screen that reads them.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it fails: shared_preferences_android 2.5.0 and R8
&lt;/h2&gt;

&lt;p&gt;Check which version pub resolved, in the directory of the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub deps | &lt;span class="nb"&gt;grep &lt;/span&gt;shared_preferences_android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have this case when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it prints &lt;code&gt;shared_preferences_android 2.5.0&lt;/code&gt; or &lt;code&gt;2.5.1&lt;/code&gt;. &lt;code&gt;shared_preferences&lt;/code&gt; 2.5.5 allows any 2.x of its Android side from 2.3.4 on, so a &lt;code&gt;flutter pub upgrade&lt;/code&gt;, or a first &lt;code&gt;flutter pub get&lt;/code&gt; between 17 and 22 October, brought one of them in. 2.5.1 only updated a dependency and has the same bug;&lt;/li&gt;
&lt;li&gt;you build for Android in release: &lt;code&gt;flutter build apk&lt;/code&gt;, &lt;code&gt;flutter build appbundle&lt;/code&gt; or &lt;code&gt;flutter run --release&lt;/code&gt;. Flutter turns R8 on for every release build, and the app bundle that you upload to Google Play has the bug too;&lt;/li&gt;
&lt;li&gt;debug and profile builds of the same app work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;iOS and macOS use &lt;code&gt;shared_preferences_foundation&lt;/code&gt;, and the other platforms have packages of their own, so only Android is affected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: upgrade to shared_preferences_android 2.5.2
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;shared_preferences_android&lt;/code&gt; 2.5.2 came out on 22 October. It ships consumer rules for R8, which keep the Kotlin classes in every app that uses the plugin. Upgrade it in the directory of the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub upgrade shared_preferences_android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you added one of the workarounds below, take it out first: remove the dependency on &lt;code&gt;shared_preferences_android&lt;/code&gt; before the upgrade, or delete the six rules from &lt;code&gt;android/app/proguard-rules.pro&lt;/code&gt;, where they do no harm if you leave them. Then check that &lt;code&gt;flutter pub deps | grep shared_preferences_android&lt;/code&gt; prints 2.5.2, and start the release build again with &lt;code&gt;flutter run --release&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workaround 1: keep shared_preferences_android below 2.5.0
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub add &lt;span class="s1"&gt;'shared_preferences_android:&amp;gt;=2.4.28 &amp;lt;2.5.0'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app then depends on the Android side of the package directly, and pub goes back to 2.4.28, which talks to Kotlin through a method channel as before. The rest of the app stays as it is.&lt;/p&gt;

&lt;p&gt;To move to 2.5.2 later, remove the dependency and upgrade the package. Without the upgrade, &lt;code&gt;pubspec.lock&lt;/code&gt; keeps 2.4.28:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub remove shared_preferences_android
flutter pub upgrade shared_preferences_android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Workaround 2: keep the Kotlin classes with R8 rules
&lt;/h2&gt;

&lt;p&gt;To stay on 2.5.0 or 2.5.1, tell R8 to keep the classes. Put these lines into &lt;code&gt;android/app/proguard-rules.pro&lt;/code&gt;, and create the file if the app has none:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-keep class kotlin.Unit { *; }
-keep class kotlin.Result { *; }
-keep class kotlin.Result$Failure { *; }
-keep class kotlin.coroutines.Continuation { *; }
-keep class kotlin.coroutines.intrinsics.CoroutineSingletons { *; }
-keep class kotlin.coroutines.intrinsics.IntrinsicsKt { *; }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Gradle plugin of Flutter adds this file to the release build when it exists, so &lt;code&gt;build.gradle.kts&lt;/code&gt; stays as it is. The rules are those of Step 5 in the &lt;a href="https://github.com/flutter/packages/blob/main/packages/pigeon/native_interop_guide.md#step-5-android-release-build-configuration-jni" rel="noopener noreferrer"&gt;native interop guide of Pigeon&lt;/a&gt;, the tool that generates the code of the plugin.&lt;/p&gt;

&lt;p&gt;After either workaround, start the release build again with &lt;code&gt;flutter run --release&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the shared_preferences release build crash happens
&lt;/h2&gt;

&lt;p&gt;Version 2.5.0 moved the plugin from method channels to the native interop of Pigeon (&lt;a href="https://github.com/flutter/packages/pull/12329" rel="noopener noreferrer"&gt;flutter/packages#12329&lt;/a&gt;). The Dart code now calls the Kotlin code of the plugin through JNI, and JNI finds classes and methods at run time by their names and descriptors.&lt;/p&gt;

&lt;p&gt;R8 renames the classes that, as far as it can see, nothing refers to by name. The generated Kotlin classes of the plugin have &lt;code&gt;@Keep&lt;/code&gt; since &lt;a href="https://github.com/flutter/packages/pull/13151" rel="noopener noreferrer"&gt;flutter/packages#13151&lt;/a&gt;, so &lt;code&gt;SharedPreferencesAsyncApiRegistrar&lt;/code&gt; keeps its name. Its methods are &lt;code&gt;suspend&lt;/code&gt; functions, though, and their last parameter is a &lt;code&gt;kotlin.coroutines.Continuation&lt;/code&gt;. Nothing keeps that class. From &lt;code&gt;build/app/outputs/mapping/release/mapping.txt&lt;/code&gt; of a release build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;kotlin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coroutines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Continuation&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;R8 renames the class and rewrites the descriptor of &lt;code&gt;getAll&lt;/code&gt; with the new name. The Dart side still asks for a method that ends with &lt;code&gt;Lkotlin/coroutines/Continuation;&lt;/code&gt;, and that method no longer exists. The same goes for &lt;code&gt;kotlin.Unit&lt;/code&gt;, &lt;code&gt;kotlin.Result&lt;/code&gt; and the coroutine classes that &lt;code&gt;package:jni&lt;/code&gt; 1.1.0 looks up by name in &lt;code&gt;lib/src/kotlin.dart&lt;/code&gt;. The plugin has no consumer rules for R8, and the rules of &lt;code&gt;jni&lt;/code&gt; keep only its own package, &lt;code&gt;com.github.dart_lang.jni.**&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Debug and profile builds run without R8, so every name stays and the calls work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a green CI with debug builds missed it
&lt;/h2&gt;

&lt;p&gt;The release APK builds without an error and fails only when its code runs. I found it by chance on the day 2.5.0 came out: while checking an unrelated change in SMF, I started a release build on an emulator, and it stayed on the splash screen. An app from a plain &lt;code&gt;flutter create&lt;/code&gt; did the same. The CI of SMF builds its generated apps for Android and starts them on an emulator, on every pull request and every night, and every Android build there was a debug build. So it stayed green.&lt;/p&gt;

&lt;p&gt;Your users get a release build, so start one before they do: once a day in CI, or by hand before each release. Build it with &lt;code&gt;flutter build apk --release&lt;/code&gt;, install it on an emulator and check that the first screen shows. R8 runs only in release builds, and that is the only place where a bug like this one shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The upstream fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/flutter/flutter/issues/194649" rel="noopener noreferrer"&gt;flutter/flutter#194649&lt;/a&gt; is the issue I opened on 17 October, with the steps to reproduce. Within a day, other apps linked to it from their own fixes.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/flutter/packages/pull/13311" rel="noopener noreferrer"&gt;flutter/packages#13311&lt;/a&gt; is my pull request. It adds the six rules to &lt;code&gt;shared_preferences_android&lt;/code&gt; as consumer rules, which R8 applies in every app that uses the plugin. It was merged on 21 October and came out in 2.5.2 a day later, since an outage of the CI of Flutter stopped the first release run.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dart-lang/native/issues/3732" rel="noopener noreferrer"&gt;dart-lang/native#3732&lt;/a&gt; is about shipping the rules in &lt;code&gt;jni&lt;/code&gt; itself, so that every plugin on the native interop of Pigeon gets them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If another plugin of your app moves to the native interop of Pigeon and its release build fails with a &lt;code&gt;NoSuchMethodError&lt;/code&gt; or a &lt;code&gt;ClassNotFoundException&lt;/code&gt; that names a &lt;code&gt;kotlin&lt;/code&gt; class, try the same rules first.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SMF keeps generated apps on shared_preferences_android 2.4.28
&lt;/h2&gt;

&lt;p&gt;I ran into this while building &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;SMF&lt;/a&gt;, a free, open-source Flutter project generator. Since version 0.4.2, released on 17 October, its &lt;a href="https://doc.saymyframe.com/modules/shared-preferences" rel="noopener noreferrer"&gt;shared_preferences module&lt;/a&gt; keeps &lt;code&gt;shared_preferences_android&lt;/code&gt; below 2.5.0 in every app it generates. The guide for coding agents in the app says why, and asks them to start a release build on Android before they raise the constraint. The CI of SMF now also starts a release build of a generated app on an Android emulator.&lt;/p&gt;

&lt;p&gt;On iOS I hit the same kind of trap, where debug works and the release archive fails: the Crashlytics build phase of FlutterFire breaks &lt;code&gt;flutter build ipa&lt;/code&gt; in apps with Swift Package Manager. &lt;a href="https://saymyframe.com/blog/flutterfire-upload-crashlytics-symbols-error" rel="noopener noreferrer"&gt;The fix for the flutterfire upload-crashlytics-symbols error&lt;/a&gt; is in an earlier post.&lt;/p&gt;

</description>
      <category>troubleshooting</category>
      <category>android</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>SMF 0.5: Flutter sign-in with Firebase Authentication</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/smf-05-flutter-sign-in-with-firebase-authentication-4b0o</link>
      <guid>https://dev.to/ybeshkarov/smf-05-flutter-sign-in-with-firebase-authentication-4b0o</guid>
      <description>&lt;p&gt;Say My Frame (SMF) generates Flutter apps from independent modules. &lt;a href="https://pub.dev/packages/smf_flutter_cli" rel="noopener noreferrer"&gt;Version 0.5 is out on pub.dev&lt;/a&gt;, and it brings Flutter sign-in. An app you generate can now have screens where a user signs in with an email address and a password, creates an account or resets a password. It also gets an account screen, and Firebase Authentication keeps the accounts. The post about 0.4 ended with sign-in as the next thing on my list. This one is about what you get, what it needs from you, and what it still can't do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create my_app &lt;span class="nt"&gt;-m&lt;/span&gt; home,onboarding,sign_in,bloc,bottom_tabs,gen_l10n,material_theme

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-5-flutter-sign-in%2Fsmf-create.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-5-flutter-sign-in%2Fsmf-create.gif" alt="smf create in a terminal asks for the app name and the modules, adds go_router, shared_preferences, firebase_auth, firebase_core and the settings screen, which the chosen modules need, asks who may use the app without an account, and generates a Flutter app with an onboarding, a start screen, sign-in, tabs at the bottom, a theme, two languages, get_it and BLoC" width="720" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recording answers the questions of &lt;code&gt;smf create&lt;/code&gt; instead of passing &lt;code&gt;-m&lt;/code&gt;, and it also picks get_it. Either way, SMF adds what the chosen modules need and says why: &lt;code&gt;sign_in&lt;/code&gt; brings &lt;code&gt;firebase_auth&lt;/code&gt;, &lt;code&gt;firebase_core&lt;/code&gt; and &lt;code&gt;settings&lt;/code&gt; with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ustj8dw837azsblr512.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ustj8dw837azsblr512.png" alt="The app that smf create generates, on a phone: the onboarding, the sign-in, the start screen, and the account screen in the dark theme and in Ukrainian" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;home page of Say My Frame&lt;/a&gt; has a video of a walk through this app, with a sign-up.&lt;/p&gt;

&lt;p&gt;An app with &lt;code&gt;sign_in&lt;/code&gt; needs a Firebase project. &lt;code&gt;smf create&lt;/code&gt; offers to run &lt;code&gt;flutterfire configure&lt;/code&gt; once the app is generated. Until that command has run, the app compiles but stops at start-up with an &lt;code&gt;UnsupportedError&lt;/code&gt;, because its Firebase options are still a placeholder.&lt;/p&gt;

&lt;p&gt;As before, what you get is a regular Flutter project. It has no SMF package in its dependencies, and SMF never touches it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Flutter sign-in looks like after smf create
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sign-in, sign-up, password reset and an account screen
&lt;/h3&gt;

&lt;p&gt;The module &lt;code&gt;sign_in&lt;/code&gt; adds four screens. On three of them a user signs in, creates an account, or asks for the message that resets a password. The password reset says the same whether the address has an account or not. The fourth is the account screen, where the user signs out or deletes the account after a confirmation, and the settings screen gets a row that leads to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qp63ndcm6jw3jbitgwm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8qp63ndcm6jw3jbitgwm.png" alt="Four more screens of the generated app: the sign-up with the fields Email and Password and the button Create account, the password reset with the button Send link, the settings screen with the row Account above Theme and Language, and the sheet Delete your account? with a red button Delete and Cancel" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screens take their fields, buttons and colors from the theme of the app, and their texts are in English and Ukrainian. Every reason of a failure has a text of its own, such as a wrong password or no connection. As with the onboarding in 0.4, the tests put the screens on a phone of 320 by 480 with the text at three times its size. The &lt;a href="https://doc.saymyframe.com/modules/sign-in" rel="noopener noreferrer"&gt;page of the sign-in module&lt;/a&gt; shows each screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  One question: who may use the app without an account
&lt;/h3&gt;

&lt;p&gt;In an app with sign-in, &lt;code&gt;smf create&lt;/code&gt; asks one new question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;? Who may use the app without an account? (↑↓ to move, enter to choose)
❯ Nobody: the user signs in first
  Everyone: an account only where a screen needs one
  Everyone, as an anonymous user with an id from the start

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--auth-mode&lt;/code&gt; answers it in advance with &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;guest&lt;/code&gt; or &lt;code&gt;anonymous&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With the first answer, the sign-in shows in place of every screen until the user is signed in.&lt;/li&gt;
&lt;li&gt;With the second, the app is open. When the user asks for a screen that needs an account, such as the account screen, the sign-in opens over the page they are on. Back returns to that page, and after the sign-in the app shows the screen that was asked for.&lt;/li&gt;
&lt;li&gt;The third works the same way, and the app also signs in an anonymous user when it starts, so your code has the id of a user before anyone has an account. Signing up gives that user the account, with the same id.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code of the screens is the same in all three, and the answer is one constant in &lt;code&gt;lib/core/auth/app_session.dart&lt;/code&gt;. The &lt;a href="https://doc.saymyframe.com/guides/sign-in" rel="noopener noreferrer"&gt;sign-in guide&lt;/a&gt; describes the modes and how a screen of your own asks for an account.&lt;/p&gt;

&lt;h3&gt;
  
  
  The screens change the session, and the router does the rest
&lt;/h3&gt;

&lt;p&gt;No screen of &lt;code&gt;sign_in&lt;/code&gt; closes itself or navigates after a sign-in or a sign-out. The module declares two guards of the routes, and the router of the app shows the sign-in and takes it away by what they say. The route of the account screen asks for a condition:&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="n"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s"&gt;'/account'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'account'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;screen:&lt;/span&gt; &lt;span class="n"&gt;ScreenRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;'AccountScreen'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ImportRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$_folder&lt;/span&gt;&lt;span class="s"&gt;/account_screen.dart'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;conditions:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AuthRole&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;account&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 authentication role publishes &lt;code&gt;AuthRole.account&lt;/code&gt;, and &lt;code&gt;sign_in&lt;/code&gt; stands for it with a guard. A feature of your own that asks for the same condition gets the same guard without naming &lt;code&gt;sign_in&lt;/code&gt;. The module never mentions go_router either.&lt;/p&gt;

&lt;p&gt;The router role had to learn two things for this. The flow of a guard now closes itself: once the user is signed in, the router takes the sign-in away. And a route can ask for a condition, which opens the sign-in over the screen the user is on and brings the user back. &lt;a href="https://doc.saymyframe.com/concepts/guards" rel="noopener noreferrer"&gt;Guards of routes&lt;/a&gt; and &lt;a href="https://doc.saymyframe.com/concepts/conditions" rel="noopener noreferrer"&gt;conditions of routes&lt;/a&gt; have the rules.&lt;/p&gt;

&lt;h3&gt;
  
  
  BLoC or Riverpod over one look
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sign_in&lt;/code&gt; is the first module of SMF with a variant for each state manager. The views, the forms and the texts are the same files in both. With &lt;code&gt;bloc&lt;/code&gt;, each screen has a cubit, and with &lt;code&gt;riverpod&lt;/code&gt;, a provider that lasts as long as the screen. Either way the state signs in through &lt;code&gt;appSession&lt;/code&gt;, the session of the app, and knows nothing of Firebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase Authentication behind a role
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sign_in&lt;/code&gt; keeps no accounts. It requires the authentication role, and the module &lt;code&gt;firebase_auth&lt;/code&gt; provides that role with Firebase Authentication.&lt;/p&gt;

&lt;p&gt;A Firebase project needs one more thing than &lt;code&gt;flutterfire configure&lt;/code&gt;: the ways to sign in are enabled in the project, not in the code. The module generates a script of the app that enables them with the Firebase CLI, 15.6.0 or later, and &lt;code&gt;smf create&lt;/code&gt; offers to run it right after the configuration:&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="n"&gt;dart&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;enable_firebase_sign_in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dart&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asks first, because the Firebase CLI adds a web app named "Default Web App" to a project that has none. The Firebase console enables the same methods without it. If you answer no, or the run has no terminal, nothing went wrong, so the run doesn't warn. It lists the steps that are left, in the order to run them, after it says that the app is created. The &lt;a href="https://doc.saymyframe.com/guides/firebase#sign-in-methods-of-firebase-authentication" rel="noopener noreferrer"&gt;Firebase guide&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Firebase Authentication answers on a real device
&lt;/h2&gt;

&lt;p&gt;The service that talks to Firebase had tests before it met a device. They run the real firebase_auth package over a stand-in for its platform side, and they passed. Before I merged the module, I ran the same calls by hand on an Android emulator and an iPhone simulator, against a project with sign-in enabled and against one where Authentication had never been opened. The devices answered in ways the stand-in knew nothing about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a project without Authentication, Android answers with an unknown error that has &lt;code&gt;CONFIGURATION_NOT_FOUND&lt;/code&gt; in its message. iOS answers with an internal error whose message is the description of the error over many lines.&lt;/li&gt;
&lt;li&gt;Android refuses an empty password with a code that says nothing of the reason. iOS answers that the password is wrong.&lt;/li&gt;
&lt;li&gt;An anonymous user that Android kept on the device comes back with an empty email address instead of no address.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generated service evens these out, so a screen shows the same text on both platforms. It checks an empty field before it asks Firebase, takes an empty address for none, and gives each code of Firebase one of ten reasons that the screens have a text for. When sign-in is not enabled in the project, the user reads only that sign-in is unavailable, and in debug mode the screen adds a hint for you, with the command and the link that fix it. The &lt;a href="https://doc.saymyframe.com/modules/firebase-auth" rel="noopener noreferrer"&gt;page of the firebase_auth module&lt;/a&gt; lists every code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirteen roles and eighteen modules, checked in running apps
&lt;/h2&gt;

&lt;p&gt;0.4 had twelve roles and sixteen modules. 0.5 has thirteen and eighteen: the new role is authentication, and the new modules are &lt;code&gt;sign_in&lt;/code&gt; and &lt;code&gt;firebase_auth&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When I wrote this, CI generated 46 apps from the modules of the CLI and ran &lt;code&gt;flutter analyze&lt;/code&gt; and &lt;code&gt;flutter test&lt;/code&gt; in each. The tests of sign-in run in each of the three modes. CI also configures the app with every module for a real Firebase project, enables its sign-in methods there with the script, and starts the app on an Android emulator and on an iOS simulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMF 0.5 can't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sign-in is with an email address and a password. There is no sign-in with Google or Apple yet.&lt;/li&gt;
&lt;li&gt;Firebase Authentication is the only provider of the role, so an app with &lt;code&gt;sign_in&lt;/code&gt; needs a Firebase project and does not start until &lt;code&gt;flutterfire configure&lt;/code&gt; has run.&lt;/li&gt;
&lt;li&gt;The app doesn't ask a new user to confirm the email address, and it has no screen that changes the address or the password.&lt;/li&gt;
&lt;li&gt;Firebase may refuse to delete an account whose user signed in long ago. The screen then tells the user to sign out and sign in again.&lt;/li&gt;
&lt;li&gt;A screen that needs an account can't be a tab of the main navigation or the screen the app starts on.&lt;/li&gt;
&lt;li&gt;There is still no networking and no push notifications, and SMF still can't add a module to an app you already have.&lt;/li&gt;
&lt;li&gt;It generates Android and iOS apps for Flutter 3.44 or newer and Dart 3.12 or newer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;Sign-in with Google and Apple and push notifications are next on my list. Adding modules to an app that already exists is still on it too. There are no dates. SMF is a side project, and I'd rather ship things when they work.&lt;/p&gt;

&lt;p&gt;If you have a module in mind, tell me which one on &lt;a href="https://saymyframe.com/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or in &lt;a href="https://github.com/saymyframe/smf_flutter_cli/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 0.4
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No option of &lt;code&gt;smf create&lt;/code&gt; changed or went away. &lt;code&gt;--auth-mode&lt;/code&gt; is new.&lt;/li&gt;
&lt;li&gt;A step that you leave for later, such as &lt;code&gt;flutterfire configure&lt;/code&gt;, is no longer a warning. The run lists it after it says that the app is created.&lt;/li&gt;
&lt;li&gt;In an app without a main navigation, &lt;code&gt;smf create&lt;/code&gt; now warns of each destination that no screen leads to.&lt;/li&gt;
&lt;li&gt;The module API of &lt;code&gt;smf_contracts&lt;/code&gt; changed: a guard now names its stage, and a module that provides the router role handles new answers of the guards. A module written for 0.4 needs an update.&lt;/li&gt;
&lt;li&gt;Apps generated by 0.4 stay as they are.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://github.com/saymyframe/smf_flutter_cli/releases/tag/smf_flutter_cli-v0.5.0" rel="noopener noreferrer"&gt;release notes of SMF 0.5.0 on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try Flutter sign-in with smf create
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMF is tested on macOS, Linux and Windows. The &lt;a href="https://doc.saymyframe.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; starts with installation and your first app, the &lt;a href="https://doc.saymyframe.com/guides/sign-in" rel="noopener noreferrer"&gt;sign-in guide&lt;/a&gt; goes from the screens to the Firebase project, and the code is &lt;a href="https://github.com/saymyframe/smf_flutter_cli" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Stars there are welcome, but only if you actually like it.&lt;/p&gt;

</description>
      <category>release</category>
      <category>firebase</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>Fix the flutterfire upload-crashlytics-symbols error</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/fix-the-flutterfire-upload-crashlytics-symbols-error-3fhc</link>
      <guid>https://dev.to/ybeshkarov/fix-the-flutterfire-upload-crashlytics-symbols-error-3fhc</guid>
      <description>&lt;p&gt;&lt;code&gt;flutter run&lt;/code&gt; works, &lt;code&gt;flutter build ios&lt;/code&gt; works, and then &lt;code&gt;flutter build ipa&lt;/code&gt; fails in the build phase that the FlutterFire CLI adds for Firebase Crashlytics. This flutterfire upload-crashlytics-symbols error says that the upload script of Crashlytics is missing. The script is on the disk, but when Xcode archives an app that uses Swift Package Manager, the phase looks for it in the wrong directory. The fix changes one path in the Xcode project, and you have to apply it again after every &lt;code&gt;flutterfire configure&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Could not find the Crashlytics upload symbols script" in flutter build ipa
&lt;/h2&gt;

&lt;p&gt;The phase named &lt;code&gt;FlutterFire: "flutterfire upload-crashlytics-symbols"&lt;/code&gt; uploads the dSYM files of the iOS app to Firebase Crashlytics. With Swift Package Manager, the archive stops in it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unhandled exception:
Exception: Could not find the Crashlytics upload symbols script at "/Users/&amp;lt;you&amp;gt;/Library/Developer/Xcode/DerivedData/Runner-&amp;lt;hash&amp;gt;/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run". This usually means CocoaPods or Swift Package Manager has not finished installing "FirebaseCrashlytics" yet, or it was installed to an unexpected location. Try cleaning and rebuilding the project.
#0 UploadCrashlyticsSymbols.run (package:flutterfire_cli/src/commands/upload_symbols.dart:368:7)
Command PhaseScriptExecution failed with a nonzero exit code

**ARCHIVE FAILED**

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got it with flutterfire_cli 1.4.1, the latest version when I wrote this, Flutter 3.44.2 and Xcode 27. The message suggests cleaning and rebuilding. Flutter 3.44 keeps the Swift packages somewhere else, so the script will not turn up in DerivedData however often you clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it fails: flutter build ipa, but not flutter run
&lt;/h2&gt;

&lt;p&gt;You have this case when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the Firebase packages of the app come through Swift Package Manager, which Flutter 3.44 uses by default, and not through CocoaPods;&lt;/li&gt;
&lt;li&gt;flutterfire_cli 1.4.1 configured the app on a Mac (it adds the phase only on macOS);&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter run&lt;/code&gt; and &lt;code&gt;flutter build ios&lt;/code&gt; succeed, and the phase uploads the symbols;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutter build ipa&lt;/code&gt; fails. A comment in the upstream issue reports the same for Product &amp;gt; Archive in Xcode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;flutter run&lt;/code&gt; fails too, with &lt;code&gt;ProcessException: No such file or directory&lt;/code&gt;, an older flutterfire_cli wrote the phase. That case has its own section below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix for the flutterfire upload-crashlytics-symbols error
&lt;/h2&gt;

&lt;p&gt;The phase has the path &lt;code&gt;$BUILD_DIR/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run&lt;/code&gt; twice. Replace both with &lt;code&gt;$SRCROOT/../build/ios/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run&lt;/code&gt;. In the directory of the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;ruby&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="s1"&gt;'f = ARGV[0]; s = File.exist?(f) ? File.binread(f) : ""; t = s.gsub("$BUILD_DIR/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run", "$SRCROOT/../build/ios/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"); if t == s then puts "Nothing to fix in #{f}" else File.binwrite(f, t); puts "Fixed the Crashlytics phase in #{f}" end'&lt;/span&gt; &lt;span class="n"&gt;ios&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="no"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xcodeproj&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pbxproj&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It prints &lt;code&gt;Fixed the Crashlytics phase in ios/Runner.xcodeproj/project.pbxproj&lt;/code&gt;, or &lt;code&gt;Nothing to fix in ios/Runner.xcodeproj/project.pbxproj&lt;/code&gt; when the file has no such path, for example because you fixed it already. Nothing else in the project changes. You can make the same edit by hand in Xcode: the Runner target, Build Phases, the phase with the name above.&lt;/p&gt;

&lt;p&gt;After that, &lt;code&gt;flutter build ipa&lt;/code&gt; archives the app and the phase uploads the symbols. &lt;code&gt;flutter run&lt;/code&gt; and &lt;code&gt;flutter build ios&lt;/code&gt; work as before, because for them both paths are the same directory.&lt;/p&gt;

&lt;p&gt;Two caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;flutterfire configure&lt;/code&gt; writes the phase again when its script differs from the one it would write. Run the fix after every &lt;code&gt;flutterfire configure&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The new path assumes that Flutter builds into &lt;code&gt;build&lt;/code&gt;. If you changed that with &lt;code&gt;flutter config --build-dir&lt;/code&gt;, the path is wrong for you. Flutter exports &lt;code&gt;FLUTTER_APPLICATION_PATH&lt;/code&gt; and &lt;code&gt;FLUTTER_BUILD_DIR&lt;/code&gt; to the phase, so &lt;code&gt;"${FLUTTER_APPLICATION_PATH}/${FLUTTER_BUILD_DIR}/ios/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run"&lt;/code&gt; should follow your setting. I have tried only the default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the phase misses the Crashlytics upload script
&lt;/h2&gt;

&lt;p&gt;The script of the phase looks for the upload script in four places, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;$PODS_ROOT/FirebaseCrashlytics/run&lt;/code&gt;, for CocoaPods;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$BUILD_DIR/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the same &lt;code&gt;SourcePackages&lt;/code&gt; path in the DerivedData directory of the project;&lt;/li&gt;
&lt;li&gt;whatever &lt;code&gt;find&lt;/code&gt; turns up in &lt;code&gt;$BUILD_DIR&lt;/code&gt; and in that DerivedData directory.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With Swift Package Manager, Flutter clones the packages of the app into &lt;code&gt;build/ios/SourcePackages&lt;/code&gt;: it passes &lt;code&gt;-clonedSourcePackagesDirPath&lt;/code&gt; to &lt;code&gt;xcodebuild&lt;/code&gt;. For &lt;code&gt;flutter run&lt;/code&gt; and &lt;code&gt;flutter build ios&lt;/code&gt; it also passes &lt;code&gt;BUILD_DIR=&amp;lt;app&amp;gt;/build/ios&lt;/code&gt;, so the second place has the script. For an archive it leaves &lt;code&gt;BUILD_DIR&lt;/code&gt; alone, on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buildAction&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt;
          &lt;span class="nx"&gt;XcodeBuildAction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// dSYM files aren't copied to the archive if BUILD_DIR is set.&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BUILD_DIR=${globals.fs.path.absolute(buildDirectoryPath)}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So during &lt;code&gt;flutter build ipa&lt;/code&gt;, &lt;code&gt;$BUILD_DIR&lt;/code&gt; is the one Xcode picks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/Library/Developer/Xcode/DerivedData/Runner-&amp;lt;hash&amp;gt;/Build/Intermediates.noindex/ArchiveIntermediates/Runner/BuildProductsPath

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of the four places has the script. The phase hands flutterfire the DerivedData path of the third place, and that is the path in the error. The stable branch of Flutter had the same lines on the day I wrote this.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$SRCROOT&lt;/code&gt; is the &lt;code&gt;ios&lt;/code&gt; directory of the app. &lt;code&gt;$SRCROOT/../build/ios/SourcePackages&lt;/code&gt; is where Flutter put the packages, whatever &lt;code&gt;$BUILD_DIR&lt;/code&gt; says.&lt;/p&gt;

&lt;h2&gt;
  
  
  ProcessException: No such file or directory with flutterfire_cli 1.4.0 or older
&lt;/h2&gt;

&lt;p&gt;The phase that flutterfire_cli 1.4.0 and older versions write knows only CocoaPods and DerivedData. With Flutter 3.44 and Swift Package Manager it fails in every iOS build, &lt;code&gt;flutter run&lt;/code&gt; included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unhandled exception:
ProcessException: No such file or directory
  Command:
  /Users/&amp;lt;you&amp;gt;/Library/Developer/Xcode/DerivedData/Runner-&amp;lt;hash&amp;gt;/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run --validate --flutter-project .../app_id_file.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activating flutterfire_cli 1.4.1 does not fix it, because the old phase stays in the Xcode project. Activate it and configure the app again on a Mac, and the new phase replaces the old one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate flutterfire_cli 1.4.1
flutterfire configure

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply the fix above, for &lt;code&gt;flutter build ipa&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the fix stands in flutterfire_cli
&lt;/h2&gt;

&lt;p&gt;The upstream issue is &lt;a href="https://github.com/invertase/flutterfire_cli/issues/443" rel="noopener noreferrer"&gt;invertase/flutterfire_cli#443&lt;/a&gt;, open since May 2026. Version 1.4.1, from August, added the lookup in &lt;code&gt;$BUILD_DIR/SourcePackages&lt;/code&gt;, which covers &lt;code&gt;flutter run&lt;/code&gt; and &lt;code&gt;flutter build ios&lt;/code&gt;. The comments of the issue describe the archive case, the latest of them from the end of September. Two open pull requests change the lookup: &lt;a href="https://github.com/invertase/flutterfire_cli/pull/444" rel="noopener noreferrer"&gt;#444&lt;/a&gt; and &lt;a href="https://github.com/invertase/flutterfire_cli/pull/456" rel="noopener noreferrer"&gt;#456&lt;/a&gt;. &lt;a href="https://github.com/invertase/flutterfire_cli/issues/428" rel="noopener noreferrer"&gt;#428&lt;/a&gt; is about the same phase in macOS apps, which I have not tried.&lt;/p&gt;

&lt;p&gt;When a release with one of them is out, activate it and run &lt;code&gt;flutterfire configure&lt;/code&gt; again. You will not need the fix from this post then.&lt;/p&gt;

&lt;h2&gt;
  
  
  How SMF applies the fix after flutterfire configure
&lt;/h2&gt;

&lt;p&gt;I ran into this while building &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;SMF&lt;/a&gt;, a free, open-source Flutter project generator. &lt;a href="https://saymyframe.com/blog/smf-0-3-flutter-project-generator" rel="noopener noreferrer"&gt;The launch post of SMF 0.3&lt;/a&gt; says what it does today and what it can't do yet. For an app with Firebase Crashlytics, SMF runs this fix on macOS right after &lt;code&gt;flutterfire configure&lt;/code&gt;, and puts the command into the README of the app for the next time you configure it yourself. Its CI archives a generated app with &lt;code&gt;flutter build ipa --no-codesign&lt;/code&gt; and the fixed phase. Another job runs the real &lt;code&gt;flutterfire configure&lt;/code&gt; and fails when the phase no longer has the path that the fix replaces, so a change upstream shows up as a red job. The &lt;a href="https://doc.saymyframe.com/guides/firebase#crashlytics-and-flutter-build-ipa" rel="noopener noreferrer"&gt;Firebase guide of SMF&lt;/a&gt; has the details.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>firebase</category>
      <category>ios</category>
      <category>opensource</category>
    </item>
    <item>
      <title>SMF 0.4: a Flutter starter app with onboarding and settings</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/smf-04-a-flutter-starter-app-with-onboarding-and-settings-3kah</link>
      <guid>https://dev.to/ybeshkarov/smf-04-a-flutter-starter-app-with-onboarding-and-settings-3kah</guid>
      <description>&lt;p&gt;Say My Frame (SMF) generates Flutter apps from independent modules. &lt;a href="https://pub.dev/packages/smf_flutter_cli" rel="noopener noreferrer"&gt;Version 0.4 is out on pub.dev&lt;/a&gt;, and what it generates now is a Flutter starter app that you can show to someone. It opens with an onboarding, greets you on a start screen that lists what to do next, and has a settings screen where the user chooses the theme and the language. The post about 0.3 said that the app you get is a skeleton. This one is about what replaced the skeleton, what is still missing, and what I plan next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create my_app &lt;span class="nt"&gt;-m&lt;/span&gt; home,onboarding,settings,bottom_tabs,material_theme,gen_l10n,get_it,bloc

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-4-flutter-starter-app%2Fsmf-create.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fsaymyframe.com%2Fblog%2Fsmf-0-4-flutter-starter-app%2Fsmf-create.gif" alt="smf create in a terminal asks for the app name and the modules, adds go_router and shared_preferences, which the chosen modules need, and generates a Flutter app with an onboarding, a start screen, a settings screen, tabs at the bottom, a theme, two languages, get_it and BLoC" width="759" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recording answers the questions of &lt;code&gt;smf create&lt;/code&gt; instead of passing &lt;code&gt;-m&lt;/code&gt;. Either way, SMF adds &lt;code&gt;go_router&lt;/code&gt; and &lt;code&gt;shared_preferences&lt;/code&gt; by itself: the start screen needs a router, and the onboarding needs a place to remember that the user has finished it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6ay29kxe9uvdfc76t8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6ay29kxe9uvdfc76t8v.png" alt="The app that smf create generates, on a phone: the onboarding, the start screen, the settings screen, and the start screen in the dark theme and in Ukrainian" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;home page of Say My Frame&lt;/a&gt; has a half-minute video of a walk through this app.&lt;/p&gt;

&lt;p&gt;0.3 put the work into the module model, and the app it generated showed none of it: after &lt;code&gt;smf create&lt;/code&gt; you saw one screen with the name of the app. 0.4 went into the app.&lt;/p&gt;

&lt;p&gt;As before, what you get is a regular Flutter project. It has no SMF package in its dependencies, and SMF never touches it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Flutter starter app has after smf create
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A Flutter onboarding screen on the first launch
&lt;/h3&gt;

&lt;p&gt;The module &lt;code&gt;onboarding&lt;/code&gt; adds two pages that a new user goes through once. Its buttons only finish the onboarding and navigate nowhere. The module declares a guard of the routes, and the router of the app shows the onboarding in place of every other screen until the user has finished it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;guards:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;RouteGuard(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;name:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'firstRun'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;allows:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;FunctionRef('onboardingCompleted'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;import:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;_status)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;redirectTo:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;_route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;_status&lt;/code&gt; is the file with that function, and &lt;code&gt;_route&lt;/code&gt; is the route of the onboarding itself. The module never mentions go_router. Guards are part of the router role, so another router would have to ask them the same way. The app remembers that the onboarding is finished, and &lt;code&gt;onboardingStatus.restart()&lt;/code&gt; shows it again.&lt;/p&gt;

&lt;p&gt;The pages are a list in &lt;code&gt;lib/features/onboarding/onboarding_pages.dart&lt;/code&gt;, and you replace them with your own:&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="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Widget&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onboardingPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&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="p"&gt;[&lt;/span&gt;
  &lt;span class="n"&gt;OnboardingPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;symbol:&lt;/span&gt; &lt;span class="s"&gt;'Ma'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'My App'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;l10n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onboardingWelcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;OnboardingPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;icon:&lt;/span&gt; &lt;span class="n"&gt;Icons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;check_rounded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;l10n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onboardingReadyTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;l10n&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onboardingReady&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;The &lt;a href="https://doc.saymyframe.com/modules/onboarding" rel="noopener noreferrer"&gt;page of the onboarding module&lt;/a&gt; has the rest, and &lt;a href="https://doc.saymyframe.com/concepts/guards" rel="noopener noreferrer"&gt;guards of routes&lt;/a&gt; explains how a module keeps the user away from a screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  A start screen written for the developer
&lt;/h3&gt;

&lt;p&gt;In 0.3 the start screen of &lt;code&gt;home&lt;/code&gt; showed the name of the app. Now it is a welcome for you, the developer. It names the app, greets you by the time of the day and lists three next steps. Each step has the path of a file, and a tap copies it. The first step is to replace this screen with the first screen of your app, and the &lt;a href="https://doc.saymyframe.com/modules/home" rel="noopener noreferrer"&gt;page of the home module&lt;/a&gt; shows how.&lt;/p&gt;

&lt;h3&gt;
  
  
  A settings screen that the modules fill
&lt;/h3&gt;

&lt;p&gt;The module &lt;code&gt;settings&lt;/code&gt; adds a settings screen and knows nothing about what is on it. Modules and roles give it entries. In an app with a theme and a localization, the screen has the theme mode (light, dark or the one of the device) and the language.&lt;/p&gt;

&lt;p&gt;Neither entry comes from the module that provides the theme or the texts. The theme mode belongs to the theme role and the language to the localization role, so a second theme module would get the same entry without writing it. In an app whose modules have no setting, the screen shows a note for the developer instead of an empty list.&lt;/p&gt;

&lt;h3&gt;
  
  
  A light and a dark Material 3 theme with a bundled font
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;material_theme&lt;/code&gt; generates &lt;code&gt;lib/core/theme/app_theme.dart&lt;/code&gt; with a palette, text styles and the look of the components. That file is yours to edit, and the &lt;a href="https://doc.saymyframe.com/guides/theme" rel="noopener noreferrer"&gt;theme guide&lt;/a&gt; says what to change and where. The font files are in &lt;code&gt;assets/fonts/geist/&lt;/code&gt; with their license, so the app loads no font from the network and gets no package for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flutter localization in two languages with gen-l10n
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;gen_l10n&lt;/code&gt; writes the texts of the modules into ARB files, one for each language, and &lt;code&gt;flutter pub get&lt;/code&gt; generates the class that code reads them through. The modules come with texts in English and Ukrainian. &lt;code&gt;--locales&lt;/code&gt; chooses the languages of the app. Until the user chooses one, the app follows the device.&lt;/p&gt;

&lt;p&gt;The theme mode and the language are remembered between launches. &lt;code&gt;shared_preferences&lt;/code&gt; keeps them, and it is meant only for settings that are no secret. Code changes them with one call each:&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;appThemeMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;choose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThemeMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;appLocale&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;choose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'uk'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://doc.saymyframe.com/guides/languages" rel="noopener noreferrer"&gt;languages guide&lt;/a&gt; and the &lt;a href="https://doc.saymyframe.com/guides/preferences" rel="noopener noreferrer"&gt;preferences guide&lt;/a&gt; have the details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Large text, less motion and a screen reader
&lt;/h3&gt;

&lt;p&gt;I did not want screens that look right only on my phone. The tests of the onboarding and of the start screen put them on a phone of 320 by 480 with the text at three times its size, in each language of the app, and fail when something overflows. The motion of these screens ends, and an app that asks for less motion shows them at once. Titles are headers for a screen reader, and each tab of the bar at the bottom is one button for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A guide for coding agents in every app
&lt;/h3&gt;

&lt;p&gt;Every generated app has &lt;code&gt;AGENTS.md&lt;/code&gt; and &lt;code&gt;CLAUDE.md&lt;/code&gt;. The guide says where the files of each role are, how code of the app uses them and what code must not do. The roles and the modules of the app write its sections, so it describes this app and not SMF in general. There is an example in the &lt;a href="https://doc.saymyframe.com/guides/coding-agents" rel="noopener noreferrer"&gt;coding agents guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twelve roles and sixteen modules that still don't know each other
&lt;/h2&gt;

&lt;p&gt;0.3 had eight roles and eleven modules. 0.4 has twelve roles and sixteen modules. The new roles are the preferences, the settings screen, the localization and the theme, and the new modules are &lt;code&gt;onboarding&lt;/code&gt;, &lt;code&gt;settings&lt;/code&gt;, &lt;code&gt;material_theme&lt;/code&gt;, &lt;code&gt;gen_l10n&lt;/code&gt; and &lt;code&gt;shared_preferences&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule of 0.3 held: a module names roles and never another module. A feature shows its texts through the localization role, and in an app without it the same feature shows them in English. The screens read their colors from the theme of the app, whichever module provides it, and they work in an app with no theme module too.&lt;/p&gt;

&lt;p&gt;CI checks this by running the apps. When I wrote this, it generated 32 apps from the modules of the CLI and ran &lt;code&gt;flutter analyze&lt;/code&gt; and &lt;code&gt;flutter test&lt;/code&gt; in each. It also starts apps on an Android emulator and on an iOS simulator. Tests of the roles run inside the generated apps. To show that these tests can fail, the repository keeps a provider with one planted bug for each role they cover, and CI checks that the tests fail on it. &lt;a href="https://doc.saymyframe.com/concepts/independence" rel="noopener noreferrer"&gt;Module independence&lt;/a&gt; in the docs describes the rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMF 0.4 can't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;There is no sign-in, no networking and no push notifications.&lt;/li&gt;
&lt;li&gt;SMF creates new apps. It still can't add a module to an app you already have.&lt;/li&gt;
&lt;li&gt;The texts of the modules are in English and Ukrainian. For another language of &lt;code&gt;--locales&lt;/code&gt;, the app shows them in English until you translate its ARB file.&lt;/li&gt;
&lt;li&gt;Most roles have one provider: one router, one theme, one way to keep the texts.&lt;/li&gt;
&lt;li&gt;It generates Android and iOS apps for Flutter 3.44 or newer and Dart 3.12 or newer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;Sign-in is next on my list: email and password, and screens only for users who are signed in. The guards of routes that the onboarding uses were built with that in mind. Adding modules to an app that already exists is still on the list too. There are no dates. SMF is a side project, and I'd rather ship things when they work.&lt;/p&gt;

&lt;p&gt;If you have a module in mind, tell me which one on &lt;a href="https://saymyframe.com/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or in &lt;a href="https://github.com/saymyframe/smf_flutter_cli/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 0.3
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No option of &lt;code&gt;smf create&lt;/code&gt; changed or went away. &lt;code&gt;--locales&lt;/code&gt; is new.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;home&lt;/code&gt; is a welcome with next steps now, and &lt;code&gt;bottom_tabs&lt;/code&gt; draws its bar in the colors of the theme.&lt;/li&gt;
&lt;li&gt;The module API of &lt;code&gt;smf_contracts&lt;/code&gt; changed in several places, so a module written for 0.3 needs an update.&lt;/li&gt;
&lt;li&gt;Apps generated by 0.3 stay as they are. SMF generates an app once and never touches it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://github.com/saymyframe/smf_flutter_cli/releases/tag/smf_flutter_cli-v0.4.0" rel="noopener noreferrer"&gt;release notes of SMF 0.4.0 on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the Flutter starter app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMF is tested on macOS, Linux and Windows. The &lt;a href="https://doc.saymyframe.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; starts with installation and your first app, the &lt;a href="https://doc.saymyframe.com/getting-started/generated-app" rel="noopener noreferrer"&gt;tour of a generated app&lt;/a&gt; goes through its files, and the code is &lt;a href="https://github.com/saymyframe/smf_flutter_cli" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Stars there are welcome, but only if you actually like it.&lt;/p&gt;

</description>
      <category>release</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>SMF 0.3: an open-source Flutter project generator</title>
      <dc:creator>Yevhen Beshkarov</dc:creator>
      <pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/ybeshkarov/smf-03-an-open-source-flutter-project-generator-5dja</link>
      <guid>https://dev.to/ybeshkarov/smf-03-an-open-source-flutter-project-generator-5dja</guid>
      <description>&lt;p&gt;Say My Frame (SMF) is a free, open-source Flutter project generator whose modules are checked together: its CI generates apps from many combinations of them and analyzes each one. It also sets up Firebase Crashlytics and Analytics all the way to &lt;code&gt;flutter build ipa&lt;/code&gt;. You choose what the app needs, such as go_router, tabs at the bottom, get_it, and BLoC or Riverpod, and &lt;code&gt;smf create&lt;/code&gt; generates a Flutter project in which these parts already work together. &lt;a href="https://pub.dev/packages/smf_flutter_cli" rel="noopener noreferrer"&gt;Version 0.3 is out on pub.dev&lt;/a&gt;. It does less than the word "generator" might promise, so this post is about what it does today, what it can't do yet, and where I want to take it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create my_app &lt;span class="nt"&gt;--org&lt;/span&gt; com.example &lt;span class="nt"&gt;-m&lt;/span&gt; home,bottom_tabs,get_it,bloc &lt;span class="nt"&gt;--no-input&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdoc.saymyframe.com%2Fdemo%2Fsmf_create.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdoc.saymyframe.com%2Fdemo%2Fsmf_create.gif" alt="smf create in a terminal asks for the app name, the organization and the modules, adds go_router for the start screen and generates a Flutter app with tabs at the bottom, BLoC and get_it" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What you get is a regular Flutter project. It has no SMF package in its dependencies, and nothing in it has to be generated again, so the code is yours from the first commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SMF 0.3 is a rewrite
&lt;/h2&gt;

&lt;p&gt;SMF 0.1 came out in August 2025, and 0.2 followed in September. Each module brought its own templates, and an engine patched the shared files. It worked for the combinations I had tried.&lt;/p&gt;

&lt;p&gt;This September I ran the published 0.2 with Flutter 3.44. It reported success and generated apps that did not build. Some modules assumed go_router or get_it, and CI back then never generated an app or ran the analyzer, so nothing caught it.&lt;/p&gt;

&lt;p&gt;0.3 starts from one rule: modules do not know each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Flutter project generator does today
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Modules name roles, not other modules
&lt;/h3&gt;

&lt;p&gt;The start screen needs a router, not go_router. The analytics module logs screen views when the app has a router, whichever router that is. SMF calls these shared parts roles, and there are eight of them: app entry, router, layout, state management, dependency injection, events, crash reporting and analytics. A role can have more than one provider. BLoC and Riverpod both provide state management.&lt;/p&gt;

&lt;p&gt;Modules don't edit files either. Shared files, such as &lt;code&gt;bootstrap.dart&lt;/code&gt;, have sockets: named places where modules put code without knowing who else puts code there. This is how &lt;code&gt;firebase_analytics&lt;/code&gt; hands the router a listener of the screen, and only when the app has a router:&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;const&lt;/span&gt; &lt;span class="n"&gt;SocketContribution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;RouterRole&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;screenListeners&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Fragment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;'logFirebaseScreenView'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;imports:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;ImportRef&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;'core/analytics/firebase_analytics_service.dart'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;show:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'logFirebaseScreenView'&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;span class="k"&gt;when&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;routerRole&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;Before it writes a single file, &lt;code&gt;smf create&lt;/code&gt; checks the rules. A module may put code only into the sockets of its own roles, of the app entry and of the modules it depends on, and every file of the app has exactly one owner. The tests of every module run the same checks. CI then generates apps from combinations of the modules, 18 of them for the modules of the CLI when I wrote this, runs &lt;code&gt;flutter analyze&lt;/code&gt; on each one and runs tests in some of them. The &lt;a href="https://doc.saymyframe.com/concepts/module-model" rel="noopener noreferrer"&gt;module model&lt;/a&gt; and &lt;a href="https://doc.saymyframe.com/concepts/independence" rel="noopener noreferrer"&gt;module independence&lt;/a&gt; pages of the docs go deeper.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firebase Crashlytics and Analytics, set up all the way to &lt;code&gt;flutter build ipa&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Three modules bring Firebase: &lt;code&gt;firebase_core&lt;/code&gt;, &lt;code&gt;firebase_crashlytics&lt;/code&gt; and &lt;code&gt;firebase_analytics&lt;/code&gt;. Before it generates anything, SMF checks the machine for the Firebase CLI, a Firebase login and the FlutterFire CLI. In a terminal it offers to set up what is missing, and asks first. After generation it offers to run &lt;code&gt;flutterfire configure&lt;/code&gt; with the ids of the app.&lt;/p&gt;

&lt;p&gt;Two details took longer than the rest. With a router, &lt;code&gt;firebase_analytics&lt;/code&gt; logs one screen view for each screen the user sees, tab switches included. And on macOS, SMF fixes the Crashlytics build phase that flutterfire_cli 1.4.1 adds, because &lt;code&gt;flutter build ipa&lt;/code&gt; fails in that phase when the app uses Swift Package Manager. That story gets a post of its own. Until then, the &lt;a href="https://doc.saymyframe.com/guides/firebase" rel="noopener noreferrer"&gt;Firebase guide&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Flutter app with go_router, tabs, get_it and BLoC or Riverpod
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;-m home,bottom_tabs,get_it,event_bus&lt;/code&gt;, SMF adds two more modules and tells you why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Adding flutter_core: the only provider of the app entry role, which every app needs.
Adding go_router: the only provider of the router role, which home requires.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Screens navigate with &lt;code&gt;context.nav.home.home().go()&lt;/code&gt;, which doesn't mention go_router. Code that needs a service asks the &lt;code&gt;ServiceLocator&lt;/code&gt; of the DI role, which doesn't mention get_it. With &lt;code&gt;bottom_tabs&lt;/code&gt;, the destinations of the features become tabs of a &lt;code&gt;NavigationBar&lt;/code&gt;, and each tab keeps its own stack. &lt;code&gt;bootstrap()&lt;/code&gt; runs the start-up code of the modules in four phases before the first frame. The Android and iOS projects are the ones &lt;code&gt;flutter create&lt;/code&gt; writes in Flutter 3.44.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://doc.saymyframe.com/getting-started/generated-app" rel="noopener noreferrer"&gt;tour of a generated app&lt;/a&gt; goes file by file and names the module that wrote each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  A plan before anything is written
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;--explain&lt;/code&gt; prints what would be generated and stops. It lists the modules and why each is there, the roles, the dependencies, the commands that would run after generation and the state of the machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;Modules&lt;/span&gt;
  &lt;span class="s"&gt;home&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;requested&lt;/span&gt;
  &lt;span class="s"&gt;bottom_tabs&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;requested&lt;/span&gt;
  &lt;span class="s"&gt;get_it&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;requested&lt;/span&gt;
  &lt;span class="s"&gt;event_bus&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;requested&lt;/span&gt;
  &lt;span class="s"&gt;flutter_core&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;the only provider of the app entry role, which every app needs&lt;/span&gt;
  &lt;span class="s"&gt;go_router&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;the only provider of the router role, which home requires&lt;/span&gt;

&lt;span class="s"&gt;Roles&lt;/span&gt;
  &lt;span class="s"&gt;Layout&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bottom_tabs&lt;/span&gt;
  &lt;span class="s"&gt;Dependency injection&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get_it&lt;/span&gt;
  &lt;span class="s"&gt;Events&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;event_bus&lt;/span&gt;
  &lt;span class="s"&gt;App entry&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter_core&lt;/span&gt;
  &lt;span class="s"&gt;Router&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;go_router&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For scripts and CI there are &lt;code&gt;--no-input&lt;/code&gt;, &lt;code&gt;--skip-external-setup&lt;/code&gt;, &lt;code&gt;--strict&lt;/code&gt; and exit codes: 0, 1, 64, 70 and 130. SMF renders the app in memory and finishes it in a temporary directory, so a run that fails or that you cancel leaves no half-made app in your project. The &lt;a href="https://doc.saymyframe.com/guides/smf-create" rel="noopener noreferrer"&gt;reference of smf create&lt;/a&gt; lists every option.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules of your own
&lt;/h3&gt;

&lt;p&gt;Modules are Dart packages built on &lt;code&gt;smf_contracts&lt;/code&gt;, and yours follow the same rules as the built-in ones. The contract harness in &lt;code&gt;smf_pipeline&lt;/code&gt; generates in memory every app that matters for your module and checks it. To generate apps with your module, you write a small command of your own with &lt;code&gt;runCli&lt;/code&gt; and &lt;code&gt;smfModules&lt;/code&gt;. &lt;a href="https://doc.saymyframe.com/extending" rel="noopener noreferrer"&gt;Extending SMF&lt;/a&gt; shows how, and one of its pages builds &lt;a href="https://doc.saymyframe.com/extending/feature-module" rel="noopener noreferrer"&gt;a counter feature with BLoC and Riverpod variants&lt;/a&gt; from start to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SMF 0.3 can't do yet
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The app you get is a skeleton. There is one feature module, &lt;code&gt;home&lt;/code&gt;, a start screen with the name of the app. The tab bar stays hidden until a second screen is a destination, so after &lt;code&gt;smf create&lt;/code&gt; you see one screen.&lt;/li&gt;
&lt;li&gt;There are no modules for sign-in, networking, localization or theming. The list has eleven modules, and I wanted the foundation to hold before adding more.&lt;/li&gt;
&lt;li&gt;SMF creates new apps. It can't add a module to an app you already have.&lt;/li&gt;
&lt;li&gt;It generates Android and iOS apps for Flutter 3.44 or newer and Dart 3.12 or newer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;flutterfire configure&lt;/code&gt; needs you at the keyboard to log in and pick a Firebase project. In CI, SMF leaves it for later and prints the command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's going
&lt;/h2&gt;

&lt;p&gt;Two things are next on my list: modules with real screens, and a way to add modules to an app that already exists. There are no dates. SMF is a side project, and I'd rather ship things when they work.&lt;/p&gt;

&lt;p&gt;If you have a module in mind, tell me which one on &lt;a href="https://saymyframe.com/discord" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; or in &lt;a href="https://github.com/saymyframe/smf_flutter_cli/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 0.2
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The state manager is a module now: &lt;code&gt;-m bloc&lt;/code&gt; or &lt;code&gt;-m riverpod&lt;/code&gt; instead of &lt;code&gt;-s&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--start&lt;/code&gt; replaces &lt;code&gt;-r&lt;/code&gt; and &lt;code&gt;--route&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--strict&lt;/code&gt; and &lt;code&gt;--on-conflict&lt;/code&gt; are options of &lt;code&gt;create&lt;/code&gt; now, so they go after it: &lt;code&gt;smf create my_app --strict&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tabs at the bottom are a module of their own, &lt;code&gt;bottom_tabs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The module API of &lt;code&gt;smf_contracts&lt;/code&gt; is new, so modules written for 0.2 need a rewrite.&lt;/li&gt;
&lt;li&gt;Apps generated by 0.2 stay as they are. SMF generates an app once and never touches it again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same list, with what's new in 0.3, is in the &lt;a href="https://github.com/saymyframe/smf_flutter_cli/releases/tag/smf_flutter_cli-v0.3.0" rel="noopener noreferrer"&gt;release notes of SMF 0.3.0 on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the Flutter project generator
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dart pub global activate smf_flutter_cli
smf create

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SMF is tested on macOS, Linux and Windows. The &lt;a href="https://doc.saymyframe.com/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; starts with installation and your first app, the &lt;a href="https://saymyframe.com/" rel="noopener noreferrer"&gt;Say My Frame site&lt;/a&gt; shows the roles and modules at a glance, and the code is &lt;a href="https://github.com/saymyframe/smf_flutter_cli" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;. Stars there are welcome, but only if you actually like it.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>opensource</category>
      <category>firebase</category>
    </item>
  </channel>
</rss>
