<?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: koreanDev</title>
    <description>The latest articles on DEV Community by koreanDev (@devsnake).</description>
    <link>https://dev.to/devsnake</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3808604%2F84f059eb-e8dd-43e2-bf9e-d4d26f79f155.png</url>
      <title>DEV Community: koreanDev</title>
      <link>https://dev.to/devsnake</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devsnake"/>
    <language>en</language>
    <item>
      <title>Why I Switched from http to Dio in Flutter — Centralizing Error Tracking with Interceptors</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Sat, 18 Apr 2026 05:46:47 +0000</pubDate>
      <link>https://dev.to/devsnake/why-i-switched-from-http-to-dio-in-flutter-centralizing-error-tracking-with-interceptors-5d8f</link>
      <guid>https://dev.to/devsnake/why-i-switched-from-http-to-dio-in-flutter-centralizing-error-tracking-with-interceptors-5d8f</guid>
      <description>&lt;p&gt;Your Flutter app has Crashlytics. Global handlers are set up. Feels safe.&lt;/p&gt;

&lt;p&gt;Open the dashboard. How many API errors are recorded? Probably zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Dart's global error handler only catches &lt;strong&gt;uncaught Errors&lt;/strong&gt;. In Dart, Error and Exception are different things. &lt;code&gt;StackOverflowError&lt;/code&gt; is an Error — programming mistake. API failure is an Exception — expected, recoverable.&lt;/p&gt;

&lt;p&gt;The thing is, API exceptions get caught in try-catch blocks. They never bubble up to the global handler. Crashlytics never sees them. Complete blind spot.&lt;/p&gt;

&lt;p&gt;To track Exceptions, you need to record them separately. That's why I refactored my solo app Book Log's network layer from &lt;code&gt;http&lt;/code&gt; to &lt;code&gt;Dio&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before: http package
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// auth_repository.dart — repeat this for every request&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="s"&gt;'serverToken'&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kt"&gt;Uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$_baseUrl&lt;/span&gt;&lt;span class="s"&gt;/user/fcm-token'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Bearer &lt;/span&gt;&lt;span class="si"&gt;$token&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Content-Type'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;'fcm_token'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fcmToken&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;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&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="c1"&gt;// Crashlytics? gotta add it manually here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had an &lt;code&gt;ApiClient&lt;/code&gt; wrapper class. &lt;code&gt;BookRepositoryImpl&lt;/code&gt; and &lt;code&gt;SentenceRepositoryImpl&lt;/code&gt; used it. But &lt;code&gt;AuthRepository&lt;/code&gt; was calling &lt;code&gt;http&lt;/code&gt; directly. Crashlytics recording only happened inside &lt;code&gt;ApiClient._execute()&lt;/code&gt;. All auth-related errors were in a blind spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  After: Dio + Interceptors
&lt;/h2&gt;

&lt;p&gt;Two interceptors. That's it.&lt;/p&gt;

