<?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>Forem: Gautier 💙</title>
    <description>The latest articles on Forem by Gautier 💙 (@gautier).</description>
    <link>https://forem.com/gautier</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%2F508997%2Fa6cc950e-fd7d-478e-bb5d-29bf52103f6e.png</url>
      <title>Forem: Gautier 💙</title>
      <link>https://forem.com/gautier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gautier"/>
    <language>en</language>
    <item>
      <title>Send Meta Ad Events from Flutter with AEM Support</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Wed, 15 Apr 2026 06:19:22 +0000</pubDate>
      <link>https://forem.com/gautier/send-meta-ad-events-from-flutter-with-aem-support-2eli</link>
      <guid>https://forem.com/gautier/send-meta-ad-events-from-flutter-with-aem-support-2eli</guid>
      <description>&lt;p&gt;If you run ads for a mobile app, iOS is where the money is. &lt;br&gt;
iOS users spend 2 to 3 times more than Android users on in-app purchases and subscriptions. &lt;br&gt;
They convert better, retain longer, and have higher lifetime value. In most markets, the top revenue apps make 60-70% of their income from iOS — even when Android has more downloads. &lt;/p&gt;

&lt;p&gt;If you are spending money on Meta ads, iOS users are the ones you want to reach.&lt;br&gt;
And that is exactly where the problem starts.&lt;/p&gt;

&lt;p&gt;I spent tons of money on Meta ads last year.&lt;/p&gt;

&lt;p&gt;A big chunk of that was wasted.&lt;br&gt;
Not because the ads were bad, but because Facebook never saw the conversions.&lt;br&gt;
My campaigns could not optimize because the conversions events were not getting through.&lt;/p&gt;

&lt;p&gt;If you are running Meta ads for a Flutter app on iOS, you have probably seen this: your app is making sales, but Events Manager shows almost nothing.&lt;br&gt;
Your ROAS looks terrible. Your campaigns stay stuck in learning phase.&lt;/p&gt;

&lt;p&gt;The problem is not your ads (or it might be). The problem is your event pipeline.&lt;/p&gt;


&lt;h2&gt;
  
  
  The iOS 14+ problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;When Apple introduced App Tracking Transparency (ATT), it changed everything for mobile advertisers. Users can now opt out of tracking — and most of them do. On average, only 20-30% of iOS users opt in.&lt;/p&gt;

&lt;p&gt;That means 70-80% of your conversions are invisible to Meta.&lt;/p&gt;

&lt;p&gt;Meta's answer to this is &lt;strong&gt;AEM — Aggregated Event Measurement&lt;/strong&gt;. It is a protocol that lets Meta receive conversion data in a privacy-compliant way, even from users who opted out of tracking. Without AEM, Meta literally cannot see most of your iOS purchases.&lt;/p&gt;

&lt;p&gt;Here is what happens without AEM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use Apple SkanNetwork for attribution, which only reports app installs and a few post-install events with heavy limitations&lt;/li&gt;
&lt;li&gt;You spend $100 on ads&lt;/li&gt;
&lt;li&gt;10 users buy your app&lt;/li&gt;
&lt;li&gt;Meta sees 2 or 3 of those purchases (the ones who opted in but with 24-48 hours delay)&lt;/li&gt;
&lt;li&gt;Meta thinks your ads are performing terribly&lt;/li&gt;
&lt;li&gt;The algorithm stops showing your ads to good prospects&lt;/li&gt;
&lt;li&gt;You waste more money&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With AEM:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You spend $100 on ads&lt;/li&gt;
&lt;li&gt;10 users buy your app&lt;/li&gt;
&lt;li&gt;Meta sees 2-3 purchases directly from opted-in users (same as before but only 30 mn to 24 hours delay)&lt;/li&gt;
&lt;li&gt;For the other 7-8, AEM provides &lt;strong&gt;aggregated, delayed signals&lt;/strong&gt; — not full conversion records, but enough for Meta's algorithm to &lt;strong&gt;estimate&lt;/strong&gt; total conversions&lt;/li&gt;
&lt;li&gt;Meta uses statistical modeling on top of these signals to better optimize your campaigns&lt;/li&gt;
&lt;li&gt;Your campaigns improve over time — not perfectly, but dramatically better than flying blind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be clear: AEM does not magically bypass ATT. It does not send individual conversion data for users who opted out. What it does is give Meta enough aggregated signal to work with. There are real limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are limited to &lt;strong&gt;8 prioritized conversion events&lt;/strong&gt; per app — only the highest-priority event per user gets reported&lt;/li&gt;
&lt;li&gt;Data from opted-out users is &lt;strong&gt;delayed 24-72 hours&lt;/strong&gt;, not real-time&lt;/li&gt;
&lt;li&gt;Meta fills gaps with &lt;strong&gt;modeling and estimation&lt;/strong&gt;, not actual tracked data&lt;/li&gt;
&lt;li&gt;It works alongside Apple's &lt;strong&gt;SKAdNetwork&lt;/strong&gt; postbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bottom line: without AEM, Meta sees almost nothing. With AEM, Meta gets enough signal to actually optimize. It is not perfect, but it is the difference between campaigns that learn and campaigns that burn money.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is not optional if you are serious about running ads.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Why I built facebook_flutter_sdk
&lt;/h2&gt;

&lt;p&gt;The existing &lt;a href="https://pub.dev/packages/flutter_facebook_app_events" rel="noopener noreferrer"&gt;&lt;code&gt;flutter_facebook_app_events&lt;/code&gt;&lt;/a&gt; plugin is solid and works well for basic event tracking. But it does not support AEM.&lt;/p&gt;

&lt;p&gt;I needed AEM for my own campaigns. I submitted a PR to add it, but the maintainer had different priorities — totally fair, it is their project. So I forked it and published &lt;a href="https://pub.dev/packages/facebook_flutter_sdk" rel="noopener noreferrer"&gt;&lt;code&gt;facebook_flutter_sdk&lt;/code&gt;&lt;/a&gt; with AEM built in.&lt;/p&gt;

&lt;p&gt;To be clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you &lt;strong&gt;don't run Meta ads&lt;/strong&gt; or don't need AEM, &lt;code&gt;flutter_facebook_app_events&lt;/code&gt; works great&lt;/li&gt;
&lt;li&gt;If you &lt;strong&gt;run Meta ads and need accurate iOS conversion tracking&lt;/strong&gt;, &lt;code&gt;facebook_flutter_sdk&lt;/code&gt; fills that gap&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The API is compatible. Switching is a one-line change in your pubspec.&lt;/p&gt;


&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Install the package
&lt;/h3&gt;


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

&lt;/div&gt;

&lt;h3&gt;
  
  
  2. Get your Facebook credentials
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="https://developers.facebook.com/" rel="noopener noreferrer"&gt;Meta for Developers&lt;/a&gt;, open your app, and grab:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;App ID&lt;/strong&gt; — found in Settings &amp;gt; Basic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client Token&lt;/strong&gt; — found in Settings &amp;gt; Advanced&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. Android configuration
&lt;/h3&gt;

&lt;p&gt;Add your credentials to &lt;code&gt;android/app/src/main/res/values/strings.xml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="utf-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;resources&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"facebook_app_id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;YOUR_APP_ID&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"facebook_client_token"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;YOUR_CLIENT_TOKEN&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/resources&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then reference them in your &lt;code&gt;AndroidManifest.xml&lt;/code&gt; inside the &lt;code&gt;&amp;lt;application&amp;gt;&lt;/code&gt; tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta-data&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"com.facebook.sdk.ApplicationId"&lt;/span&gt;
    &lt;span class="na"&gt;android:value=&lt;/span&gt;&lt;span class="s"&gt;"@string/facebook_app_id"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta-data&lt;/span&gt;
    &lt;span class="na"&gt;android:name=&lt;/span&gt;&lt;span class="s"&gt;"com.facebook.sdk.ClientToken"&lt;/span&gt;
    &lt;span class="na"&gt;android:value=&lt;/span&gt;&lt;span class="s"&gt;"@string/facebook_client_token"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. iOS configuration
&lt;/h3&gt;

&lt;p&gt;Update your &lt;code&gt;ios/Runner/Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;FacebookAppID&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;YOUR_APP_ID&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;FacebookClientToken&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;YOUR_CLIENT_TOKEN&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;FacebookDisplayName&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;Your App Name&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;CFBundleURLTypes&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;CFBundleURLSchemes&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;fbYOUR_APP_ID&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;fb&lt;/code&gt; prefix before your App ID in the URL scheme. This is required by the Facebook SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Request ATT permission (iOS)
&lt;/h3&gt;

&lt;p&gt;You need to ask users for tracking permission before sending events. You probably already use &lt;code&gt;permission_handler&lt;/code&gt; in your app — it supports ATT out of the box:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Add the usage description to your &lt;code&gt;Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSUserTrackingUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This identifier will be used to deliver personalized ads to you.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then request permission early in your app — typically after onboarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:permission_handler/permission_handler.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:facebook_flutter_sdk/facebook_flutter_sdk.dart'&lt;/span&gt;&lt;span class="o"&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;requestTracking&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;status&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;Permission&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;appTrackingTransparency&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;request&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;facebookAppEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FacebookAppEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Tell Facebook SDK whether the user opted in&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setAdvertiserTracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;enabled:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isGranted&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;
  
  
  Sending events
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Initialize the SDK
&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;final&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FacebookAppEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Log custom events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;'level_completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;parameters:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'level'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'score'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'1200'&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;h3&gt;
  
  
  Log purchases