&lt;h3&gt;
  
  
  AuthInterceptor — auto token injection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_AuthInterceptor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;_AuthInterceptor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_storage&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;FlutterSecureStorage&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;RequestOptions&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;RequestInterceptorHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;authenticated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;extra&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'authenticated'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;true&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;authenticated&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;true&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;key:&lt;/span&gt; &lt;span class="n"&gt;_kServerTokenKey&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;token&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="n"&gt;DioException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nl"&gt;requestOptions:&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;ApiException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;statusCode:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;'Not logged in'&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="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'Bearer &lt;/span&gt;&lt;span class="si"&gt;$token&lt;/span&gt;&lt;span class="s"&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;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&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;&lt;code&gt;options.extra&lt;/code&gt; is a custom metadata map from Dio. Doesn't affect the HTTP request. Not sent to the server. Just a switch for the interceptor to decide: attach token or not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;authenticated: true&lt;/code&gt; → token injected (post-login APIs)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authenticated: false&lt;/code&gt; → no token (Apple login, pre-auth APIs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CrashlyticsInterceptor — auto error logging
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_CrashlyticsInterceptor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DioException&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrorInterceptorHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;FirebaseCrashlytics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stackTrace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;reason:&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${err.requestOptions.method}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;${err.requestOptions.path}&lt;/span&gt;&lt;span class="s"&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;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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;Every HTTP error recorded to Crashlytics. The &lt;code&gt;reason&lt;/code&gt; shows &lt;code&gt;POST /user/fcm-token&lt;/code&gt; so you know which API failed. Unlike the global handler, this catches all API failures regardless of try-catch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kt"&gt;Uri&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$_baseUrl&lt;/span&gt;&lt;span class="s"&gt;/user/fcm-token'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'Authorization'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Bearer &lt;/span&gt;&lt;span class="si"&gt;$token&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'Content-Type'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s"&gt;'fcm_token'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fcmToken&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_apiClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'/user/fcm-token'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'fcm_token'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fcmToken&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token injection, error handling, Crashlytics logging — all automatic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration scope
&lt;/h2&gt;

&lt;p&gt;Only two files were using &lt;code&gt;http&lt;/code&gt; directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;api_client.dart&lt;/code&gt; → replaced with Dio. Kept existing method signatures.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;auth_repository.dart&lt;/code&gt; → now injects &lt;code&gt;ApiClient&lt;/code&gt; instead of calling &lt;code&gt;http&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;BookRepositoryImpl&lt;/code&gt; and &lt;code&gt;SentenceRepositoryImpl&lt;/code&gt; already used &lt;code&gt;ApiClient&lt;/code&gt;. Zero changes needed. Keeping the &lt;code&gt;get/post/patch&lt;/code&gt; signatures the same made this possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 bugs from Claude Code
&lt;/h2&gt;

&lt;p&gt;I delegated this migration to Claude Code. Overall result was solid. But server logs told a different story.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Wrong endpoint path
&lt;/h3&gt;

&lt;p&gt;Original code used &lt;code&gt;/validate-token&lt;/code&gt; from env variable. After migration it became &lt;code&gt;/auth/validate&lt;/code&gt;. Route doesn't exist. &lt;strong&gt;404.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Token not attached
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;validateToken&lt;/code&gt; was set to &lt;code&gt;authenticated: false&lt;/code&gt;. Token validation API not sending the token. &lt;strong&gt;400.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. JSON key mismatch
&lt;/h3&gt;

&lt;p&gt;Client sending &lt;code&gt;fcmToken&lt;/code&gt; (camelCase). Server expecting &lt;code&gt;fcm_token&lt;/code&gt; (snake_case). &lt;strong&gt;400.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Found all three by running &lt;code&gt;gcloud app logs tail&lt;/code&gt;. AI-generated code still needs manual verification. Especially env variables and API schemas — AI doesn't track those well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before (http)&lt;/th&gt;
&lt;th&gt;After (Dio)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token injection&lt;/td&gt;
&lt;td&gt;manual per request&lt;/td&gt;
&lt;td&gt;interceptor auto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crashlytics&lt;/td&gt;
&lt;td&gt;only via ApiClient&lt;/td&gt;
&lt;td&gt;all APIs auto&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error handling&lt;/td&gt;
&lt;td&gt;duplicated per file&lt;/td&gt;
&lt;td&gt;centralized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AuthRepository&lt;/td&gt;
&lt;td&gt;direct http calls&lt;/td&gt;
&lt;td&gt;via ApiClient&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Set up error tracking infra before going to production. This migration was that work.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>refactoring</category>
      <category>dart</category>
      <category>api</category>
    </item>
    <item>
      <title>I Debugged Xcode for Hours. The Bug Was in My Joi Schema.</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Fri, 17 Apr 2026 02:48:01 +0000</pubDate>
      <link>https://dev.to/devsnake/i-debugged-xcode-for-hours-the-bug-was-in-my-joi-schema-38i8</link>
      <guid>https://dev.to/devsnake/i-debugged-xcode-for-hours-the-bug-was-in-my-joi-schema-38i8</guid>
      <description>&lt;h1&gt;
  
  
  I Debugged Xcode for Hours. The Bug Was in My Joi Schema.
&lt;/h1&gt;

&lt;p&gt;My first iOS app got rejected by Apple.&lt;/p&gt;

&lt;p&gt;Reason: "Error when logging in with Apple account."&lt;/p&gt;

&lt;p&gt;I had seen the exact same error a few days before. That time, it was a missing capability in Xcode. So I assumed the same cause.&lt;/p&gt;

&lt;p&gt;I spent hours checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provisioning profile&lt;/li&gt;
&lt;li&gt;App ID capability&lt;/li&gt;
&lt;li&gt;Distribution certificate&lt;/li&gt;
&lt;li&gt;Sign in with Apple key&lt;/li&gt;
&lt;li&gt;Bundle ID&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything was fine.&lt;/p&gt;

&lt;p&gt;Then I opened the server logs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /apple/login → 400
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The request was reaching the server with all the data. But the server was returning 400.&lt;/p&gt;

&lt;p&gt;The real cause? During a refactoring session with an AI coding tool, a Joi validation middleware was added. The schema was missing one field that the client was sending — &lt;code&gt;identityToken&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The controller never ran. Joi blocked it silently.&lt;/p&gt;

&lt;p&gt;The fix was one line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;identityToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Joi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote about the full debugging process, the wrong assumptions I made, and what I learned about reviewing AI-generated code.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://medium.com/@devjunmin/i-debugged-xcode-for-hours-the-bug-was-in-my-joi-schema-b698cdf82a6f" rel="noopener noreferrer"&gt;Read the full post on Medium&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>flutter</category>
      <category>dart</category>
      <category>node</category>
    </item>
    <item>
      <title>Claude Code for Solo Developers — Prompt Engineering That Keeps Things Clean</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Sun, 12 Apr 2026 19:45:35 +0000</pubDate>
      <link>https://dev.to/devsnake/claude-code-for-solo-developers-prompt-engineering-that-keeps-things-clean-34kd</link>
      <guid>https://dev.to/devsnake/claude-code-for-solo-developers-prompt-engineering-that-keeps-things-clean-34kd</guid>
      <description>&lt;p&gt;I'm building an iOS reading app by myself. The stack is Flutter, Express/TypeScript, and Supabase. I used Claude Code for most of the heavy lifting — Apple Sign In, account deletion, API client abstraction, design system, card sharing, and localization.&lt;/p&gt;

&lt;p&gt;What I found out pretty quickly is that how you write the prompt changes everything. This is a record of what worked, what didn't, and the calls I made along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  I wrote prompts with Claude, not for Claude
&lt;/h2&gt;

&lt;p&gt;I didn't start with perfect prompts. I'd explain what I needed, Claude would draft a prompt, and then I'd add my own rules or split the task up before running it.&lt;/p&gt;

&lt;p&gt;Error handling is a good example. Claude's first draft looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add consistent error handling across all API call sites.
Wrap every API call in try-catch.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what I added on top:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scan the entire codebase — not just repositories.
- Catch ApiException and rethrow as-is
- Catch all other exceptions and wrap in ApiException(statusCode: 0, message: e.toString())
Maintain existing feature-first clean architecture.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I knew that "all repository methods" would miss files like &lt;code&gt;auth_repository&lt;/code&gt;. So I made it explicit. Claude drafts, I refine, then we run it.&lt;/p&gt;




&lt;h2&gt;
  
  
  1 — Always declare the scope
&lt;/h2&gt;

&lt;p&gt;This was the most important thing I did. Every prompt had a clear list of what to touch and what to leave alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## Constraints
- Only modify setting_page.dart
- Do not modify auth or book features
- Maintain existing feature-first clean architecture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of this, I had zero unintended file changes across the whole project. Claude never touched something I didn't ask it to. Every result stayed exactly in the lane I set.&lt;/p&gt;




&lt;h2&gt;
  
  
  2 — Give architecture context in one line
&lt;/h2&gt;

&lt;p&gt;Early on I wrote "Keep Riverpod pattern." Too narrow. What I actually needed was the whole structure to stay intact.&lt;/p&gt;

&lt;p&gt;So I switched to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Maintain existing feature-first clean architecture.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Covered Riverpod, directory structure, and file responsibilities all at once. Anthropic's own docs say to think of Claude like a smart new hire with limited context — give enough to work with, but don't over-explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  3 — Split the work
&lt;/h2&gt;

&lt;p&gt;When you throw too much at once, instructions can conflict or things get dropped. Anthropic's docs recommend giving instructions as sequential steps.&lt;/p&gt;

&lt;p&gt;For the design system, I split it like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Round 1 — Extract design constants:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract design constants into a centralized theme system.
Inside lib/app/:
- app_colors.dart
- app_text_styles.dart
- Update existing theme.dart
Replace all hardcoded color/font values.
Do not change any UI layout or behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Round 2 — Navigation and Settings:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Simplify navigation and complete Settings page.
Reduce bottom tabs to 2: Books and Settings.
Complete Settings page with user info and logout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Splitting made it easy to verify each result and track down issues if something went wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  4 — Product decisions are mine to make
&lt;/h2&gt;

&lt;p&gt;Claude writes good code. It doesn't make good product decisions.&lt;/p&gt;

&lt;p&gt;When I needed to store a language setting, Claude suggested a new table and middleware layer. But there was already a &lt;code&gt;language_code&lt;/code&gt; column in the DB. I just used that.&lt;/p&gt;

&lt;p&gt;Claude tends to over-engineer for flexibility. In solo development, simplicity wins. That call is mine to make.&lt;/p&gt;

&lt;p&gt;Same thing with the sentence delete feature. I cut it from the MVP. A bad sentence can just stop being the representative one — and having sentences pile up is actually part of the app's value.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I didn't know
&lt;/h2&gt;

&lt;p&gt;After the project, I read through Anthropic's official prompt engineering guide. A few things stood out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XML tags&lt;/strong&gt; — Wrapping sections in &lt;code&gt;&amp;lt;instructions&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;constraints&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;context&amp;gt;&lt;/code&gt; tags helps Claude parse the prompt more cleanly. I used markdown headers (&lt;code&gt;##&lt;/code&gt;) the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Few-shot examples&lt;/strong&gt; — Including 3–5 examples alongside instructions can significantly improve output quality. I never did this. For consistency-heavy work like design systems, it probably would've helped.&lt;/p&gt;

&lt;p&gt;There's also stuff in the docs about parallel tool calls and agent behavior control. Didn't use any of it. Worth looking into for the next project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;Scope declaration, architecture context, splitting the work — those three things kept everything clean. No unintended changes, no broken structure, results that stayed exactly where I put them.&lt;/p&gt;

&lt;p&gt;Prompting is just communication. The clearer you are about what you want, what's off limits, and what the constraints are — the better the output. But what to build, how to break it down, how simple to keep it — that's still on you.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>claude</category>
      <category>promptengineering</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>I Threw Away My Keyword Strategy Right Before Launch — Here's What I Did Instead</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Sun, 12 Apr 2026 15:30:12 +0000</pubDate>
      <link>https://dev.to/devsnake/i-threw-away-my-keyword-strategy-right-before-launch-heres-what-i-did-instead-c2b</link>
      <guid>https://dev.to/devsnake/i-threw-away-my-keyword-strategy-right-before-launch-heres-what-i-did-instead-c2b</guid>
      <description>&lt;h2&gt;
  
  
  Some context
&lt;/h2&gt;

&lt;p&gt;I had already built and shipped a vocabulary learning app on both iOS and Android before. It was pretty basic — nothing fancy in terms of design. But it kept getting steady organic downloads without any marketing at all. The revenue wasn't life-changing, but watching users show up on their own was honestly motivating.&lt;/p&gt;

&lt;p&gt;I wanted that same feeling with my second app. I wasn't chasing big money. I just wanted to build something where users would keep coming in, little by little. And I figured the best way to do that was to focus on one sharp feature instead of building an app that tries to do everything. I wanted to nail one thing and see how people react.&lt;/p&gt;

&lt;p&gt;So I came up with the idea for a reading app — you save sentences you like from books, turn them into nice-looking cards, and share them. I built it with Flutter, and the MVP was almost done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I went back to check the keywords
&lt;/h2&gt;

&lt;p&gt;During the early planning phase, I used AI to do some market research. Back then, it suggested a certain keyword as my main target — something along the lines of "reading ****s." The competition looked manageable at the time.&lt;/p&gt;

&lt;p&gt;But right before launch, I asked the same AI again. And it gave me a different answer. Maybe the market had changed, or maybe it was just looking at things from a different angle. Either way, I decided I couldn't just go with the old plan. Time to start over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I was really looking for
&lt;/h2&gt;

&lt;p&gt;It wasn't just about finding keywords with low competition. I needed to check three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is this a niche that actually exists?&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is there proven demand?&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Can a new app actually get in?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I needed all three to be true at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  I started searching myself
&lt;/h2&gt;

&lt;p&gt;I thought about using paid ASO tools, but decided to just search the app stores directly first. I grabbed my iPhone, typed in keywords one by one, and took screenshots of every result.&lt;/p&gt;

&lt;p&gt;The keyword ideas came up naturally while chatting with AI. I'd check what showed up in autocomplete, look at how competitor apps wrote their subtitles, and we'd go back and forth narrowing things down.&lt;/p&gt;

&lt;h3&gt;
  
  
  First search: my original target
&lt;/h3&gt;

&lt;p&gt;I searched for the keyword I had picked during planning. Turns out my earlier judgment was wrong. The top app had 18,000 reviews and was ranked #19 on the charts. Four more apps below it had reviews in the thousands. App names were even showing up in autocomplete — that's how packed it was.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second search: similar keyword
&lt;/h3&gt;

&lt;p&gt;Tried a variation. Same apps showed up again. The top two apps combined had over 20,000 reviews. Pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third search: a feature-based keyword
&lt;/h3&gt;

&lt;p&gt;This time I searched for something that directly describes what my app does. Things looked different here. The top app had around 360 reviews. That told me two things — the demand is real, and the top spot isn't locked down by a giant. From the second result on, most apps had fewer than 30 reviews.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fourth search: the turning point
&lt;/h3&gt;

&lt;p&gt;Then I tried one more keyword, and the results were completely different.&lt;/p&gt;

&lt;p&gt;The top app had 17,000 reviews. So clearly, people search for this term a lot. But here's the thing — that app wasn't even in the "Books" category. It was in a totally different one. Same for the second result. There wasn't a single Books-category app ranking for this keyword.&lt;/p&gt;

&lt;p&gt;So the search volume was there, but the category was wide open. Honestly, I wasn't sure if this was a real opportunity or not — I'd only know after launching. Whether the people searching actually want what my app offers, whether they'd actually install it — that's something only real data can answer. But it definitely felt worth trying.&lt;/p&gt;

&lt;h3&gt;
  
  
  More searches: finding more gaps
&lt;/h3&gt;

&lt;p&gt;I searched for a few more keywords in a similar style. Either no relevant apps showed up, or the ones that did had 1 to 3 reviews. Tiny.&lt;/p&gt;

&lt;h3&gt;
  
  
  What autocomplete told me
&lt;/h3&gt;

&lt;p&gt;I also noticed something useful while typing. When I entered a certain word, autocomplete suggested several related keyword combinations. That's a pretty clear sign that real people are actually searching for those terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checked Android too
&lt;/h2&gt;

&lt;p&gt;I didn't stop at iOS. I ran the same searches on Google Play. There were a few more competing apps on Android, but the overall pattern was the same — demand exists, but there's no great solution yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned from competitor reviews
&lt;/h2&gt;

&lt;p&gt;I downloaded the most direct competitor and tried it out. Read through the reviews too.&lt;/p&gt;

&lt;p&gt;Users loved that it was "lightweight," "fast," and "easy to use." That confirmed the demand is real. I also saw feature requests and complaints, but I didn't adjust my app's direction based on those. I just took note and moved on. All I needed to confirm was two things: the market exists, and existing apps aren't perfect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;After taking around 20 screenshots and going through everything, my keyword strategy changed completely. I dropped all the keywords I had picked during planning and rebuilt around the ones I found through this process.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The app is almost ready to ship. Right now I'm thinking about how to make the store screenshots stand out, and how to put together marketing content that actually grabs attention.&lt;/p&gt;




</description>
      <category>seo</category>
      <category>aso</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>Flutter Apple Sign In Error 1000: The Fix No One Talks About</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Fri, 10 Apr 2026 15:50:27 +0000</pubDate>
      <link>https://dev.to/devsnake/flutter-apple-sign-in-error-1000-the-fix-no-one-talks-about-3nld</link>
      <guid>https://dev.to/devsnake/flutter-apple-sign-in-error-1000-the-fix-no-one-talks-about-3nld</guid>
      <description>&lt;p&gt;I'm building an iOS reading app solo. The idea is simple — save sentences that stop you while reading, turn them into beautiful cards, and share them. The stack is Flutter, Express/TypeScript, and Supabase. Authentication is handled with Apple Sign In.&lt;/p&gt;

&lt;p&gt;Apple requires apps to support account deletion, or they'll reject your app during review. So I built it. And with Apple Sign In, deleting an account isn't just about wiping the user from your database. You have to revoke the Apple &lt;code&gt;refresh_token&lt;/code&gt; too. That's how Apple officially disconnects the user from your app.&lt;/p&gt;

&lt;p&gt;I got it working. Revoke call on the server, soft delete in the database. Everything looked fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Then It Broke
&lt;/h2&gt;

&lt;p&gt;After testing account deletion, I tried signing in again. The moment I tapped the login button, it crashed. No Apple authentication sheet. Nothing. Just an error.&lt;/p&gt;

&lt;p&gt;SignInWithAppleAuthorizationException(&lt;br&gt;
AuthorizationErrorCode.unknown,&lt;br&gt;
The operation couldn't be completed.&lt;br&gt;
(com.apple.AuthenticationServices.AuthorizationError error 1000.)&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;My first thought was a bad request to Apple, or maybe a network issue. I deleted the app, reinstalled it. Same error.&lt;/p&gt;


&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;I was using the &lt;code&gt;sign_in_with_apple&lt;/code&gt; Flutter package. To figure out exactly where it was failing, I added logs at every step inside &lt;code&gt;signInWithApple()&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="n"&gt;debugPrint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'🍎 [1] signInWithApple started'&lt;/span&gt;&lt;span class="p"&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;'🍎 [2] before appleLogin call'&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appleLogin&lt;/span&gt;&lt;span class="p"&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;'🍎 [3] appleLogin complete'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output was this:&lt;br&gt;
🍎 [1] signInWithApple started&lt;br&gt;
🍎 [2] before appleLogin call&lt;br&gt;
🍎 [ERROR] SignInWithAppleAuthorizationException(...)&lt;/p&gt;

&lt;p&gt;It crashed right after &lt;code&gt;[2]&lt;/code&gt;. The package's &lt;code&gt;getAppleIDCredential()&lt;/code&gt; method was failing instantly. No request ever reached the server. The Apple authentication sheet never even had a chance to appear.&lt;/p&gt;

&lt;p&gt;This wasn't a server issue. It wasn't a Flutter logic issue. Something was wrong at a much lower level.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Cause
&lt;/h2&gt;

&lt;p&gt;After spending a little bit time on this with claude, I Googled it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sign in with Apple was missing from Xcode Capabilities.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At some point during a &lt;code&gt;pod install&lt;/code&gt; or a Flutter dependency update, the Xcode project settings got reset and the capability disappeared. Without it, Apple Authentication Services simply doesn't work. It fails immediately and returns &lt;code&gt;error 1000&lt;/code&gt;. The system blocks it before anything else runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Xcode → Select Runner target → Signing &amp;amp; Capabilities → &lt;code&gt;+&lt;/code&gt; → Add Sign in with Apple&lt;/p&gt;

&lt;p&gt;That was it. After adding it back, the Apple authentication sheet appeared immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  One More Thing
&lt;/h2&gt;

&lt;p&gt;I asked AI about this while debugging. Every single answer it gave was wrong. It pointed me toward token revocation policies, network issues, things that had nothing to do with the actual problem. I solved it by Googling myself.&lt;/p&gt;

&lt;p&gt;AI is genuinely useful for a lot of things in development. But it doesn't always get it right. When you're stuck, don't forget that Google is still there.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>googleing</category>
      <category>dart</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Why Android Camera Permission Stops Showing After First Denial — A Flutter Deep Dive</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Thu, 09 Apr 2026 14:49:38 +0000</pubDate>
      <link>https://dev.to/devsnake/why-android-camera-permission-stops-showing-after-first-denial-a-flutter-deep-dive-a3c</link>
      <guid>https://dev.to/devsnake/why-android-camera-permission-stops-showing-after-first-denial-a-flutter-deep-dive-a3c</guid>
      <description>&lt;p&gt;About a year ago at my previous company, I got a task from my boss that seemed straightforward at first.&lt;/p&gt;

&lt;p&gt;Our Flutter app had a structure where all permissions were requested at once during app initialization — handled inside a single service class that ran on startup. The request was simple: if a user denies camera permission at that point, make sure the permission popup shows up again when they navigate to a screen that actually needs the camera.&lt;/p&gt;

&lt;p&gt;So I started working on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;On a Samsung Galaxy device, the behavior was not what I expected.&lt;/p&gt;

&lt;p&gt;First request on app launch → popup shows up fine.&lt;br&gt;&lt;br&gt;
User denies it → navigates to the camera screen → tries to request again → nothing happens.&lt;br&gt;&lt;br&gt;
Flutter just returns &lt;code&gt;"requested"&lt;/code&gt; with no popup.&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;channel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'fida_app/camera_permission'&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'requestPermission'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s"&gt;'permission'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'android.permission.CAMERA'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// second call → result: "requested", no popup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  I Thought It Was a Code Problem
&lt;/h2&gt;

&lt;p&gt;My first instinct was to look at the native side. I checked &lt;code&gt;PermissionHandler.kt&lt;/code&gt; and thought maybe the &lt;code&gt;onRequestPermissionsResult&lt;/code&gt; callback wasn't properly connected to &lt;code&gt;MainActivity&lt;/code&gt;. So I tried wiring it up.&lt;/p&gt;

&lt;p&gt;That's when something unexpected happened. The app started freezing on the splash screen.&lt;/p&gt;

&lt;p&gt;I couldn't pinpoint the exact cause, but my guess — and I later looked into this through ChatGPT — was that ad SDKs like &lt;code&gt;AudienceNetworkAds&lt;/code&gt; and &lt;code&gt;AdPopcornSSP&lt;/code&gt; were being initialized synchronously inside &lt;code&gt;configureFlutterEngine()&lt;/code&gt;, blocking the main thread before Flutter could reach &lt;code&gt;runApp()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The general fix for this kind of issue is to wrap those SDK calls in &lt;code&gt;Handler.post { }&lt;/code&gt;, so they run after the Flutter engine has already attached.&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="c1"&gt;// before — blocking the main thread&lt;/span&gt;
&lt;span class="nc"&gt;AudienceNetworkAds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;AdPopcornSSP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// after — deferred asynchronously&lt;/span&gt;
&lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Looper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMainLooper&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;AudienceNetworkAds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nc"&gt;AdPopcornSSP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;appKey&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;I rolled back the permission code changes and the splash freeze went away. I had gone in to fix one thing and accidentally touched something else entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Actual Root Cause
&lt;/h2&gt;

&lt;p&gt;I stepped back and thought about it more carefully.&lt;/p&gt;

&lt;p&gt;What made this confusing was that the behavior wasn't consistent. Sometimes the popup showed up, sometimes it didn't — with no clear pattern. So naturally I assumed this was something I could control through code. That assumption was wrong.&lt;/p&gt;

&lt;p&gt;I dug into this further with ChatGPT to understand the OS-level behavior, and the answer was simpler than I expected.&lt;/p&gt;

&lt;p&gt;Android stores permission state at the OS level.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If granted&lt;/strong&gt; → stored as &lt;code&gt;GRANTED&lt;/code&gt;. Calling &lt;code&gt;requestPermissions()&lt;/code&gt; again does nothing. The system just passes through silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If denied&lt;/strong&gt; → stored as "explicitly denied". Standard Android policy allows another popup on the next request. But on &lt;strong&gt;Samsung One UI (Android 13~15)&lt;/strong&gt;, even a single denial can internally flip to &lt;code&gt;Don't ask again&lt;/code&gt; state — skipping the popup on the very next call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What looked like inconsistent behavior was actually Samsung's OEM customization policy sitting on top of standard Android rules. It wasn't something I could fix through code — the OS was making the call.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Android Permission Behavior Changed Across Versions
&lt;/h2&gt;

&lt;p&gt;While looking into this, I also checked how permission behavior evolved across Android API levels — again with the help of ChatGPT to get the details right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API 22 and below (Android 5.x)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
No runtime permissions. Everything was granted at install time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API 23~29 (Android 6.0~10)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Runtime permissions introduced. On denial, &lt;code&gt;shouldShowRequestPermissionRationale()&lt;/code&gt; returns &lt;code&gt;true&lt;/code&gt; and the next request shows the popup again. The popup only gets permanently blocked if the user explicitly checks "Don't ask again" before denying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API 30~32 (Android 11~12L)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
One-time permissions added for camera, mic, and location. Choosing "Only this time" means the permission gets revoked when the app goes to background or closes — so the popup appears again on next entry. &lt;code&gt;auto-reset&lt;/code&gt; was also introduced here: the OS automatically revokes permissions for apps that haven't been used in a while.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;API 33+ (Android 13~)&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Same behavior as above. Notification permission is now a separate runtime request (&lt;code&gt;POST_NOTIFICATIONS&lt;/code&gt;). Camera permission logic stays the same.&lt;/p&gt;

&lt;p&gt;Our app was on &lt;code&gt;targetSdk = 35&lt;/code&gt;, &lt;code&gt;minSdk = 23&lt;/code&gt; — so all of these policies applied, including Samsung's OEM behavior on top.&lt;/p&gt;


&lt;h2&gt;
  
  
  iOS Is Simpler, But Stricter
&lt;/h2&gt;

&lt;p&gt;iOS has one clear rule: deny once, and that's it.&lt;/p&gt;

&lt;p&gt;After a user denies, &lt;code&gt;AVCaptureDevice.requestAccess(for: .video)&lt;/code&gt; always returns &lt;code&gt;false&lt;/code&gt;. The system will never show the popup again no matter how many times you call it.&lt;/p&gt;

&lt;p&gt;The only option is to open the settings screen directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openSettingsURLString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I didn't know this at first and kept calling &lt;code&gt;requestPermission&lt;/code&gt; from Flutter expecting something to happen. Nothing did.&lt;/p&gt;

&lt;p&gt;To open the iOS settings screen from Flutter, you need to register a native channel in &lt;code&gt;AppDelegate.swift&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;settingsChannel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FlutterMethodChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"fida_app/open_settings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;binaryMessenger&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;binaryMessenger&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;settingsChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setMethodCallHandler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"openAppSettings"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;openSettingsURLString&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
       &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;canOpenURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[:],&lt;/span&gt; &lt;span class="nv"&gt;completionHandler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;FlutterMethodNotImplemented&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;h2&gt;
  
  
  So I Changed the Approach
&lt;/h2&gt;

&lt;p&gt;What I originally expected:&lt;/p&gt;

&lt;p&gt;Enter camera screen → call requestPermission → popup shows&lt;/p&gt;

&lt;p&gt;What I actually built:&lt;/p&gt;

&lt;p&gt;Enter camera screen&lt;br&gt;
→ request permission&lt;br&gt;
→ granted → proceed&lt;br&gt;
→ denied&lt;br&gt;
→ show explanation modal&lt;br&gt;
→ try requesting again&lt;br&gt;
→ still denied → open settings directly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Android flow:&lt;/strong&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;static&lt;/span&gt; &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ensureCameraPermission&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="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="s"&gt;'requestPermission'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'permission'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'android.permission.CAMERA'&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;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'granted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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;showDialog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;context:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AlertModal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;message:&lt;/span&gt; &lt;span class="s"&gt;'Camera permission is required to use this feature.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;onConfirm:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="s"&gt;'requestPermission'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'permission'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'android.permission.CAMERA'&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;result&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;'granted'&lt;/span&gt;&lt;span class="p"&gt;)&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;_settingsChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'openAppSettings'&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;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'granted'&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;&lt;strong&gt;iOS — go straight to settings on denial:&lt;/strong&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_iosChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;'requestPermission'&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;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;'granted'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;_settingsChannel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'openAppSettings'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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 original ask was simple — show the permission popup again when users enter a camera screen after denying at launch. Turns out that’s not something you can freely control through code. The OS decides, and each platform has its own rules.&lt;/p&gt;

&lt;p&gt;Understanding how the permission system actually works at the OS level was more useful than any code fix I could have written.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>android</category>
      <category>ios</category>
      <category>permissions</category>
    </item>
    <item>
      <title>Provider to Riverpod AsyncNotifier: A Real Migration with Before &amp; After Code</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Mon, 06 Apr 2026 20:54:21 +0000</pubDate>
      <link>https://dev.to/devsnake/provider-to-riverpod-asyncnotifier-a-real-migration-with-before-after-code-ff5</link>
      <guid>https://dev.to/devsnake/provider-to-riverpod-asyncnotifier-a-real-migration-with-before-after-code-ff5</guid>
      <description>&lt;p&gt;Recently, I made a decision to change the state management library in my personal app. For a long time, I used Provider and Bloc packages for state management. GetX as well.&lt;/p&gt;

&lt;p&gt;In medium-scale apps, I prefer using Provider with the MVVM pattern. But I realized that currently in the Flutter community, most developers use Riverpod. I thought that Provider was going to become a legacy skill. Riverpod provides more convenience than Provider, in terms of dependency injection and overall usability.&lt;/p&gt;

&lt;p&gt;Riverpod doesn't rely heavily on context and the widget tree, and it allows easier testing.&lt;/p&gt;




&lt;h2&gt;
  
  
  My App's Architecture
&lt;/h2&gt;

&lt;p&gt;Previously, my app's architecture followed Feature-First and Clean Architecture partially. The reason I designed it that way was that this was a private app and I didn't want to create a lot of boilerplate. But I did want to make this app maintainable and scalable. So even if I see this code again in the future, I can easily read it.&lt;/p&gt;

&lt;p&gt;On the project root, there was a feature folder, and it included individual feature folders. Each feature had its own data, domain, and presentation folders. When a new feature was added, it would be placed in the feature folder, and it would have its own data, domain, and presentation folders as well.&lt;/p&gt;

&lt;p&gt;When I changed Provider to Riverpod, my app's architecture didn't require many changes. My app didn't have many features, so I could easily refactor it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;I just changed the spot where dependencies were injected, and changed ChangeNotifier to AsyncNotifier. The domain layer and data layer were not affected at all. I just changed some Provider functions to Riverpod methods.&lt;/p&gt;

&lt;p&gt;When I used the Provider package, I followed the MVVM pattern, and after the migration, it didn't change. Because the architecture separated concerns properly, the migration only affected the presentation layer.&lt;/p&gt;

&lt;p&gt;I haven't added an abstract layer or use cases yet, because it's enough to add them when needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technical Decisions
&lt;/h2&gt;

&lt;p&gt;During this process, I made some technical decisions. First of all, as far as I know, there are a few coding approaches when using Riverpod, such as StateNotifier, Notifier/AsyncNotifier, and codegen.&lt;/p&gt;

&lt;p&gt;In my project, every API call was async, and the codegen approach would make this project over-engineered. So I thought AsyncNotifier was the best choice for my case.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration with Claude Code
&lt;/h2&gt;

&lt;p&gt;I didn't want to migrate file by file, so I decided to use Claude Code for the migration. I focused on making sure Claude Code didn't touch the overall structure and approached the project carefully. I wanted to easily read the migrated code after the migration.&lt;/p&gt;

&lt;p&gt;Here is the prompt I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Migrate the entire Flutter project from Provider to Riverpod (AsyncNotifier, no codegen).

## Scope
- Remove `provider` package, add `flutter_riverpod`
- Convert all ChangeNotifier classes to AsyncNotifier
- Declare Repositories as global Riverpod Providers
- Replace MultiProvider in main.dart with ProviderScope
- Convert all UI files using Provider API (Consumer, context.read, context.watch) to ConsumerWidget + ref
- Preserve all existing behavior: auto-login, country selection branching, book search/add/list

## Migration comments
Add brief inline comments at key migration points for learning reference.
Format: `// Migration: [what changed and why]`
Keep comments concise — one line per migration point, only at meaningful changes.

## Rules
- Do NOT modify Repository classes (data layer stays untouched)
- Do NOT modify Entity/model classes
- Do NOT add new features or refactor business logic
- Keep the same file/folder structure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Before &amp;amp; After
&lt;/h2&gt;

&lt;h3&gt;
  
  
  DI — main.dart
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before (Provider):&lt;/strong&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="c1"&gt;// main.dart&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;MultiProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;providers:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;ChangeNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuthRepository&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ChangeNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;create:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BookProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BookRepository&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="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&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;&lt;strong&gt;After (Riverpod):&lt;/strong&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="c1"&gt;// main.dart&lt;/span&gt;
&lt;span class="n"&gt;runApp&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;ProviderScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;MyApp&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="c1"&gt;// di.dart — Repositories declared as global providers&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;authRepositoryProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&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;AuthRepository&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;bookRepositoryProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ref&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;BookRepository&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MultiProvider and constructor injection were replaced by ProviderScope at the root and global provider declarations. Dependencies are now resolved through &lt;code&gt;ref&lt;/code&gt; instead of being passed through constructors.&lt;/p&gt;




&lt;h3&gt;
  
  
  State Management — book_provider.dart
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before (Provider):&lt;/strong&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;class&lt;/span&gt; &lt;span class="nc"&gt;BookProvider&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ChangeNotifier&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;BookRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;BookProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fetchBooks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;error&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="n"&gt;notifyListeners&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBooks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;notifyListeners&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;&lt;strong&gt;After (Riverpod):&lt;/strong&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;class&lt;/span&gt; &lt;span class="nc"&gt;BookNotifier&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;AsyncNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBooks&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;addBook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt; &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookRepositoryProvider&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addBook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invalidateSelf&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;bookNotifierProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;AsyncNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BookNotifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Book&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;BookNotifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;new&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;code&gt;isLoading&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, and &lt;code&gt;notifyListeners()&lt;/code&gt; boilerplate disappeared entirely. &lt;code&gt;AsyncValue&lt;/code&gt; handles loading, error, and data states automatically. The &lt;code&gt;build()&lt;/code&gt; method replaces the manual &lt;code&gt;fetchBooks()&lt;/code&gt; that was called in &lt;code&gt;initState&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  UI — books_page.dart
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Before (Provider):&lt;/strong&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;class&lt;/span&gt; &lt;span class="nc"&gt;BooksPage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BooksPage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&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;_BooksPageState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_BooksPageState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BooksPage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;WidgetsBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addPostFrameCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&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;read&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BookProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetchBooks&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="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Consumer&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BookProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&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;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&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="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (Riverpod):&lt;/strong&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;class&lt;/span&gt; &lt;span class="nc"&gt;BooksPage&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ConsumerWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&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="n"&gt;WidgetRef&lt;/span&gt; &lt;span class="n"&gt;ref&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;asyncBooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bookNotifierProvider&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;asyncBooks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;loading:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CircularProgressIndicator&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="nl"&gt;data:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;books&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;ListView&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&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;StatefulWidget with initState became a simple ConsumerWidget. The &lt;code&gt;ref.watch&lt;/code&gt; automatically triggers the initial fetch and rebuilds the UI when data changes. The &lt;code&gt;asyncBooks.when()&lt;/code&gt; pattern replaces the manual if/else chain for loading, error, and data states.&lt;/p&gt;




&lt;p&gt;As a next step, I'm planning to add an abstract layer and use cases. My app has two ways to add a book — manual input and search via API — so abstracting this part makes sense. But before that, I want to refine the product direction and find a sharper niche first.&lt;/p&gt;

</description>
      <category>mobile</category>
      <category>riverpod</category>
      <category>flutter</category>
      <category>dart</category>
    </item>
    <item>
      <title>"This AdWidget is already in the Widget tree" — It Only Crashed After One Specific Flow</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Thu, 02 Apr 2026 20:01:56 +0000</pubDate>
      <link>https://dev.to/devsnake/this-adwidget-is-already-in-the-widget-tree-it-only-crashed-after-one-specific-flow-54mg</link>
      <guid>https://dev.to/devsnake/this-adwidget-is-already-in-the-widget-tree-it-only-crashed-after-one-specific-flow-54mg</guid>
      <description>&lt;h2&gt;
  
  
  The Error
&lt;/h2&gt;

&lt;p&gt;I was working on a Flutter app that uses &lt;code&gt;google_mobile_ads&lt;/code&gt; for banner ads. One day, I got this error on the MyPage screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This AdWidget is already in the Widget tree

Make sure you are not using the same ad object in more than one AdWidget.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error message was clear. But the weird thing was — &lt;strong&gt;it didn't always happen&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the Pattern
&lt;/h2&gt;

&lt;p&gt;I tested different scenarios and found a pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Switch tabs normally → no crash&lt;/li&gt;
&lt;li&gt;Go to another page and come back → no crash&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update address → go back to main page → open MyPage tab → crash&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the question became: what's different about the address update flow?&lt;/p&gt;

&lt;h2&gt;
  
  
  What Was Different
&lt;/h2&gt;

&lt;p&gt;The app uses &lt;code&gt;BottomNavigationBar&lt;/code&gt; with &lt;code&gt;IndexedStack&lt;/code&gt;. After the address update, the code navigated back to the main page like this:&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;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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="n"&gt;MainPage&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;This looks normal. But &lt;code&gt;Navigator.push&lt;/code&gt; &lt;strong&gt;adds a new page on top of the stack&lt;/strong&gt;. It does not remove the old one. So the old MainPage — and the old MyPage inside it — was still alive in memory.&lt;/p&gt;

&lt;p&gt;When I opened MyPage on the new MainPage, the old MyPage's &lt;code&gt;State&lt;/code&gt; was still holding a &lt;code&gt;BannerAd&lt;/code&gt; with its &lt;code&gt;AdWidget&lt;/code&gt; mounted. Now there were two &lt;code&gt;AdWidget&lt;/code&gt;s trying to use the same ad resources at the same time. That's why it crashed.&lt;/p&gt;

&lt;p&gt;In normal tab switching, this never happened because there was only one MainPage and one MyPage &lt;code&gt;State&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;I changed &lt;code&gt;Navigator.push&lt;/code&gt; to &lt;code&gt;Navigator.pushAndRemoveUntil&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="n"&gt;Navigator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pushAndRemoveUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MaterialPageRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;builder:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;MainPage&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;route&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="kc"&gt;false&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;This clears the entire navigation stack. The old MainPage and its MyPage get properly disposed. When the user opens MyPage again, it's a completely new instance — no duplicate &lt;code&gt;AdWidget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After this change, the crash never happened again.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Also Removed &lt;code&gt;didUpdateWidget&lt;/code&gt; for Ads
&lt;/h2&gt;

&lt;p&gt;The original code had this:&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="nd"&gt;@override&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;didUpdateWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;covariant&lt;/span&gt; &lt;span class="n"&gt;MyPage&lt;/span&gt; &lt;span class="n"&gt;oldWidget&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;didUpdateWidget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oldWidget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;_bannerAd&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="na"&gt;dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;_bannerAd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AdHelper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createBannerAd&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;This recreated the ad every time the parent widget rebuilt. But if the old &lt;code&gt;AdWidget&lt;/code&gt; was still in the tree, disposing and recreating the ad caused the same duplication problem. I removed this entirely. Banner ads should only be created in &lt;code&gt;initState&lt;/code&gt; and cleaned up in &lt;code&gt;dispose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;google_mobile_ads&lt;/code&gt; plugin internally tracks whether each ad is already mounted in the widget tree. In the source code (&lt;code&gt;ad_containers.dart&lt;/code&gt;), &lt;code&gt;_AdWidgetState.initState&lt;/code&gt; checks the ad's ID through &lt;code&gt;instanceManager.isWidgetAdIdMounted()&lt;/code&gt;. If the ad is already mounted somewhere, the &lt;code&gt;build&lt;/code&gt; method throws the "already in the Widget tree" error. The ad ID is only released when &lt;code&gt;_AdWidgetState.dispose&lt;/code&gt; calls &lt;code&gt;unmountWidgetAdId&lt;/code&gt;. This means one &lt;code&gt;BannerAd&lt;/code&gt; can only be used by one &lt;code&gt;AdWidget&lt;/code&gt; at a time — the plugin enforces this at the code level.&lt;/p&gt;

&lt;p&gt;The real lesson was about navigation. &lt;code&gt;Navigator.push&lt;/code&gt; keeps the old page alive. If that page owns native resources like ads, those resources stay alive too — and the old &lt;code&gt;AdWidget&lt;/code&gt; never gets disposed, so the ad ID is never released. When you need to "reset" to the main page, use &lt;code&gt;pushAndRemoveUntil&lt;/code&gt; to make sure the old state is fully disposed.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>admob</category>
      <category>adwidget</category>
    </item>
    <item>
      <title>Flutter iOS: Did Not Find a Dart VM Service — Fixed by Recreating the Xcode Runner Scheme</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Sun, 29 Mar 2026 15:37:57 +0000</pubDate>
      <link>https://dev.to/devsnake/flutter-ios-did-not-find-a-dart-vm-service-fixed-by-recreating-the-xcode-runner-scheme-189d</link>
      <guid>https://dev.to/devsnake/flutter-ios-did-not-find-a-dart-vm-service-fixed-by-recreating-the-xcode-runner-scheme-189d</guid>
      <description>&lt;h2&gt;
  
  
  The Dart VM Service was not discovered after 60 seconds. This is taking much longer than expected... Open the Xcode window the project is opened in to ensure the app is running.
&lt;/h2&gt;

&lt;p&gt;If the app is not running, try selecting "Product &amp;gt; Run" to fix the problem.&lt;/p&gt;

&lt;p&gt;blah blah..&lt;/p&gt;

&lt;p&gt;Error: Did not find a Dart VM Service advertised for com.app.myproject.&lt;/p&gt;

&lt;p&gt;I got stuck on this error while building an iOS app in Android Studio. The strange thing was that the app built just fine in Xcode.&lt;br&gt;
At first, I suspected it might be related to iOS local network permissions. &lt;/p&gt;

&lt;p&gt;"Did not find a Dart VM Service" usually means the Flutter framework failed to connect over the local network. So I checked whether my Info.plist included the NSLocalNetworkUsageDescription key. &lt;/p&gt;

&lt;p&gt;But the key was already included in my Info.plist file.&lt;/p&gt;

&lt;p&gt;I decided to reinstall the app to retrigger the permission prompt. I deleted the app from my iPhone.&lt;/p&gt;

&lt;p&gt;And I tried resetting the trust settings on my iPhone. &lt;br&gt;
I went to Settings &amp;gt; General &amp;gt; Transfer or Reset iPhone &amp;gt; Reset &amp;gt; Reset Location &amp;amp; Privacy, then reconnected via USB to trigger the "Trust This Computer?" prompt again.&lt;/p&gt;

&lt;p&gt;After tapping Trust, I launched the app — it came to the foreground, and the local network permission popup appeared. I allowed it.&lt;br&gt;
But nothing happened. No connection. Flutter was still silent.&lt;/p&gt;

&lt;p&gt;I can't remember every single thing I tried at that time. I tried many things to get it building on Android Studio.&lt;/p&gt;

&lt;p&gt;At some point, I realized — I had switched from an iPhone 8 to an iPhone 12 right before this issue started. Both were my debugging devices, and I hadn't thought much about it at the time. That felt like the real clue.&lt;/p&gt;

&lt;p&gt;I shared this suspicion with AI, and it confirmed that this is actually a common issue. When you change the target debug device, Xcode's internal project configuration can get corrupted — the Runner Scheme ends up still tied to the old device's settings.&lt;/p&gt;

&lt;p&gt;So I went to Product &amp;gt; Scheme &amp;gt; Manage Schemes in Xcode, deleted the existing Runner scheme, and created a new one.&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.amazonaws.com%2Fuploads%2Farticles%2Fasf6ge4a8h3aotlsj0xb.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.amazonaws.com%2Fuploads%2Farticles%2Fasf6ge4a8h3aotlsj0xb.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, the app finally built successfully on Android Studio — and kept working after restarts.&lt;/p&gt;

</description>
      <category>dart</category>
      <category>mobile</category>
      <category>ios</category>
      <category>flutter</category>
    </item>
    <item>
      <title>Before Writing Code, I Write the Spec Myself</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Mon, 23 Mar 2026 15:21:22 +0000</pubDate>
      <link>https://dev.to/devsnake/before-writing-code-i-write-the-spec-myself-5b2b</link>
      <guid>https://dev.to/devsnake/before-writing-code-i-write-the-spec-myself-5b2b</guid>
      <description>&lt;p&gt;Before you start coding, you usually need to clearly define what needs to be built. In other words, you take unclear requirements and turn them into actual features based on what you hear from your client, senior developer, or designer.&lt;/p&gt;

&lt;p&gt;I started my career in a small company, and in many small companies, the development process is not well systemized. (I'm in Korea.)&lt;br&gt;
In my case, I often didn't receive proper feature specification documents, and sometimes there were no API documents at all.&lt;/p&gt;

&lt;p&gt;Because of that, I had to take responsibility for clarifying the requirements myself. I would summarize the feature requirements, write them down in my own words, and then go back to confirm whether my understanding was correct. &lt;/p&gt;

&lt;p&gt;This process helped me avoid misunderstandings and reduce unnecessary rework during development.&lt;/p&gt;

&lt;p&gt;Regardless of company size, clear documentation is essential. When it's properly shared and used across the team, it helps reduce unnecessary communication and meetings.&lt;/p&gt;




&lt;p&gt;Recently, I did some additional work for a previous company. &lt;/p&gt;

&lt;p&gt;The requirements were mainly about user verification use cases.&lt;br&gt;
My boss was not a developer and didn't have much knowledge of programming. Because of that, the requirements were often quite messy and not clearly defined, so I had to put effort into organizing and structuring them myself.&lt;/p&gt;

&lt;p&gt;Our app had multiple user statuses, such as guest, blocked user, and pending user. However, communication between the backend and frontend was not very clear, which made things more complicated.&lt;/p&gt;

&lt;p&gt;To solve this, I broke down the user verification flow into clearly defined statuses and documented them. For example:&lt;/p&gt;

&lt;p&gt;*** Non-member**&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;guest: active &amp;amp;&amp;amp; none (no authentication)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;*** Member **&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;user: pending &amp;amp;&amp;amp; not_submitted (just signed up)&lt;br&gt;
user: pending &amp;amp;&amp;amp; submitted (documents submitted to admin)&lt;br&gt;
user: active &amp;amp;&amp;amp; approved (approved by admin)&lt;br&gt;
user: pending &amp;amp;&amp;amp; rejected (rejected by admin)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To make sure my understanding was correct, I created a Confluence document based on this.&lt;/p&gt;

&lt;p&gt;It worked as a shared reference for everyone involved - my boss could verify that each use case matched what he had in mind, and the backend developer could confirm whether the status values he was returning were correct.&lt;/p&gt;

&lt;p&gt;Another thing I liked was that whenever requirements changed or something new was added, we could just update that one document. No more back-and-forth messages trying to remember what we agreed on.&lt;/p&gt;

&lt;p&gt;One doc, three people, way less confusion.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>productivity</category>
      <category>documentation</category>
      <category>codenewbie</category>
    </item>
    <item>
      <title>Flutter App Stuck on Splash Screen: A dev_dependencies Mistake</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Thu, 19 Mar 2026 12:34:45 +0000</pubDate>
      <link>https://dev.to/devsnake/flutter-app-stuck-on-splash-screen-a-devdependencies-mistake-3o71</link>
      <guid>https://dev.to/devsnake/flutter-app-stuck-on-splash-screen-a-devdependencies-mistake-3o71</guid>
      <description>&lt;p&gt;Recently, i added some features to my side project flutter app.&lt;br&gt;
The app is available on both platforms: Google Play Store and App Store.&lt;/p&gt;

&lt;p&gt;If I release to both platforms at once and some bugs occur, it would be difficult to handle.&lt;br&gt;
So I decided to release my new build to the Google Play Store first.&lt;br&gt;
After the new build was released on the Google Play Store, I installed the app.&lt;br&gt;
But something went wrong.&lt;/p&gt;

&lt;p&gt;I couldn't access the main page. The app was stuck on the splash screen.&lt;/p&gt;

&lt;p&gt;At that moment, something came to my mind.&lt;br&gt;
I had just resolved an issue related to the sign_button package.&lt;br&gt;
I was using the sign_button package on the login page, but the Apple login button didn't render correctly.&lt;/p&gt;

&lt;p&gt;So I replaced it with the sign_in_button package and added it to dependencies in the pubspec.yaml file.&lt;br&gt;
I started to think that I might have messed up the pubspec.yaml structure while doing this.&lt;br&gt;
I might have placed the package in the wrong section.&lt;/p&gt;

&lt;p&gt;It should have been in dependencies, but I accidentally added it to dev_dependencies.&lt;br&gt;
I checked my pubspec.yaml again and realized that I had placed the package in dev_dependencies.&lt;br&gt;
So I modified it and submitted a new build again. After that, the problem was solved.&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.amazonaws.com%2Fuploads%2Farticles%2Fzxhyah4p3vn5s87y15c0.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.amazonaws.com%2Fuploads%2Farticles%2Fzxhyah4p3vn5s87y15c0.png" alt=" " width="800" height="796"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If Flutter packages are placed in the dev_dependencies section, the app can still build while running in the debug environment.&lt;br&gt;
But if they have runtime dependencies, they should be placed in the dependencies section. &lt;/p&gt;

&lt;p&gt;Otherwise, crash problems can happen.&lt;br&gt;
Actually, most Flutter packages have runtime dependencies, except for a few packages,&lt;br&gt;
such as flutter_test, flutter_lints, and flutter_launcher_icons.&lt;br&gt;
These have nothing to do with running the app.&lt;/p&gt;

&lt;p&gt;Google Play Store reviewers are not that strict, i think.&lt;br&gt;
I assume they didn't test my new build in the release environment.&lt;br&gt;
Anyway, I fixed the packages in pubspec.yaml properly.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>softwareengineering</category>
      <category>dart</category>
    </item>
    <item>
      <title>What My Failed Apps Taught Me About Building Products</title>
      <dc:creator>koreanDev</dc:creator>
      <pubDate>Wed, 18 Mar 2026 11:19:06 +0000</pubDate>
      <link>https://dev.to/devsnake/what-my-failed-apps-taught-me-about-building-products-5fm9</link>
      <guid>https://dev.to/devsnake/what-my-failed-apps-taught-me-about-building-products-5fm9</guid>
      <description>&lt;p&gt;I have released a few apps on the app stores.&lt;/p&gt;

&lt;p&gt;Most of them were small-scale applications.&lt;/p&gt;

&lt;p&gt;They were not built for learning purposes.&lt;/p&gt;

&lt;p&gt;These apps had fewer than five pages&lt;/p&gt;

&lt;p&gt;and only one or two features.&lt;/p&gt;

&lt;p&gt;Pop-up advertisements were shown to users on almost every page.&lt;/p&gt;

&lt;p&gt;That was okay.&lt;/p&gt;

&lt;p&gt;I did build these apps mainly for ad revenue.&lt;/p&gt;

&lt;p&gt;But there was a problem.&lt;/p&gt;

&lt;p&gt;Most of them had very low retention.&lt;/p&gt;

&lt;p&gt;They didn’t really make money,&lt;/p&gt;

&lt;p&gt;and they didn’t give me meaningful experience either.&lt;/p&gt;

&lt;p&gt;That said, not all of my apps were built casually.&lt;/p&gt;

&lt;p&gt;I did put real effort into a fortune-telling app&lt;/p&gt;

&lt;p&gt;and an emotion tracking app.&lt;/p&gt;

&lt;p&gt;They were more complete technically,&lt;/p&gt;

&lt;p&gt;but the planning and features were driven mostly by my assumptions,&lt;/p&gt;

&lt;p&gt;not by actual user needs.&lt;/p&gt;

&lt;p&gt;In that sense, they also failed.&lt;/p&gt;

&lt;p&gt;Interestingly, a very simple Korean language learning app&lt;/p&gt;

&lt;p&gt;that I built with much less effort&lt;/p&gt;

&lt;p&gt;is still generating around three to four dollars per day.&lt;/p&gt;

&lt;p&gt;At some point, I felt that even if I made no ad revenue at all,&lt;/p&gt;

&lt;p&gt;I just wanted the experience of running an app&lt;/p&gt;

&lt;p&gt;where users actually accumulate over time.&lt;/p&gt;

&lt;p&gt;And I needed a Flutter portfolio.&lt;/p&gt;

&lt;p&gt;My Git repository was full of low-quality apps.&lt;/p&gt;

&lt;p&gt;I didn’t really have anything I could call a portfolio,&lt;/p&gt;

&lt;p&gt;nothing that clearly showed my experience or skills,&lt;/p&gt;

&lt;p&gt;even though I had been developing for almost five years.&lt;/p&gt;

&lt;p&gt;Long story short,&lt;/p&gt;

&lt;p&gt;these days I decided to start another app.&lt;/p&gt;

&lt;p&gt;This time, I’m really focusing on planning.&lt;/p&gt;

&lt;p&gt;I’m comparing similar apps&lt;/p&gt;

&lt;p&gt;and spending a lot of time researching&lt;/p&gt;

&lt;p&gt;what kind of app makes sense to build,&lt;/p&gt;

&lt;p&gt;based on niche markets and real user needs.&lt;/p&gt;

&lt;p&gt;At the beginning, I considered making a running app&lt;/p&gt;

&lt;p&gt;that helps people with running.&lt;/p&gt;

&lt;p&gt;But I dropped that idea at the planning stage.&lt;/p&gt;

&lt;p&gt;I didn’t want to compete with big applications&lt;/p&gt;

&lt;p&gt;like Nike Run Club.&lt;/p&gt;

&lt;p&gt;Beyond that,&lt;/p&gt;

&lt;p&gt;there are already too many apps&lt;/p&gt;

&lt;p&gt;that simply measure distance.&lt;/p&gt;

&lt;p&gt;So now I’m about to make an app related to book reading.&lt;/p&gt;

&lt;p&gt;I joined a book reading group on a social media app,&lt;/p&gt;

&lt;p&gt;and I’m quite active in that group.&lt;/p&gt;

&lt;p&gt;Because of that, I know there are many people&lt;/p&gt;

&lt;p&gt;who are interested in growing their reading habits&lt;/p&gt;

&lt;p&gt;or recording book phrases that impressed them.&lt;/p&gt;

&lt;p&gt;Actually, I’m rarely coding right now.&lt;/p&gt;

&lt;p&gt;I’m mostly forming the directory structure&lt;/p&gt;

&lt;p&gt;and considering which libraries would fit this project.&lt;/p&gt;

&lt;p&gt;As much as possible, I want to choose effective tools,&lt;/p&gt;

&lt;p&gt;considering maintainability and scalability.&lt;/p&gt;

&lt;p&gt;I don’t want to overengineer it,&lt;br&gt;
but I do want to build a well-structured project.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>admob</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