&lt;/h3&gt;

&lt;p&gt;This is the most important event for ad optimization. &lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;eventId&lt;/code&gt; parameter for deduplication:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logPurchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;amount:&lt;/span&gt; &lt;span class="mf"&gt;9.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;eventId:&lt;/span&gt; &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// unique ID for deduplication&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;eventId&lt;/code&gt; is critical. If you also send purchase events server-side via the Conversions API (you should), Meta uses this ID to deduplicate and avoid counting the same purchase twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Log add-to-cart events
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logAddToCart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;id:&lt;/span&gt; &lt;span class="s"&gt;'premium_yearly'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;type:&lt;/span&gt; &lt;span class="s"&gt;'product'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;price:&lt;/span&gt; &lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;currency:&lt;/span&gt; &lt;span class="s"&gt;'USD'&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;h3&gt;
  
  
  Set user data for advanced matching
&lt;/h3&gt;

&lt;p&gt;Advanced matching improves attribution accuracy by letting Meta match events to users across devices:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUserData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;email:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;firstName:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;lastName:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;country:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;country&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;Hash the data yourself if you prefer — Meta also accepts pre-hashed values.&lt;/p&gt;




&lt;h2&gt;
  
  
  RevenueCat integration
&lt;/h2&gt;

&lt;p&gt;If you use RevenueCat for subscriptions, here is how to log purchases to Meta:&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="p"&gt;...&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;facebookAppEvents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FacebookAppEvents&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;purchaseParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PurchaseParams&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;package&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;res&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;Purchases&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;purchaseParams&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;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purchaserInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entitlements&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&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;transactionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;storeTransaction&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionIdentifier&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;logMetaPurchase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;storeProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;storeProduct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;transactionIdentifier&lt;/code&gt; from RevenueCat is perfect as an &lt;code&gt;eventId&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you also send this purchase server-side via RevenueCat's webhook to your backend (which then calls the Conversions API), use the same transaction ID. &lt;br&gt;
Meta will deduplicate automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Event deduplication with Conversions API
&lt;/h2&gt;

&lt;p&gt;Client-side events alone are not enough. Some events get lost due to network issues, app crashes, or ad blockers. That is why Meta recommends sending events from both client and server.&lt;/p&gt;

&lt;p&gt;The flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client-side&lt;/strong&gt;: your Flutter app sends the event via &lt;code&gt;facebook_flutter_sdk&lt;/code&gt; with an &lt;code&gt;eventId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-side&lt;/strong&gt;: your backend sends the same event via the &lt;a href="https://developers.facebook.com/docs/marketing-api/conversions-api/" rel="noopener noreferrer"&gt;Conversions API&lt;/a&gt; with the same &lt;code&gt;eventId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meta deduplicates&lt;/strong&gt;: if both events arrive, Meta keeps one and discards the duplicate&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This gives you the best of both worlds — real-time client events plus reliable server events as a fallback.&lt;/p&gt;

&lt;p&gt;If you use RevenueCat, you can set up a webhook from RevenueCat to your server, then forward purchase events to the Conversions API with the same transaction ID.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why you probably don't need an MMP
&lt;/h2&gt;

&lt;p&gt;When I was trying to figure out why my events were not working, every article told the same thing: use an MMP.&lt;/p&gt;

&lt;p&gt;MMPs like Adjust, AppsFlyer, or Branch are tools that sit between your app and ad networks. &lt;br&gt;
They handle attribution, event forwarding, and reporting.&lt;br&gt;
They are powerful. They are also expensive — $500 to $2,000+ per month after 10k monthly active users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But they are not magic, and most of the. time they are really hard to set up correctly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the thing: &lt;strong&gt;if Meta is your primary ad channel, you don't need one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;facebook_flutter_sdk&lt;/code&gt; + Conversions API combo gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accurate conversion tracking with AEM&lt;/li&gt;
&lt;li&gt;Event deduplication&lt;/li&gt;
&lt;li&gt;Advanced matching&lt;/li&gt;
&lt;li&gt;Full control over your data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MMPs make sense when you run campaigns across 5+ ad networks simultaneously and need unified attribution. &lt;br&gt;
For most indie developers and small teams running Meta ads, direct integration is enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Not requesting ATT early enough
&lt;/h3&gt;

&lt;p&gt;If you request ATT after the user has already triggered events, those early events are sent without tracking authorization. Request it during onboarding, before any purchase flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forgetting the eventId on purchases
&lt;/h3&gt;

&lt;p&gt;Without &lt;code&gt;eventId&lt;/code&gt;, Meta cannot deduplicate client and server events. You either miss events or double-count them. Both destroy your campaign optimization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Volume of Advertiser Tracking Enabled parameter out of range
&lt;/h3&gt;

&lt;p&gt;Many sees this error in their facebook events manager. The error is not really understandable. But this is what you get when plugin is not properly implementing AEM protocol. &lt;br&gt;
When you get this error you can't start a campaign to optimize for purchases. You can only optimize for install or user SKAdNetwork events. That is a problem because purchase optimization is where the money is.&lt;br&gt;
So if you see this error, switch to &lt;code&gt;facebook_flutter_sdk&lt;/code&gt; and it should fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Go further with Meta Ads
&lt;/h2&gt;

&lt;p&gt;Setting up events is step one. &lt;br&gt;
But creating campaigns that actually convert — choosing the right objective, structuring ad sets, estimating budgets, analyzing results — that is a whole other skill.&lt;/p&gt;

&lt;p&gt;I put everything I learned from spending $$$ on Meta ads into a &lt;a href="https://dev.to/flutter-meta-ads-course/"&gt;complete guide for Flutter developers&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;It covers setup, campaign strategy, budget estimation, and how to avoid the mistakes that cost me thousands.&lt;/p&gt;

&lt;p&gt;If you are serious about growing your app with ads, &lt;a href="https://dev.to/flutter-meta-ads-course/"&gt;check it out&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop flying blind
&lt;/h2&gt;

&lt;p&gt;Every day you run Meta ads without AEM, you are paying for data that never reaches the algorithm. Your campaigns cannot optimize. Your ROAS stays flat. You burn money.&lt;/p&gt;

&lt;p&gt;The fix is straightforward: install &lt;a href="https://pub.dev/packages/facebook_flutter_sdk" rel="noopener noreferrer"&gt;&lt;code&gt;facebook_flutter_sdk&lt;/code&gt;&lt;/a&gt;, set up your events with proper deduplication, and let Meta actually see your conversions.&lt;/p&gt;

&lt;p&gt;One package. A few lines of code. Campaigns that finally learn.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>flutter</category>
      <category>ads</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Automate Your Flutter iOS Deployment with Fastlane</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Tue, 14 Apr 2026 08:03:18 +0000</pubDate>
      <link>https://forem.com/gautier/automate-your-flutter-ios-deployment-with-fastlane-kab</link>
      <guid>https://forem.com/gautier/automate-your-flutter-ios-deployment-with-fastlane-kab</guid>
      <description>&lt;p&gt;If you have ever shipped a Flutter app to the App Store, you know the pain.&lt;/p&gt;

&lt;p&gt;Open Xcode. Wait for indexing. Archive. Wait again. Upload. Fill in release notes manually for every locale. Click through 15 screens. Hope nothing breaks.&lt;/p&gt;

&lt;p&gt;Now imagine doing that every week. Or every time you fix a critical bug.&lt;/p&gt;

&lt;p&gt;There is a better way. &lt;strong&gt;&lt;a href="https://docs.fastlane.tools/" rel="noopener noreferrer"&gt;Fastlane&lt;/a&gt;&lt;/strong&gt; lets you automate the entire iOS deployment process with a single terminal command. No Xcode GUI, no manual uploads, no copy-pasting release notes.&lt;/p&gt;

&lt;p&gt;In this guide, I will walk you through a real Fastlane setup that I use to deploy my own Flutter apps. Not a toy example. A production config that handles API authentication, localized metadata, and App Store uploads.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Fastlane for Flutter iOS deployment
&lt;/h2&gt;

&lt;p&gt;Flutter builds your app. Fastlane ships it.&lt;/p&gt;

&lt;p&gt;Flutter's &lt;code&gt;flutter build ipa&lt;/code&gt; gives you a binary. But getting that binary to the App Store with the right metadata, release notes, and screenshots is a whole separate job. That is where Fastlane comes in.&lt;/p&gt;

&lt;p&gt;Here is what Fastlane handles for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt; with App Store Connect (no 2FA prompts)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata management&lt;/strong&gt; — descriptions, keywords, release notes for every locale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary uploads&lt;/strong&gt; — push your IPA directly to App Store Connect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version creation&lt;/strong&gt; — create new app versions programmatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submission&lt;/strong&gt; — optionally submit for review automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All from one command in your terminal.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start, make sure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Flutter app&lt;/strong&gt; that builds successfully with &lt;code&gt;flutter build ipa&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An Apple Developer account&lt;/strong&gt; with App Store Connect access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt; installed (macOS comes with it, or use &lt;code&gt;rbenv&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fastlane&lt;/strong&gt; installed:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;fastlane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then initialize Fastlane in your Flutter project's &lt;code&gt;ios/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ios
fastlane init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose option 4 (manual setup) when prompted. This creates the &lt;code&gt;ios/fastlane/&lt;/code&gt; directory with a &lt;code&gt;Fastfile&lt;/code&gt; and &lt;code&gt;Appfile&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Set up App Store Connect API key
&lt;/h2&gt;

&lt;p&gt;The first thing you want to do is stop using your Apple ID to authenticate. Apple's 2FA will block your automation every time.&lt;/p&gt;

&lt;p&gt;Instead, create an &lt;strong&gt;App Store Connect API key&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://appstoreconnect.apple.com/" rel="noopener noreferrer"&gt;App Store Connect&lt;/a&gt; &amp;gt; Users and Access &amp;gt; Integrations &amp;gt; App Store Connect API&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;+&lt;/strong&gt; button to generate a new key&lt;/li&gt;
&lt;li&gt;Give it a name like "Fastlane CI" and select the &lt;strong&gt;App Manager&lt;/strong&gt; role&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;.p8&lt;/code&gt; key file — you can only download it once&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now store these values as environment variables. Add them to your &lt;code&gt;.zshrc&lt;/code&gt;, &lt;code&gt;.bashrc&lt;/code&gt;, or CI secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FASTLANE_APP_STORE_CONNECT_API_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_KEY_ID"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"YOUR_ISSUER_ID"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;FASTLANE_APP_STORE_CONNECT_API_KEY_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/AuthKey.p8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;Fastfile&lt;/code&gt;, create a helper function that loads this key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_api_key&lt;/span&gt;
  &lt;span class="n"&gt;app_store_connect_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;key_id: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_KEY_ID"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;issuer_id: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;key_filepath: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_KEY_PATH"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;in_house: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every lane calls &lt;code&gt;load_api_key&lt;/code&gt; first. No passwords, no 2FA, no session tokens that expire.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Organize your metadata
&lt;/h2&gt;

&lt;p&gt;Fastlane uses a folder structure to manage your App Store metadata. Each locale gets its own folder with text files for the different fields.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fastlane/metadata/
  en-US/
    release_notes.txt
    description.txt
    keywords.txt
  fr-FR/
    release_notes.txt
    description.txt
  de-DE/
    release_notes.txt
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can bootstrap this structure by pulling your existing metadata from App Store Connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Download metadata from App Store Connect"&lt;/span&gt;
&lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:download_app_metadata&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;load_api_key&lt;/span&gt;
  &lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;download_metadata: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;download_screenshots: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&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;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane download_app_metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads all your current descriptions, keywords, and release notes into the &lt;code&gt;metadata/&lt;/code&gt; folder. &lt;/p&gt;

&lt;p&gt;Now you can edit them locally and push updates without touching App Store Connect.&lt;/p&gt;

&lt;p&gt;(I tend to split screenshots and metadata into separate lanes, so I can update text without worrying about images, and vice versa.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling localized release notes
&lt;/h3&gt;

&lt;p&gt;If you support multiple languages, you need release notes for each locale. Here is a helper that loads them all, with a fallback to English:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_release_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;metadata_path&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'en-US/release_notes.txt'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="n"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[ar-SA ca cs da de-DE el en-AU en-CA en-GB en-US
               es-ES es-MX fi fr-CA fr-FR he hi hr hu id it ja
               ko ms nl-NL no pl pt-BR pt-PT ro ru sk sv th tr
               uk vi zh-Hans zh-Hant]&lt;/span&gt;
  &lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_object&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'release_notes.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important. If you forget a locale, Apple will reject your submission or show stale release notes. This helper makes sure every supported locale has content.&lt;br&gt;
You can remove any locale you don't support, but keep the fallback to avoid submission failures.&lt;/p&gt;

&lt;p&gt;Note:&lt;br&gt;
You can create a translations script to automatically translate your release notes from English to all your other languages.&lt;/p&gt;
&lt;h3&gt;
  
  
  All Apple App Store locales by priority
&lt;/h3&gt;

&lt;p&gt;Not all locales are equal. Some cover massive markets with high App Store spending. Others are nice-to-have. Here is a prioritization to help you decide what to translate first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — Cover these first.&lt;/strong&gt; These locales represent the largest App Store markets by revenue and downloads. Skipping any of them means leaving money on the table.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Locale&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;en-US&lt;/td&gt;
&lt;td&gt;English (US)&lt;/td&gt;
&lt;td&gt;Largest App Store market by revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;en-GB&lt;/td&gt;
&lt;td&gt;English (UK)&lt;/td&gt;
&lt;td&gt;Second-largest English-speaking market (optionnal as it's covered by en-US)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ja&lt;/td&gt;
&lt;td&gt;Japanese&lt;/td&gt;
&lt;td&gt;Japan is the #2 App Store market worldwide&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zh-Hans&lt;/td&gt;
&lt;td&gt;Chinese Simplified&lt;/td&gt;
&lt;td&gt;Massive user base, high spending on in-app purchases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ko&lt;/td&gt;
&lt;td&gt;Korean&lt;/td&gt;
&lt;td&gt;South Korea has one of the highest ARPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;de-DE&lt;/td&gt;
&lt;td&gt;German&lt;/td&gt;
&lt;td&gt;Germany is the top European market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fr-FR&lt;/td&gt;
&lt;td&gt;French (France)&lt;/td&gt;
&lt;td&gt;Major European market and 2nd language impact on surrounding countries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;es-ES&lt;/td&gt;
&lt;td&gt;Spanish (Spain)&lt;/td&gt;
&lt;td&gt;Gateway to all Spanish-speaking countries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pt-BR&lt;/td&gt;
&lt;td&gt;Portuguese (Brazil)&lt;/td&gt;
&lt;td&gt;Largest Latin American market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;en-AU&lt;/td&gt;
&lt;td&gt;English (Australia)&lt;/td&gt;
&lt;td&gt;High ARPU English-speaking market&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — Add these next.&lt;/strong&gt; Strong markets that will move the needle once Tier 1 is covered.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Locale&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;zh-Hant&lt;/td&gt;
&lt;td&gt;Chinese Traditional&lt;/td&gt;
&lt;td&gt;Covers Taiwan and Hong Kong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;it&lt;/td&gt;
&lt;td&gt;Italian&lt;/td&gt;
&lt;td&gt;Large European economy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nl-NL&lt;/td&gt;
&lt;td&gt;Dutch&lt;/td&gt;
&lt;td&gt;High smartphone penetration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sv&lt;/td&gt;
&lt;td&gt;Swedish&lt;/td&gt;
&lt;td&gt;High ARPU Nordic market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;da&lt;/td&gt;
&lt;td&gt;Danish&lt;/td&gt;
&lt;td&gt;High ARPU Nordic market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;Norwegian&lt;/td&gt;
&lt;td&gt;High ARPU Nordic market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tr&lt;/td&gt;
&lt;td&gt;Turkish&lt;/td&gt;
&lt;td&gt;Large and growing mobile market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pl&lt;/td&gt;
&lt;td&gt;Polish&lt;/td&gt;
&lt;td&gt;Biggest Central European market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ar-SA&lt;/td&gt;
&lt;td&gt;Arabic (Saudi Arabia)&lt;/td&gt;
&lt;td&gt;High spending Gulf market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;he&lt;/td&gt;
&lt;td&gt;Hebrew&lt;/td&gt;
&lt;td&gt;Israel is a strong tech market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;en-CA&lt;/td&gt;
&lt;td&gt;English (Canada)&lt;/td&gt;
&lt;td&gt;High ARPU North American market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fr-CA&lt;/td&gt;
&lt;td&gt;French (Canada)&lt;/td&gt;
&lt;td&gt;Required for Quebec App Store visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;es-MX&lt;/td&gt;
&lt;td&gt;Spanish (Mexico)&lt;/td&gt;
&lt;td&gt;Second-largest Latin American market&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pt-PT&lt;/td&gt;
&lt;td&gt;Portuguese (Portugal)&lt;/td&gt;
&lt;td&gt;Completes Portuguese coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — Nice to have.&lt;/strong&gt; Smaller markets or languages where English metadata often performs just as well. Add them when you have the bandwidth.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Locale&lt;/th&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ca&lt;/td&gt;
&lt;td&gt;Catalan&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cs&lt;/td&gt;
&lt;td&gt;Czech&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ru&lt;/td&gt;
&lt;td&gt;Russian&lt;/td&gt;
&lt;td&gt;Large user base but currently can't pay for apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;el&lt;/td&gt;
&lt;td&gt;Greek&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fi&lt;/td&gt;
&lt;td&gt;Finnish&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hi&lt;/td&gt;
&lt;td&gt;Hindi&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hr&lt;/td&gt;
&lt;td&gt;Croatian&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hu&lt;/td&gt;
&lt;td&gt;Hungarian&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;id&lt;/td&gt;
&lt;td&gt;Indonesian&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ms&lt;/td&gt;
&lt;td&gt;Malay&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ro&lt;/td&gt;
&lt;td&gt;Romanian&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sk&lt;/td&gt;
&lt;td&gt;Slovak&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;th&lt;/td&gt;
&lt;td&gt;Thai&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;uk&lt;/td&gt;
&lt;td&gt;Ukrainian&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vi&lt;/td&gt;
&lt;td&gt;Vietnamese&lt;/td&gt;
&lt;td&gt;can be used to impact US market&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A practical approach: start with Tier 1 using quality translations, cover Tier 2 with machine translation reviewed by a native speaker, and use English fallbacks for Tier 3 until your app gains traction in those markets.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — Create the "release new version" lane
&lt;/h2&gt;

&lt;p&gt;This lane creates a new app version on App Store Connect and pushes your metadata — without uploading a binary. This is useful when you want to update release notes, descriptions, or keywords independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Create a new version and push metadata"&lt;/span&gt;
&lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:release_new_version&lt;/span&gt; &lt;span class="k"&gt;do&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="n"&gt;load_api_key&lt;/span&gt;

  &lt;span class="n"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;app_identifier: &lt;/span&gt;&lt;span class="s2"&gt;"com.yourcompany.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;app_version: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;release_notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_release_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./metadata'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;app_version: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="ss"&gt;skip_binary_upload: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;submit_for_review: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;automatic_release: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;metadata_path: &lt;/span&gt;&lt;span class="s2"&gt;"./fastlane/metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;release_notes: &lt;/span&gt;&lt;span class="n"&gt;release_notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;precheck_include_in_app_purchases: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;skip_screenshots: &lt;/span&gt;&lt;span class="kp"&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;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane release_new_version version:&lt;span class="s2"&gt;"1.2.0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command. New version created. Metadata pushed. Release notes in 20+ languages. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Create the deploy lane
&lt;/h2&gt;

&lt;p&gt;This is the main lane. It takes your Flutter IPA, uploads it to App Store Connect, and optionally submits for review.&lt;/p&gt;

&lt;p&gt;First, build your Flutter app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter build ipa &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates an IPA file in &lt;code&gt;build/ios/ipa/&lt;/code&gt;. Now the deploy lane picks it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="s2"&gt;"Build and deploy app to App Store Connect"&lt;/span&gt;
&lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:deploy&lt;/span&gt; &lt;span class="k"&gt;do&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="n"&gt;load_api_key&lt;/span&gt;
  &lt;span class="n"&gt;get_push_certificate&lt;/span&gt;

  &lt;span class="n"&gt;project_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;ipa_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"build/ios/ipa/*.ipa"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
    &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not find IPA file."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Found IPA at: &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;release_notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;load_release_notes&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;

  &lt;span class="n"&gt;upload_to_app_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;ipa: &lt;/span&gt;&lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;skip_metadata: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;skip_screenshots: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;submit_for_review: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:submit_for_review&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;automatic_release: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:automatic_release&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;metadata_path: &lt;/span&gt;&lt;span class="s2"&gt;"./fastlane/metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;release_notes: &lt;/span&gt;&lt;span class="n"&gt;release_notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;precheck_include_in_app_purchases: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;submission_information: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;export_compliance_uses_encryption: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;export_compliance_encryption_updated: &lt;/span&gt;&lt;span class="kp"&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy without submitting for review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy and submit for review in one shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fastlane deploy submit_for_review:true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;submission_information&lt;/code&gt; block handles Apple's export compliance questions automatically. No more clicking through those dialogs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The full Fastfile
&lt;/h2&gt;

&lt;p&gt;Here is the complete Fastfile putting it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;default_platform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:ios&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="ss"&gt;:ios&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_api_key&lt;/span&gt;
    &lt;span class="n"&gt;app_store_connect_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;key_id: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_KEY_ID"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;issuer_id: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_ISSUER_ID"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;key_filepath: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FASTLANE_APP_STORE_CONNECT_API_KEY_PATH"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;in_house: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_release_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;metadata_path&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'en-US/release_notes.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;locales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w[ar-SA ca cs da de-DE el en-AU en-CA en-GB en-US
                 es-ES es-MX fi fr-CA fr-FR he hi hr hu id it ja
                 ko ms nl-NL no pl pt-BR pt-PT ro ru sk sv th tr
                 uk vi zh-Hans zh-Hant]&lt;/span&gt;
    &lt;span class="n"&gt;locales&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each_with_object&lt;/span&gt;&lt;span class="p"&gt;({})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'release_notes.txt'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exist?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;fallback&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:download_app_metadata&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;load_api_key&lt;/span&gt;
    &lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;download_metadata: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;download_screenshots: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&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;end&lt;/span&gt;

  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:release_new_version&lt;/span&gt; &lt;span class="k"&gt;do&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="n"&gt;load_api_key&lt;/span&gt;
    &lt;span class="n"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;app_identifier: &lt;/span&gt;&lt;span class="s2"&gt;"com.yourcompany.yourapp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;app_version: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;release_notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;load_release_notes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'./metadata'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;app_version: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;skip_binary_upload: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;submit_for_review: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;automatic_release: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;metadata_path: &lt;/span&gt;&lt;span class="s2"&gt;"./fastlane/metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;release_notes: &lt;/span&gt;&lt;span class="n"&gt;release_notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;precheck_include_in_app_purchases: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_screenshots: &lt;/span&gt;&lt;span class="kp"&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;end&lt;/span&gt;

  &lt;span class="n"&gt;lane&lt;/span&gt; &lt;span class="ss"&gt;:deploy&lt;/span&gt; &lt;span class="k"&gt;do&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="n"&gt;load_api_key&lt;/span&gt;
    &lt;span class="n"&gt;get_push_certificate&lt;/span&gt;
    &lt;span class="n"&gt;project_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"../../"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ipa_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"build/ios/ipa/*.ipa"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="no"&gt;UI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Could not find IPA file."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="n"&gt;release_notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:version&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;load_release_notes&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
    &lt;span class="n"&gt;upload_to_app_store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;ipa: &lt;/span&gt;&lt;span class="n"&gt;ipa_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_metadata: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;skip_screenshots: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;force: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;submit_for_review: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:submit_for_review&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;automatic_release: &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:automatic_release&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;metadata_path: &lt;/span&gt;&lt;span class="s2"&gt;"./fastlane/metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;release_notes: &lt;/span&gt;&lt;span class="n"&gt;release_notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;precheck_include_in_app_purchases: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;submission_information: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;export_compliance_uses_encryption: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;export_compliance_encryption_updated: &lt;/span&gt;&lt;span class="kp"&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  No reasons to not have metadata for all locales
&lt;/h2&gt;

&lt;p&gt;One of the best reason to handle metadata with Fastlane is that you can version control your ASO.&lt;br&gt;
(ASO = App Store Optimization. The art of writing good descriptions, release notes, and keywords to rank higher in search results. This is SEO for the App Store.)&lt;/p&gt;

&lt;p&gt;When your metadata is in git, you can see the history of every change, revert if needed, and even have translators submit PRs with updated descriptions and release notes.&lt;/p&gt;

&lt;p&gt;But don't translate litterally everything. &lt;br&gt;
Some countries, even if they are non-English, will have better ASO with English metadata.&lt;/p&gt;

&lt;p&gt;I'll write another article about ASO best practices. &lt;/p&gt;


&lt;h2&gt;
  
  
  My typical deploy workflow
&lt;/h2&gt;

&lt;p&gt;Here is what a release looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Build the Flutter app&lt;/span&gt;
flutter build ipa

&lt;span class="c"&gt;# 2. Update release notes in metadata/en-US/release_notes.txt&lt;/span&gt;
&lt;span class="c"&gt;# (and other locales if needed)&lt;/span&gt;

&lt;span class="c"&gt;# 3. Deploy to App Store Connect&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;ios
fastlane deploy version:&lt;span class="s2"&gt;"1.2.0"&lt;/span&gt; submit_for_review:true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three commands. That is it. No Xcode, no web browser, no manual form filling.&lt;/p&gt;

&lt;p&gt;What used to take 20-30 minutes of clicking through App Store Connect now takes under 2 minutes of actual work. &lt;br&gt;
The upload itself still takes a few minutes depending on your binary size, but you are free to do something else while it runs.&lt;/p&gt;

&lt;p&gt;Bonus I also made a script to fetch the pubspec version and run all commands in one go. &lt;br&gt;
And for Android too but this article is about iOS so I won't go into that.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about first release?
&lt;/h2&gt;

&lt;p&gt;Start by making all metadata research for US market. Write your description, release notes, and keywords in English. &lt;/p&gt;

&lt;p&gt;Then use a script to translate description, promotional text to all other languages. &lt;br&gt;
For title, subtitle, and keywords I tend to keep writing them manually as they require some research and creativity for each market.&lt;/p&gt;

&lt;p&gt;Don't forget screenshots. &lt;br&gt;
I made a plugin to automate screenshot translations using figma. If that interests you, let me know and I can write an article about it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tips and common pitfalls
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Store your API key securely
&lt;/h3&gt;

&lt;p&gt;Never commit the &lt;code&gt;.p8&lt;/code&gt; file to git. Add it to &lt;code&gt;.gitignore&lt;/code&gt; and use environment variables or your CI's secret store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use &lt;code&gt;force: true&lt;/code&gt; carefully
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;force: true&lt;/code&gt; flag skips Fastlane's HTML report preview. This is fine for CI, but when running locally for the first time, remove it to double-check what Fastlane will push.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep metadata in version control
&lt;/h3&gt;

&lt;p&gt;Your &lt;code&gt;metadata/&lt;/code&gt; folder should be committed to git. This gives you a history of every release note change and makes it easy for translators to submit PRs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handle missing locales gracefully
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;load_release_notes&lt;/code&gt; helper above falls back to English for any missing locale. This prevents submission failures when you add a new language to your app but haven't translated the release notes yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export compliance
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;submission_information&lt;/code&gt; block in the deploy lane answers Apple's export compliance questions automatically. If your app uses encryption beyond standard HTTPS, you will need to adjust these values.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop deploying manually
&lt;/h2&gt;

&lt;p&gt;Every minute you spend clicking through Xcode and App Store Connect is a minute you are not building features or fixing bugs.&lt;/p&gt;

&lt;p&gt;Fastlane is a one-time setup that pays for itself on every single release. If you are shipping a Flutter app to the App Store, this is not optional. It is infrastructure.&lt;/p&gt;

&lt;p&gt;Set it up once. Deploy with one command. Move on to the work that actually matters.&lt;/p&gt;

&lt;p&gt;If you want to skip the boilerplate entirely and start with a Flutter project that already includes deployment automation, authentication, subscriptions, and everything else you need to ship, check out &lt;a href="https://apparencekit.dev" rel="noopener noreferrer"&gt;ApparenceKit&lt;/a&gt;. It is built for developers who want to ship fast and focus on their product.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>mobile</category>
      <category>flutter</category>
      <category>fastlane</category>
    </item>
    <item>
      <title>How to Monetize a Mobile App — 6 Proven Strategies That Actually Work</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Mon, 13 Apr 2026 08:38:03 +0000</pubDate>
      <link>https://forem.com/gautier/how-to-monetize-a-mobile-app-6-proven-strategies-that-actually-work-1ck</link>
      <guid>https://forem.com/gautier/how-to-monetize-a-mobile-app-6-proven-strategies-that-actually-work-1ck</guid>
      <description>&lt;p&gt;Most apps don't fail because of bad code.&lt;br&gt;&lt;br&gt;
They fail because the developer never figured out how to make money from them.&lt;/p&gt;

&lt;p&gt;After building and shipping apps for years at our agency, I can tell you this.&lt;br&gt;&lt;br&gt;
The monetization strategy matters more than the tech stack. More than most things developers spend their time on.&lt;/p&gt;

&lt;p&gt;Yet most developers treat monetization as an afterthought. &lt;br&gt;
They build for months, then slap a paywall at the end and wonder why nobody pays.&lt;/p&gt;

&lt;p&gt;So if you're wondering how to monetize a mobile app, this is what actually works in 2026.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before monetization: the 3 pillars of a successful app
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check if people actually use your app
&lt;/h3&gt;

&lt;p&gt;Put analytics in from day one. Track user behavior. Which screens do they visit? Where do they drop off? How long do they stay? &lt;br&gt;
Your first app version is not about making money. It's about proving that people actually use your app and find value in it.&lt;br&gt;
If you don't have users, you don't have a business. So focus on retention and engagement before worrying about monetization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build a great onboarding flow
&lt;/h3&gt;

&lt;p&gt;Onboarding is the first experience users have with your app. It's your chance to show them the value. &lt;/p&gt;

&lt;p&gt;"Don't tell them, show them."&lt;br&gt;&lt;br&gt;
That's what ex Apple designer Mike Stern says. Don't just explain what your app does. Let users experience it right away. Show them the core value in the first session.&lt;br&gt;
A good onboarding flow can increase retention and conversion dramatically. It sets the stage for monetization later on.&lt;br&gt;
You don't have to show and explain all your features. Just find the one or two core benefits and make sure users experience them during onboarding.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to monetize apps: 6 models that work
&lt;/h2&gt;

&lt;p&gt;Before diving in, here's a quick overview of the 6 main ways to make money by making apps.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subscriptions&lt;/strong&gt; — recurring revenue, highest lifetime value. The king.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freemium&lt;/strong&gt; — free core experience, paid premium features&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-app purchases&lt;/strong&gt; — consumable credits or one-time unlocks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ads&lt;/strong&gt; — display, interstitial, or rewarded video &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-time purchase&lt;/strong&gt; — pay upfront to download&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sponsorships / affiliate&lt;/strong&gt; — partner revenue&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll break down the top 3 in detail, then cover the rest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Subscriptions — the king of app revenue
&lt;/h2&gt;

&lt;p&gt;If your app has any kind of recurring usage, subscriptions are the way to go.&lt;br&gt;&lt;br&gt;
Predictable revenue. Higher lifetime value. And both Apple and Google actively push subscription apps in their stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weekly, monthly, or yearly?
&lt;/h3&gt;

&lt;p&gt;This depends on your app.&lt;br&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weekly&lt;/strong&gt; works for apps with daily usage and high engagement (fitness, AI assistants). Downside is higher churn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly&lt;/strong&gt; not the best in 2026. All report show a drop in conversion compared to weekly. Users prefer shorter commitments. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yearly&lt;/strong&gt; works when users trust the value — offer a discount to push them here&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  💡 &lt;strong&gt;Test your prices.&lt;/strong&gt; Don't guess.
&lt;/h4&gt;

&lt;p&gt;$4.99/week vs $9.99/month can make a 3x difference in revenue. Start with a hypothesis, measure, and iterate.&lt;br&gt;&lt;br&gt;
Using revenuecat, superwall, adapty... you can A/B test different price points and billing intervals to find what converts best for your audience.&lt;/p&gt;

&lt;p&gt;After testing a lot of apps and paywalls &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;copy is less important than your offer &lt;/li&gt;
&lt;li&gt;the price and billing interval are the main levers to increase conversion and revenue.&lt;/li&gt;
&lt;li&gt;the design of your paywall matters less than showing it at the right time and demonstrating value before asking for money. But you can't sell high prices if your paywall looks cheap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Show value before the paywall
&lt;/h3&gt;

&lt;p&gt;This is critical.&lt;br&gt;&lt;br&gt;
If your users hit a paywall before understanding what the app does, they will just uninstall.&lt;/p&gt;

&lt;p&gt;The flow should always be: &lt;strong&gt;Onboarding → Value → Paywall.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Show them what your app can do first. Let them feel the benefit. Then ask for money.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hard paywall or soft paywall?&lt;br&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard paywall&lt;/strong&gt; — users can't access any content or features until they subscribe. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Soft paywall&lt;/strong&gt; — users can access some content or features for free, but need to subscribe for full access.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hard paywalls convert better but can lead to higher churn if users don't understand the value.&lt;br&gt;
Also I advise you to always test. Hard paywall doesn't work for every app. Data is always king. &lt;br&gt;
I personnaly prefer freemium but if you have a strong onboarding that shows the value, a hard paywall can work well too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Show your paywall during onboarding
&lt;/h3&gt;

&lt;p&gt;This is a controversial one. Many developers think they need to wait until users have "discovered" the app.&lt;/p&gt;

&lt;p&gt;Here's the thing most developers get wrong.&lt;br&gt;&lt;br&gt;
They wait for day 3 or day 7 to show the paywall. Thinking the user needs time to "discover" the app.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most of your paid users will come from the first session.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Users who never see your paywall never convert. So show it during onboarding, right after you demonstrated value.&lt;/p&gt;

&lt;p&gt;Don't wait. Don't hope they'll come back. Show the paywall on first use.&lt;/p&gt;

&lt;p&gt;When you go at a restaurant and the waiter comes to your table, do you say "I need to discover the menu first, come back later"? No. You look at the menu, see the value, and decide if you want to order. It's the same with your app.&lt;/p&gt;

&lt;p&gt;I wrote a full article on &lt;a href="https://apparencekit.dev/blog/flutter-good-paywall/" rel="noopener noreferrer"&gt;how to create a good paywall&lt;/a&gt; if you want to go deeper on this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Freemium — let users try before they pay
&lt;/h2&gt;

&lt;p&gt;Freemium is simple.&lt;br&gt;&lt;br&gt;
And hard. &lt;/p&gt;

&lt;p&gt;Give enough value for free to hook users. Gate enough to make them want to pay.&lt;/p&gt;

&lt;p&gt;The hard part is finding the balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to gate
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Features&lt;/strong&gt; — free users get basic features, paid users get advanced ones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Usage limits&lt;/strong&gt; — free users get 5 exports per month, paid get unlimited&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt; — free users see previews, paid users get full access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove ads&lt;/strong&gt; — if you use ads, let users pay to remove them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ex :&lt;br&gt;&lt;br&gt;
Spotify does it well. Free with ads. Paid without. Simple and effective.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conversion benchmarks
&lt;/h3&gt;

&lt;p&gt;Typical freemium conversion is 2-5%.&lt;br&gt;&lt;br&gt;
Good is 5-10%. If you're above 10% you're doing great.&lt;/p&gt;

&lt;p&gt;The key to conversion? A strong &lt;a href="https://dev.to/flutter-templates/onboarding"&gt;onboarding&lt;/a&gt; that shows the value of your app before users decide to stay or leave.&lt;/p&gt;




&lt;h2&gt;
  
  
  In-app purchases, ads, and other models
&lt;/h2&gt;

&lt;h3&gt;
  
  
  In-app purchases
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Consumables&lt;/strong&gt; — credits, tokens, coins. Users buy them, use them, buy more. This works great for AI apps (token-based), games, and content apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-consumables&lt;/strong&gt; — unlock a feature or content forever. One-time purchase inside the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building an AI-powered app, token-based IAP is a very natural fit.&lt;br&gt;&lt;br&gt;
Users pay for what they use. No commitment. Low friction.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://apparencekit.dev/flutter-templates/in-app-purchase" rel="noopener noreferrer"&gt;in-app purchase template&lt;/a&gt; if you want to see how this works in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ads
&lt;/h3&gt;

&lt;p&gt;Let's be honest.&lt;br&gt;&lt;br&gt;
Ads are the default for lazy monetization.&lt;/p&gt;

&lt;p&gt;If your app solves a real problem, subscriptions or freemium will always earn more per user.&lt;br&gt;&lt;br&gt;
Ads make sense when you have high volume and low engagement — utility apps, games, content aggregators.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewarded ads&lt;/strong&gt; are the exception. Users watch a video in exchange for a bonus or feature unlock. This works well as a complement to IAP or freemium.&lt;/p&gt;

&lt;p&gt;But don't build your entire business on ad revenue. It's volatile and you need massive scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  One-time purchase
&lt;/h3&gt;

&lt;p&gt;The paid upfront model is dying for most categories.&lt;br&gt;&lt;br&gt;
Users expect to try before they pay. And a $4.99 upfront purchase has a much lower lifetime value than a $4.99/month subscription.&lt;/p&gt;

&lt;p&gt;It still works for specific utilities and tools where users need the app once or occasionally. But for most apps, skip this model.&lt;/p&gt;




&lt;h2&gt;
  
  
  App ideas to make money in 2026
&lt;/h2&gt;

&lt;p&gt;The best app ideas solve a specific problem for a specific group of people.&lt;br&gt;&lt;br&gt;
The more niche and specific, the better.&lt;/p&gt;

&lt;p&gt;Ex: &lt;br&gt;
Running app -&amp;gt; Running app for women -&amp;gt; Running app for pregnant women...&lt;/p&gt;

&lt;p&gt;Don't try to build the next Instagram. Build something useful for 1,000 people who are willing to pay.&lt;/p&gt;

&lt;p&gt;Here are some ideas that work well with a subscription or freemium model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI-powered meal planner&lt;/strong&gt; — generate weekly meal plans and grocery lists based on preferences and budget. Health niche is huge and people pay for convenience. Subscription model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Niche tracker&lt;/strong&gt; — not a generic one. One for new parents tracking baby sleep. Or for musicians tracking practice sessions. The more specific, the more people pay. Freemium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B2B inspection or checklist app&lt;/strong&gt; — this is a goldmine. Businesses pay without thinking twice. Construction inspections, restaurant health checks, fleet maintenance logs. Subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI writing assistant for a specific niche&lt;/strong&gt; — not a general ChatGPT wrapper. One for real estate agents writing listings. Or for teachers creating lesson plans. Token-based IAP or subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget tracker for freelancers&lt;/strong&gt; — free basic tracking, paid for tax reports and invoice generation. Freemium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Digital wellness coach&lt;/strong&gt; — screen time tracking with actionable suggestions. Guided digital detox challenges. Subscription. (careful this is a saturated niche, but if you find an original concept there is always some space. People love to try new ideas in this category)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The common thread?&lt;br&gt;&lt;br&gt;
All of these can be built and launched in weeks if you don't waste time building auth, payments, and infrastructure from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to make money from mobile apps — checklist
&lt;/h2&gt;

&lt;p&gt;After seeing many apps succeed and fail, here's what actually matters.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a niche with willingness to pay.&lt;/strong&gt; B2B pays more than B2C. Health pays more than entertainment. Niche pays more than generic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose your monetization model before writing code.&lt;/strong&gt; If you don't know how you'll make money, don't start building.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build your paywall into the MVP.&lt;/strong&gt; Not after launch. Not in v2. In the first version. Monetization is not a feature — it's the foundation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track events from day one.&lt;/strong&gt; Which screens do users visit? Where do they drop off? What triggers a purchase? You need this data from the start. Read more about &lt;a href="https://dev.to/blog/flutter-app-events/"&gt;managing events in your app&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forget ASO in 2026.&lt;/strong&gt; You can't rely on App Store search to get discovered anymore. The stores are saturated. Everyone is optimizing for the same keywords. ASO alone won't save you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your market with ads.&lt;/strong&gt; Before building for months, spend $50-100 on Meta or TikTok ads pointing to your App Store page. If you can get your first 100 installs cheaply, your idea has legs. If not, pivot before wasting more time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch fast, iterate based on data.&lt;/strong&gt; Your first version will not be perfect. That's fine. Get it out, see what converts, and improve weekly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  How to create an app and make money — without building everything from scratch
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth.&lt;br&gt;&lt;br&gt;
Building auth, subscriptions, paywalls, push notifications, onboarding, and analytics from scratch takes 2-3 months minimum.&lt;br&gt;&lt;br&gt;
And that's just the boring stuff. The stuff that has nothing to do with your actual idea.&lt;/p&gt;

&lt;p&gt;This is exactly why we built &lt;a href="https://apparencekit.dev" rel="noopener noreferrer"&gt;ApparenceKit&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
It's a Flutter template that ships with everything you need to monetize your app from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paywall and subscription management (Apple and Google validated)&lt;/li&gt;
&lt;li&gt;In-app purchases&lt;/li&gt;
&lt;li&gt;Authentication (email, social, anonymous)&lt;/li&gt;
&lt;li&gt;Push notifications&lt;/li&gt;
&lt;li&gt;Onboarding flow&lt;/li&gt;
&lt;li&gt;Analytics and event tracking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You buy it once, you own the code, and you focus on what makes your app unique.&lt;/p&gt;

&lt;p&gt;The developers making money from apps aren't the ones writing the best code.&lt;br&gt;&lt;br&gt;
They're the ones who ship first and iterate fast.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>mobile</category>
      <category>startup</category>
    </item>
    <item>
      <title>Flutter theme made easy</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Tue, 24 Oct 2023 14:24:34 +0000</pubDate>
      <link>https://forem.com/gautier/flutter-theme-made-easy-4jmc</link>
      <guid>https://forem.com/gautier/flutter-theme-made-easy-4jmc</guid>
      <description>&lt;p&gt;Flutter provides you with a way to customize your app theme. &lt;br&gt;&lt;br&gt;
It is extremely focused on material design but you can still customize it to fit your needs.&lt;/p&gt;
&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Flutter provides you with a great theme to let you customize your app. &lt;br&gt;&lt;br&gt;
The problem is when you want to start getting out of material and customize your app theme. &lt;br&gt;&lt;br&gt;
Most designers will want to customize the theme to fit their needs and tastes. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;You create your theme directly without any factory.&lt;br&gt;
This means you will have many developers copy-pasting colors throughout the app and the themeData file. &lt;/p&gt;

&lt;p&gt;You can't switch the theme without restarting the app (dark/light?).&lt;br&gt;&lt;br&gt;
You don't have a way to set different themes for different platforms.&lt;/p&gt;

&lt;p&gt;The good news is that we can fix all of this.&lt;/p&gt;
&lt;h2&gt;
  
  
  Handling Dark and Light theme
&lt;/h2&gt;

&lt;p&gt;Switching from light to dark mode should be easy. &lt;br&gt;&lt;br&gt;
The best way to do this is to not duplicate your theme but to create a factory that will create the theme from a color palette. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9rAP-BAa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27ae54b0g5vq9tbgy3z2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9rAP-BAa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/27ae54b0g5vq9tbgy3z2.png" alt="flutter dark and light theme" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Flutter and Material provide you with a nice logical way to handle an app theme. &lt;br&gt;
Every color is an 'on' color. &lt;br&gt;&lt;br&gt;
Meaning that if you have a white background you should have a black text color. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;Switching from light to dark should only rely on switching some colors. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bCmruVlz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq7m22dp5f36xam3jv9x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bCmruVlz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zq7m22dp5f36xam3jv9x.png" alt="Flutter theme color palette" width="800" height="231"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should not have to handle if case in your code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Limit the number of colors
&lt;/h2&gt;

&lt;p&gt;The more colors you have the more difficult it will be to maintain your app. &lt;br&gt;&lt;br&gt;
Simplicity is oftenly the best way to go. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;Instead of having primary, secondary, accent, secondaryAccent, ... &lt;br&gt;&lt;br&gt;
Try making your app with one or two colors. &lt;br&gt;&lt;br&gt;
The more colors you incorporate, the more challenging it becomes to maintain your app. &lt;br&gt;
Simplicity is often the best approach. Instead of using primary, secondary, accent, secondaryAccent, and so forth, consider designing your app with just one or two colors. &lt;/p&gt;

&lt;p&gt;(Please note that black, white, and greys are exceptions and are not considered as colors.)&lt;/p&gt;

&lt;p&gt;For example, you can check some of the most popular apps. &lt;br&gt;&lt;br&gt;
Facebook, Instagram, Twitter, Youtube, ... &lt;br&gt;&lt;br&gt;
They don't use a lot of colors. &lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a theme factory
&lt;/h2&gt;

&lt;p&gt;As we want to create our theme for maybe different platforms or different color palettes we will create a factory. &lt;br&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;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApparenceKitThemeDataFactory&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;ApparenceKitThemeDataFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="n"&gt;ApparenceKitThemeData&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;required&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitColors&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitTextTheme&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&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;This factory will take a color palette and a default text theme and will create a theme. &lt;br&gt;&lt;br&gt;
The default text theme contains the default text style and fonts for all our text. &lt;br&gt;
As the colors I also recommend to limit the number of fonts you use. &lt;br&gt;&lt;br&gt;
Our designer at Apparence tend to limit his usage to 2 fonts. &lt;br&gt;&lt;br&gt;
(There is some website that give you inspirations for 2 fonts that go well together...)&lt;/p&gt;
&lt;h3&gt;
  
  
  Using our factory
&lt;/h3&gt;

&lt;p&gt;Now that we have our factory we can use it to create our theme. &lt;br&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;UniversalThemeFactory&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitThemeDataFactory&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;UniversalThemeFactory&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;ApparenceKitThemeData&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;required&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitColors&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitTextTheme&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&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;ApparenceKitThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;colors:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;defaultTextTheme:&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;materialTheme:&lt;/span&gt; &lt;span class="n"&gt;ThemeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;colorScheme:&lt;/span&gt; &lt;span class="n"&gt;ColorScheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;seedColor:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;copyWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;surface:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;onSurface:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onSurface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;background:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;background&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;onBackground:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onBackground&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;primary:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;error:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;elevatedButtonTheme:&lt;/span&gt; &lt;span class="n"&gt;elevatedButtonTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;colors:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;inputDecorationTheme:&lt;/span&gt; &lt;span class="n"&gt;inputDecorationTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;colors:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;textTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;colors:&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;defaultTextStyle:&lt;/span&gt; &lt;span class="n"&gt;defaultTextStyle&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="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;As you can see we are using the color palette and the default text theme to create our theme. &lt;br&gt;&lt;br&gt;
Now changing colors or text style won't be done here. &lt;br&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We have defined our own color palette and default text files separetly.&lt;/strong&gt; &lt;br&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Providing our theme through the app
&lt;/h2&gt;

&lt;p&gt;As we want to access our theme from anywhere in our app we will use an InheritedNotifier. &lt;br&gt;&lt;br&gt;
This will allow us to access our theme from the BuildContext. &lt;br&gt;&lt;br&gt;
But also to switch the mode of our theme without restarting the app. &lt;br&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;/// We use this to access the theme from the BuildContext in all our widgets&lt;/span&gt;
&lt;span class="c1"&gt;/// We don't use riverpod here so we can get the theme from the context and regular widgets&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ThemeProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;InheritedNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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;ThemeProvider&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;key&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;notifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kd"&gt;required&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;child&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="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;updateShouldNotify&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;InheritedNotifier&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&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="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="n"&gt;AppTheme&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dependOnInheritedWidgetOfExactType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ThemeProvider&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;notifier&lt;/span&gt;&lt;span class="o"&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;h3&gt;
  
  
  Using our theme ThemeProvider
&lt;/h3&gt;

&lt;p&gt;Now that we have our ThemeProvider we can use it in our app. &lt;br&gt;&lt;br&gt;
It will be above our MaterialApp. &lt;br&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;MyApp&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatelessWidget&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;MyApp&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// This widget is the root of your application.&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ThemeProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;notifier:&lt;/span&gt; &lt;span class="n"&gt;AppTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;themeFactory:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;UniversalThemeFactory&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nl"&gt;lightColors:&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nl"&gt;darkColors:&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitColors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nl"&gt;textTheme:&lt;/span&gt; &lt;span class="n"&gt;ApparenceKitTextTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="nl"&gt;defaultMode:&lt;/span&gt; &lt;span class="n"&gt;ThemeMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;light&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;Builder&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;title:&lt;/span&gt; &lt;span class="s"&gt;'Flutter Pro Starter Kit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;initialRoute:&lt;/span&gt; &lt;span class="s"&gt;'home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;theme:&lt;/span&gt; &lt;span class="n"&gt;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="na"&gt;light&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;darkTheme:&lt;/span&gt; &lt;span class="n"&gt;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="na"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;themeMode:&lt;/span&gt; &lt;span class="n"&gt;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't want to use dark mode you can just remove it.&lt;/p&gt;

&lt;p&gt;This way we can access our theme from anywhere in our app. &lt;br&gt;&lt;br&gt;
Using for example  &lt;br&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="na"&gt;current&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or still using the regular Theme.of(context) &lt;br&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;Theme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="na"&gt;textTheme&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;headline1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Creating theme shortcuts
&lt;/h3&gt;

&lt;p&gt;As we want to access our theme from anywhere in our app we will create some shortcuts. &lt;br&gt;&lt;br&gt;
This will allow us to access our theme from the BuildContext. &lt;br&gt;&lt;br&gt;
But also to switch the mode of our theme without restarting the app. &lt;br&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;extension&lt;/span&gt; &lt;span class="n"&gt;ThemeExtension&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// access the theme from anywhere in the app using context.theme  &lt;/span&gt;
  &lt;span class="n"&gt;ApparenceKitThemeData&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;theme&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// access the color palette from anywhere in the app using context.colors  &lt;/span&gt;
  &lt;span class="n"&gt;ApparenceKitColors&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;colors&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// access the text theme from anywhere in the app using context.textTheme&lt;/span&gt;
  &lt;span class="n"&gt;ApparenceKitTextTheme&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;textTheme&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;current&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;textTheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// access the theme mode from anywhere in the app using context.themeMode&lt;/span&gt;
  &lt;span class="n"&gt;ThemeMode&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="n"&gt;themeMode&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// toggle the theme mode from anywhere in the app using context.toggleTheme&lt;/span&gt;
  &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;toggleTheme&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;ThemeProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toggle&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;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;There is tons of things to say about handling theme from design to code. &lt;br&gt;&lt;br&gt;
multiplatform theme, gradients, shadows, ... &lt;br&gt;&lt;/p&gt;

&lt;p&gt;I hope this article will help you to get started with creating a great multiplatform theme. &lt;br&gt;&lt;br&gt;
This way of handling theme is the one we use at Apparence and we included it in our starter kit. &lt;br&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Our Flutter Template is Here!</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Fri, 13 Oct 2023 12:28:23 +0000</pubDate>
      <link>https://forem.com/gautier/our-flutter-template-is-here-8b9</link>
      <guid>https://forem.com/gautier/our-flutter-template-is-here-8b9</guid>
      <description>&lt;p&gt;In the dynamic realm of mobile app development, speed, efficiency, and feature-rich applications are the benchmarks of success. &lt;br&gt;
If you're part of the ever-growing Flutter community, brace yourself for a game-changing announcement! &lt;br&gt;
I am thrilled to introduce our "Flutter Template," a comprehensive solution designed to catapult your app development journey to new heights. &lt;br&gt;
Whether you're an experienced developer or just beginning your Flutter adventure, this template is your passport to creating Flutter apps faster and with unprecedented ease.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use a Flutter Template?
&lt;/h2&gt;

&lt;p&gt;Instead of reinventing the wheel with every project, a Flutter Template allows you to jumpstart your development.&lt;br&gt;
Whether you're a seasoned developer looking to accelerate your workflow or a newcomer seeking a structured approach to Flutter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's included in the Flutter Template?
&lt;/h2&gt;

&lt;p&gt;The ApparenceKit Flutter Template is a comprehensive solution that includes everything you need to build a Flutter app.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Notifications Integration
&lt;/h3&gt;

&lt;p&gt;What's a mobile app without notifications?&lt;/p&gt;

&lt;p&gt;The ApparenceKit Flutter Template includes a fully integrated notifications system that allows you to receive push notifications.&lt;br&gt;
It's easy to set up and works with both Android and iOS.&lt;br&gt;
We handle device registration, message handling, and more, so you can focus on building your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Authentication
&lt;/h3&gt;

&lt;p&gt;Authentication is a critical component of any app, and the ApparenceKit Flutter Template makes it easy to add authentication to your app.&lt;br&gt;
You can choose between email/password authentication or social login with Facebook, Google, or Twitter.&lt;br&gt;
We handle all the heavy lifting so you can focus on building your app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uPnJv7Bc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tl9jh17xdfn9hoqijrm5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uPnJv7Bc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tl9jh17xdfn9hoqijrm5.jpg" alt="Login flutter template" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bonus: we also provide a CLI to switch between firebase and custom backend. &lt;/p&gt;

&lt;h3&gt;
  
  
  3. Environments Made Simple
&lt;/h3&gt;

&lt;p&gt;Enjoy a seamless development journey with predefined environments for dev, staging, and production, streamlining your workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. In-App Rating and Reviews
&lt;/h3&gt;

&lt;p&gt;Gather valuable user feedback and enhance your app's ratings with our integrated in-app rating and review features.&lt;br&gt;
Asking for feedback is a great way to improve your app and increase user engagement.&lt;br&gt;
But you have to do it right.&lt;/p&gt;

&lt;p&gt;For example you don't want to ask for feedback too often or at the wrong time.&lt;br&gt;
Apple and Google have strict guidelines on how often you can ask for feedback.&lt;br&gt;
So you should ask for feedback at the right time and in the right way.&lt;/p&gt;

&lt;p&gt;We handle all of this for you. &lt;br&gt;
We provide a customizable in-app rating and review system that is easy to integrate into your app.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Monetization Made Easy
&lt;/h3&gt;

&lt;p&gt;Handling subscriptions on iOS and Android can be a pain.&lt;br&gt;
You have to deal with different APIs, different payment methods, and different rules.&lt;br&gt;
We made a customizable subscription system with ready-to-use screens. &lt;br&gt;
You can easily integrate it into your app and start monetizing it. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ukd5i-oV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ts88t3ov567as0gq0tmy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ukd5i-oV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ts88t3ov567as0gq0tmy.jpg" alt="Subscription flutter template" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As this is a template you can customize it to your needs.&lt;br&gt;
A complete documentation is available to help you with that.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. CI/CD
&lt;/h3&gt;

&lt;p&gt;Continuous Integration and Continuous Delivery (CI/CD) is a software development practice that enables developers to build, test, and deploy code changes more frequently and reliably.&lt;br&gt;
We provides a complete CI/CD guide and Gitlab templates for your Flutter app.&lt;br&gt;
We used a custom Gitlab runner for 4 years to build and deploy all our apps. This template is the result of this experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IQYN8Yjp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emg0x1f1lgy9brmjdh8c.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IQYN8Yjp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/emg0x1f1lgy9brmjdh8c.jpg" alt="CI flutter template for gitlab" width="800" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github templates are also available but not as complete as the Gitlab ones. &lt;br&gt;
(Basically we don't build and deploy on stores with Github). &lt;br&gt;
But a dependabot configuration is available to keep your dependencies up to date. &lt;/p&gt;

&lt;h3&gt;
  
  
  7. Complete documentation
&lt;/h3&gt;

&lt;p&gt;We provide complete documentation to help you with the template.&lt;br&gt;
It includes a getting started guide, notifications setup guide, social auth setup guide, how to write great tests, release on stores guide...&lt;br&gt;
And tons of others. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VZ5z36Fe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9mbe3ebepwmrunrchih.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VZ5z36Fe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f9mbe3ebepwmrunrchih.jpg" alt="Flutter App example documentation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In the competitive world of app development, our Flutter Template is your secret weapon, allowing you to create exceptional apps quickly and efficiently. &lt;br&gt;
Whether you're working on a personal project or developing for clients, our feature-rich template equips you with the tools and support you need to thrive. &lt;br&gt;
Don't miss this opportunity to revolutionize your Flutter app development. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://apparencekit.dev"&gt;Start using our template today and unleash the full potential of your Flutter creations!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: if you aren't satisfied with the kit, reach out and you'll get a full refund. No questions asked.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Flutter - In-app education plugin</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Wed, 10 Nov 2021 09:43:57 +0000</pubDate>
      <link>https://forem.com/gautier/flutter-in-app-education-plugin-38lc</link>
      <guid>https://forem.com/gautier/flutter-in-app-education-plugin-38lc</guid>
      <description>&lt;p&gt;Close the gap between content and adoption. Provide your users a way to know what they don't know. &lt;/p&gt;

&lt;p&gt;Some months ago we created Pal. &lt;br&gt;
The vision was to create a complete solution to help developers and market team create better app experience. &lt;br&gt;
We went a bit too far so today we goes back to a simpler solution. This is the first step of this rework. &lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing Pal widgets 
&lt;/h2&gt;

&lt;p&gt;Pal widgets is an open source plugin that lets you just create helper without any server. This is a complete open source plugin just aiming giving you designed widgets. &lt;br&gt;
When Pal required you to use a no code solution to create helpers. Pal-widgets let you make helpers with code anywhere you want in your app. &lt;/p&gt;

&lt;h2&gt;
  
  
  Hole Anchored Helper
&lt;/h2&gt;

&lt;p&gt;This is the first widget from Pal that is now available in Pal widgets plugin. This lets you create a designed tuto  (inspired by google tutorials) which creates a fullscreen overlay with just a hole above the widget you wants to explain.&lt;br&gt;
Nearly everything is optional so you can create the experience you want. &lt;br&gt;
Here is an example of anchored helper that will pop out when user tap on the hole. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fsbvjjrtyznhsq94361u6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fsbvjjrtyznhsq94361u6.gif" alt="tuto without buttons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Here is the widget code 
&lt;/h3&gt;

&lt;p&gt;Pal widget hole anchored helper without buttons. You can freely choose to show or not buttons or customize the onTapAnchor action. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2F4rb1rmqzk0lhv019tqy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2F4rb1rmqzk0lhv019tqy8.png" alt="code to create anchored pal widget helper"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a demo of a tuto with 2 buttons. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.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%2Fkxypgdm0wqu6ks4slv4x.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.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%2Fkxypgdm0wqu6ks4slv4x.gif" alt="Tuto with 2 buttons"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Wants to change the design ? 
&lt;/h3&gt;

&lt;p&gt;You can create your own anchored helper factory &lt;br&gt;
But we wants to go further and let you the ability to way more customize your tuto.&lt;/p&gt;

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

&lt;p&gt;First we are all open for your suggestions on this project. The goal of pal-plugins is to provide you a complete set of widgets to create your experience. &lt;br&gt;
Next we are looking to create more design, more customization, more widgets with the same vision. &lt;br&gt;
Hope you enjoy and creates amazing onboarding experience within your app. &lt;br&gt;
Share with us what you create. &lt;br&gt;
We loves to see what you do using Pal. &lt;/p&gt;

&lt;h2&gt;
  
  
  Package links
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pub.dev/packages/pal_widgets" rel="noopener noreferrer"&gt;Pub dev package link&lt;/a&gt; &lt;br&gt;
&lt;a href="https://github.com/Apparence-io/pal-widgets" rel="noopener noreferrer"&gt;Github repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Pal - our flutter no code editor for in app messages is out</title>
      <dc:creator>Gautier 💙</dc:creator>
      <pubDate>Fri, 15 Jan 2021 09:19:12 +0000</pubDate>
      <link>https://forem.com/gautier/pal-our-flutter-no-code-editor-for-in-app-messages-is-out-1jdo</link>
      <guid>https://forem.com/gautier/pal-our-flutter-no-code-editor-for-in-app-messages-is-out-1jdo</guid>
      <description>&lt;p&gt;Ever wanted to show an onboarding without code ? Show a marketing message on a click of button ? Now you got pal. &lt;/p&gt;

&lt;p&gt;Who has never been frustrated with having to code, repost on the stores just to display help or a personalized message on their application?&lt;/p&gt;

&lt;h2&gt;
  
  
  50% of our uninstall come from the frustration of the first application opening
&lt;/h2&gt;

&lt;p&gt;That’s a fact, we love to get introduced to the new things. Even more if this introduction seems personalized. &lt;br&gt;
&lt;strong&gt;Guiding user through your app makes the user feel you are helping him and you are concerned about him.&lt;/strong&gt;&lt;br&gt;
That’s exactly why there is people in stores to help you choose and get advices. So why not apply this to your mobile application ?&lt;/p&gt;

&lt;h2&gt;
  
  
  Guide users through their first experience
&lt;/h2&gt;

&lt;p&gt;Some of us call this onboarding. But a good onboarding is not just at the beginning. You can makes some anywhere on your app (after user made an action, the first time he goes to a page, after new update…).&lt;br&gt;
Hubspot breaks down retention into three phases: short-term retention, mid-term retention, and long-term retention. According to their research, the biggest drop-off in usage happens during the short-term retention period between Week 0 and Week 1. Retention drops nearly 75 percentage points down to just above 25% in a single week.&lt;br&gt;
Imagine if you could keep them ?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dPZylkC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/jk0is7yasaxbb4c3t8k3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dPZylkC0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/jk0is7yasaxbb4c3t8k3.gif" alt="Alt Text" width="320" height="693"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a perfect way to introduce our new users, let’s show him what he can do and what we wants him to do with our app.  Of course we won’t show him this again, this won’t bother him again. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--u_dIq7xQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/cm3intcrumdkv8yk62no.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--u_dIq7xQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/cm3intcrumdkv8yk62no.gif" alt="Alt Text" width="200" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pal is helping you doing this without having to code anything.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Guide user after important updates
&lt;/h2&gt;

&lt;p&gt;How many of your users knows why their app changes or that you worked for 1 month to give them new amazing features ?&lt;br&gt;
If you knew, this could certainly depress you… &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about just show them the big lines of your update when they re-open their app?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And of course doing this without code&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep your user all along
&lt;/h2&gt;

&lt;p&gt;You guided your users through his first experience. He is completely happy with your app. What about keeping his feeling that you are concerned about him ?&lt;br&gt;
Yes that’s what we are aiming with personalized messages. You can keep showing some messages less frequently to your best users using Pal.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Personalized messages
&lt;/h2&gt;

&lt;p&gt;Your worked for month to create a really sexy app so we know you want this to stay perfect. You got your fonts, colors, sizes… &lt;br&gt;
Also for all messages you wants a special experience so we made different types of messages experiences. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://en.apparence.io/blog/pal-no-code-messages-editor-flutter-plugin"&gt;read more here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>onboarding</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
