{"id":28783,"date":"2026-07-14T10:21:06","date_gmt":"2026-07-14T10:21:06","guid":{"rendered":"https:\/\/www.tftus.com\/blog\/?p=28783"},"modified":"2026-07-15T08:06:43","modified_gmt":"2026-07-15T08:06:43","slug":"flutter-app-publishing-requirements","status":"publish","type":"post","link":"https:\/\/www.tftus.com\/blog\/flutter-app-publishing-requirements","title":{"rendered":"Flutter App Publishing Requirements: Complete Guide for App Store &amp; Google Play"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-post\" data-elementor-id=\"28783\" class=\"elementor elementor-28783\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5ed2e454 e-flex e-con-boxed e-con e-parent\" data-id=\"5ed2e454\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-47f64fae elementor-widget elementor-widget-text-editor\" data-id=\"47f64fae\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t\t\t\t\t\t\n<p>Most Flutter developers hit the same wall: they finish the app, head straight to the store dashboard, and then discover a chain of compliance gates they didn&#8217;t know existed. The right sequence is not built \u2192 submit. It&#8217;s comply \u2192 build \u2192 submit. Get that order wrong, and you&#8217;re looking at rejections, a delayed launch, or \u2014 worst case \u2014 a banned developer account.<\/p>\n\n<p>This guide covers every Flutter app publishing requirement you need to clear before pressing &#8220;Submit for Review&#8221; on both the Google Play Store and the Apple App Store, including the Flutter-specific gotchas that generic app publishing guides don&#8217;t mention.<\/p>\n\n<p><em>Last updated: May 2026. Policy references:Google Play target API level requirements,Apple Privacy Manifest documentation,Google Play Console developer account requirements.<\/em><\/p>\n\n<h2 class=\"wp-block-heading\"><strong>What Makes Flutter App Publishing Different from Native Apps<\/strong><\/h2>\n\n<p>Flutter produces native apps from a single Dart codebase, but once you&#8217;re at the publishing stage, the rules come entirely from Google and Apple \u2014 not Flutter. That said, Flutter introduces several layers of complexity that native Android or iOS developers won&#8217;t be familiar with.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Flutter-Specific Publishing Gotchas<\/strong><\/h3>\n\n<p>The three areas where Flutter developers get tripped up most often are:<\/p>\n\n<p><strong>Merged manifest permissions.<\/strong> When you add packages from pub.dev, Flutter silently injects its AndroidManifest.xml permissions into your app&#8217;s merged manifest. You may declare no sensitive permissions in your own code, yet the final merged output includes location, camera, or Bluetooth access from a dependency. If your Google Play Data Safety form doesn&#8217;t account for those, expect a rejection \u2014 or worse, an account action. Google banned over 80,000 developer accounts in 2025 for policy violations, including undeclared permissions, according to Google&#8217;s annual Bad Apps report.<\/p>\n\n<p><strong>Apple&#8217;s Privacy Manifest requirement.<\/strong> Since February 12, 2025, Apple has hard-blocked any upload that doesn&#8217;t include a valid PrivacyInfo.xcprivacy file (Apple developer announcement). This is not just about your own code. The Flutter framework itself appears on Apple&#8217;s required-SDK list, meaning even a basic Flutter app needs a Privacy Manifest. Every plugin you use that also appears on the list must be covered.<\/p>\n\n<p><strong>pubspec.yaml as the version source of truth.<\/strong> Flutter reads version: 1.0.0+1 from pubspec.yaml and maps it to versionName + versionCode on Android and CFBundleShortVersionString + CFBundleVersion on iOS. You generally do not need to touch the Gradle file or Xcode settings for versioning. Version and build numbers must be properly incremented for every release.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Timeline and Cost Reality<\/strong><\/h3>\n\n<p>Before diving into the technical steps, here is every fee you&#8217;ll pay, in USD, in one place \u2014 something most publishing guides skip entirely.<\/p>\n\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Fee<\/strong><\/td>\n<td><strong>Platform<\/strong><\/td>\n<td><strong>Amount<\/strong><\/td>\n<td><strong>Notes<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Developer account<\/td>\n<td>Google Play<\/td>\n<td>$25 (one-time)<\/td>\n<td>Unlocks Google Play Console<\/td>\n<\/tr>\n<tr>\n<td>Developer program<\/td>\n<td>Apple<\/td>\n<td>$99\/year<\/td>\n<td>App delisted if subscription lapses<\/td>\n<\/tr>\n<tr>\n<td>In-app purchase commission<\/td>\n<td>Apple<\/td>\n<td>30% (standard)<\/td>\n<td>15% under Small Business Program (&lt;$1M\/yr revenue)<\/td>\n<\/tr>\n<tr>\n<td>CI\/CD (optional)<\/td>\n<td>Both<\/td>\n<td>$0\u2013$49\/month<\/td>\n<td>Codemagic free tier available<\/td>\n<\/tr>\n<tr>\n<td>Test devices<\/td>\n<td>Both<\/td>\n<td>Variable<\/td>\n<td>Physical device testing is strongly recommended<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n<p>One timeline note that catches first-time publishers off guard: if you register a <strong>personal<\/strong> Google Play Console developer account, you are subject to a closed testing gate that requires at least 12 opted-in testers for 14 continuous days before you can apply for production access. That adds roughly three to four weeks to your launch window. Organizations and businesses that register under a company account are exempt from this gate. Decide which account type you&#8217;re opening before you pay the $25 fee.<\/p>\n\n<p><strong>Personal vs. company account \u2014 the decision framework:<\/strong><\/p>\n\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Factor<\/strong><\/td>\n<td><strong>Personal account<\/strong><\/td>\n<td><strong>Company \/ organization account<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Tester gate<\/td>\n<td>Required: 12 testers \u00d7 14 days<\/td>\n<td>Exempt \u2014 can go straight to production<\/td>\n<\/tr>\n<tr>\n<td>Launch timeline<\/td>\n<td>Add 3\u20134 weeks<\/td>\n<td>No delay<\/td>\n<\/tr>\n<tr>\n<td>D-U-N-S number<\/td>\n<td>Not required<\/td>\n<td>Required for Apple; not required for Google Play<\/td>\n<\/tr>\n<tr>\n<td>Developer name on listing<\/td>\n<td>Your real name<\/td>\n<td>Company name<\/td>\n<\/tr>\n<tr>\n<td>Best for<\/td>\n<td>Side projects, solo developers<\/td>\n<td>Agencies, companies, client apps<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n<p>If you are an agency shipping a client&#8217;s Flutter app, or if you have an active business entity, register the Google Play account under the organization from day one. There is no migration path \u2014 a personal account cannot be converted to an organizational account after creation.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Pre-Publishing Checklist for Your Flutter Project<\/strong><\/h2>\n\n<p>Before you touch either store dashboard, these items need to be in order at the Flutter project level.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Set Your Package Name and Version \u2014 Permanently<\/strong><\/h3>\n\n<p>Your application ID (com.company.appname on Android, matching bundle identifier on iOS) is permanent after the first upload. Changing it means submitting as a brand-new app, losing all ratings, reviews, and install history. Decide your own unique application identifier before you publish anything, even to internal testing.<\/p>\n\n<p>In pubspec.yaml, set your version string:<\/p>\n\n<blockquote>\n<p>version: 1.0.0+1<\/p>\n<\/blockquote>\n\n<p>The number before the + is the version name shown to users. The number after is the build number used internally by the stores. Set your version number carefully \u2014 once a version has been uploaded, you cannot upload the same version number again.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>App Icon Requirements<\/strong><\/h3>\n\n<p>Prepare your launcher icon at the correct dimensions for both platforms. The easiest path is the flutter_launcher_icons package, which generates all required sizes from a single source image. If you prefer manual control, follow the Material Design guidelines for Android and Apple&#8217;s Human Interface Guidelines for iOS.<\/p>\n\n<p>Critical iOS-specific rule: the 1024\u00d71024 App Store app icon must be fully opaque. No alpha channel, no transparency. This is one of the most common causes of iOS upload failures. Verify every size in the Xcode Asset Catalog before submitting.<\/p>\n\n<p>For Google Play, you&#8217;ll need:<\/p>\n\n<ul class=\"wp-block-list\">\n<li>App icon: 512\u00d7512 pixels<\/li>\n\n<li>Feature graphic: 1024\u00d7500 pixels<\/li>\n<\/ul>\n\n<h3 class=\"wp-block-heading\"><strong>Privacy Policy URL<\/strong><\/h3>\n\n<p>Both the Google Play Store and the Apple App Store require a publicly accessible privacy policy URL before submission. This is not optional, even for apps that collect no personal data. Prepare this before you open either store dashboard.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Test on Real Devices \u2014 Flutter Release-Mode Failures to Know<\/strong><\/h3>\n\n<p>Debug builds and release builds behave differently in Flutter, and the delta goes beyond &#8220;crashes only appear in release mode.&#8221; There are several Flutter-specific failure modes that only surface after you run flutter build appbundle &#8211;release or flutter build ios &#8211;release:<\/p>\n\n<p><strong>Tree-shaking removes assets you actually need.<\/strong> In release mode, Flutter applies aggressive Dart AOT tree-shaking. If your code references assets or classes indirectly (through string-based lookups, reflection-style patterns, or generated code), tree-shaking may strip them. The debug build works fine because tree-shaking is disabled in JIT mode. Fix: use explicit references and verify the assets block in pubspec.YAML lists everything needed.<\/p>\n\n<p><strong>Dart AOT compilation exposing null-safety issues hidden in JIT debug mode.<\/strong> Debug builds run Dart in JIT (Just-in-Time) mode, which is more forgiving with null checks and type assertions at runtime. Release builds compile to native machine code via AOT (Ahead-of-Time), which enforces strict null-safety and type soundness. Code that runs without errors in debug can throw a null check operator used on a null value in production.<\/p>\n\n<p><strong>Platform channel timeouts on low-end devices.<\/strong> Plugin calls that invoke native platform channels (camera, location, Bluetooth, payments) are synchronous from the Dart side but execute asynchronously on the host platform. On low-end Android devices under memory pressure, these calls can time out in release mode, where there are no debug pauses to mask the delay. Test on the lowest-spec device in your target range, not just a flagship. Recommended baseline Android test devices: Samsung Galaxy A14 or Xiaomi Redmi 12C (entry-level, 3\u20134 GB RAM, widely used in Southeast Asia and Latin America). For iOS, test on the oldest device your deployment target supports \u2014 if your minimum is iOS 16, an iPhone 8 (A11 chip, 2 GB RAM) is the right baseline.<\/p>\n\n<p>Before submitting, install a release APK on physical Android devices and use TestFlight for iOS testing. Thoroughly test your Flutter app on different devices and operating systems to ensure compatibility and that it functions as intended without bugs or crashes.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Google Play Store Requirements for Flutter Apps<\/strong><\/h2>\n\n<h3 class=\"wp-block-heading\"><strong>Developer Account Setup and Costs<\/strong><\/h3>\n\n<p>Create your Google Play Console account at play.google.com\/console. The one-time $25 registration fee applies to both personal and organizational accounts. This account is required to distribute Android apps through the Play Store.<\/p>\n\n<p>When you create a new app, you&#8217;ll choose between &#8220;app&#8221; and &#8220;game,&#8221; select target countries, and set free or paid pricing. These choices can be changed later, but your package name cannot.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Technical Requirements: AAB Format and API Level<\/strong><\/h3>\n\n<p><strong>Android App Bundle is mandatory.<\/strong> New apps published to Google Play since August 2021 must use the .aab format, not a plain APK. Build your release bundle from the project root:<\/p>\n\n<p>flutter build appbundle &#8211;release<\/p>\n\n<p>The output lands at build\/app\/outputs\/bundle\/release\/app-release.aab. You can still use Flutter build APK to create an APK for local device testing or sideloading, but Google Play expects the .aab for new submissions.<\/p>\n\n<p><strong>targetSdk 35 is now mandatory.<\/strong> As of August 31, 2025, all new and updated submissions on Google Play must target API level 35 (Google Play target API requirements). Apps still targeting SDK 33 or 34 are rejected immediately. Check your android\/app\/build.gradle:<\/p>\n\n<pre class=\"wp-block-code\"><code>android {\n\n\u00a0 \u00a0compileSdk 35\n\n\u00a0 \u00a0defaultConfig {\n\n\u00a0 \u00a0 \u00a0 \u00a0targetSdk 35\n\n\u00a0 \u00a0 \u00a0 \u00a0minSdk 21 \u00a0\/\/ or your minimum\n\n\u00a0 \u00a0}\n\n}<\/code><\/pre>\n\n<p><strong>64-bit compliance.<\/strong> A correctly built Flutter app bundle includes both 32-bit and 64-bit Dart code and Flutter runtime compiled artifacts by default. If you&#8217;re using native plugins that ship only armeabi-v7a libraries, you&#8217;ll need to obtain or build 64-bit versions. Use Android Studio&#8217;s APK Analyzer to inspect .so files and confirm arm64-v8a is present.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>App Signing and Keystore Management<\/strong><\/h3>\n\n<p>To publish on the Play Store, you must sign your app with a digital certificate. This involves creating an upload keystore.<\/p>\n\n<blockquote>\n<p>Create the keystore:<\/p>\n<p>\u00a0<\/p>\n\n<p>keytool -genkey -v -keystore upload-keystore.jks \\<\/p>\n<p>\u00a0<\/p>\n\n<p>\u00a0-keyalg RSA -keysize 2048 -validity 10000 \\<\/p>\n<p>\u00a0<\/p>\n\n<p>\u00a0-alias upload<\/p>\n<\/blockquote>\n\n<p>Keep the .jks file private and never commit it to public source control. Store it \u2014 and your key passwords \u2014 in a secure location separate from your codebase. <strong>A lost keystore means permanent inability to update your app.<\/strong> You cannot recover it. You would have to resubmit as a new app and lose all existing ratings, reviews, and install history.<\/p>\n\n<blockquote>\n<p>Create a key.properties file in the android\/ folder:<\/p>\n<p>\u00a0<\/p>\n\n<p>storePassword=yourStorePassword<\/p>\n<p>\u00a0<\/p>\n\n<p>keyPassword=yourKeyPassword<\/p>\n<p>\u00a0<\/p>\n\n<p>keyAlias=upload<\/p>\n<p>\u00a0<\/p>\n\n<p>storeFile=..\/upload-keystore.jks<\/p>\n<\/blockquote>\n\n<p>This is your properties file. Reference it in your Gradle file to configure signing for release builds. Add key.properties to .gitignore.<\/p>\n\n<p>Enable <strong>Play App Signing<\/strong> \u2014 it is strongly recommended. Google manages the final app signing key while you upload with your upload key. If your upload key is ever compromised, Google can rotate it. Without Play App Signing, a compromised key = a compromised app with no recourse.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>The Merged Manifest Permissions Audit<\/strong><\/h3>\n\n<p>Before running flutter build appbundle, audit your merged manifest. After a Flutter build, open the merged output at:<\/p>\n\n<blockquote>\n<p>android\/app\/build\/intermediates\/merged_manifests\/release\/AndroidManifest.xml<\/p>\n<\/blockquote>\n\n<p>Compare every &lt;uses-permission&gt; tag against your Google Play Data Safety form declarations. Any permission that appears in the merged manifest \u2014 even if injected by a pub.dev package \u2014 must be declared in Data Safety. Common offenders include analytics SDKs, ad networks, push notification plugins, and crash reporters.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Google Play Data Safety Form<\/strong><\/h3>\n\n<p>Complete the Data Safety section in Google Play Console before submitting for review. Note: In April 2025, Google reclassified Android ID as a device identifier that must be declared. If your Flutter app uses any advertising or analytics plugin, this likely affects you. Declare the data type, collection purpose, and whether it&#8217;s shared with third parties.<\/p>\n\n<p>Also, complete the app content rating questionnaire, the ads declaration, and the target audience form. Missing any of these blocks in the submission.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Testing Requirements for Personal Developer Accounts<\/strong><\/h3>\n\n<p>If you registered a personal (non-organizational) Google Play Console account, closed testing for personal developer accounts requires at least 12 opted-in testers for 14 continuous days prior to applying for production access.<\/p>\n\n<p>How to reach 12 testers: friends, colleagues, beta community forums, or services like BetaFamily. Each tester must actively opt in via your closed testing link \u2014 passive invites don&#8217;t count. Start this process as early as possible since it runs in parallel with your final QA cycle.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Apple App Store Requirements for Flutter Apps<\/strong><\/h2>\n\n<h3 class=\"wp-block-heading\"><strong>Developer Program and Hardware Requirements<\/strong><\/h3>\n\n<p>An Apple Developer Program enrollment costs $99 per year. Unlike Google&#8217;s one-time fee, this renews annually. If your subscription lapses, your apps are delisted from the App Store until you renew.<\/p>\n\n<p>A macOS computer with Xcode installed is required to build and sign iOS apps. There is no workaround for debugging on a physical iOS device \u2014 that still requires macOS. However, for teams without a Mac (agencies shipping a client&#8217;s Flutter app, for example), <strong>Codemagic<\/strong> provides a cloud-based Mac environment that can compile, sign, and submit iOS apps without a local Mac. This is a common workflow for cross-platform Flutter teams.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Bundle Identifier, Signing, and Capabilities<\/strong><\/h3>\n\n<p>Open the ios\/ folder in Xcode (or open Runner.xcworkspace). Set a unique bundle identifier matching your Apple Developer portal registration \u2014 for example, com.company.appname. An App Store listing must be created via App Store Connect and a bundle ID registered before you can upload a build.<\/p>\n\n<p>Enable automatic signing if you&#8217;re working solo, or configure manual provisioning profiles for team or CI\/CD setups. Under the &#8220;Signing &amp; Capabilities&#8221; tab, enable any necessary capabilities for your Flutter app \u2014 Sign in with Apple, Push Notifications, HealthKit, and so on. Capabilities that are declared in your code but not enabled in Xcode&#8217;s entitlements are a common rejection source.<\/p>\n\n<p><strong>Do not change the bundle identifier after publishing.<\/strong> Like Android&#8217;s application ID, changing it creates a new app entry.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Apple Privacy Manifest: A Hard Upload Block<\/strong><\/h3>\n\n<p>Since February 12, 2025, every iOS app upload that doesn&#8217;t include a valid PrivacyInfo.xcprivacy file is rejected at the point of upload \u2014 before it even reaches a human reviewer.<\/p>\n\n<p>Your Privacy Manifest must cover:<\/p>\n\n<ol class=\"wp-block-list\">\n<li><strong>The Flutter framework itself<\/strong> \u2014 Flutter appears on Apple&#8217;s list of required-reason APIs (it uses UserDefaults for preferences and file timestamp APIs). Your manifest must include the correct NSPrivacyAccessedAPITypes entries for these.<\/li>\n\n<li><strong>Every pub.dev plugin on Apple&#8217;s required-SDK list<\/strong> \u2014 check each plugin&#8217;s repository for its own PrivacyInfo.xcprivacy. Well-maintained packages include one; older or unmaintained ones may not, and you&#8217;ll need to add coverage manually or switch packages.<\/li>\n<\/ol>\n\n<p>Run flutter build ios &#8211;release and verify the archive contains a valid Privacy Manifest before attempting an upload via Xcode or the Transporter tool.<\/p>\n\n<p>Here is a minimal PrivacyInfo.xcprivacy template covering the Flutter framework&#8217;s required APIs. Add this file to your Xcode project under the Runner target (not a framework target):<\/p>\n\n<pre class=\"wp-block-code\"><code>&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n\n&lt;!DOCTYPE plist PUBLIC \"-\/\/Apple\/\/DTD PLIST 1.0\/\/EN\"\n\n  \"http:\/\/www.apple.com\/DTDs\/PropertyList-1.0.dtd\"&gt;\n\n&lt;plist version=\"1.0\"&gt;\n\n&lt;dict&gt;\n\n  &lt;key&gt;NSPrivacyAccessedAPITypes&lt;\/key&gt;\n\n  &lt;array&gt;\n\n    &lt;!-- Flutter framework: UserDefaults (NSUserDefaults) --&gt;\n\n    &lt;dict&gt;\n\n      &lt;key&gt;NSPrivacyAccessedAPIType&lt;\/key&gt;\n\n      &lt;string&gt;NSPrivacyAccessedAPICategoryUserDefaults&lt;\/string&gt;\n\n      &lt;key&gt;NSPrivacyAccessedAPITypeReasons&lt;\/key&gt;\n\n      &lt;array&gt;\n\n        &lt;string&gt;CA92.1&lt;\/string&gt;\n\n      &lt;\/array&gt;\n\n    &lt;\/dict&gt;\n\n    &lt;!-- Flutter framework: file timestamp APIs --&gt;\n\n    &lt;dict&gt;\n\n      &lt;key&gt;NSPrivacyAccessedAPIType&lt;\/key&gt;\n\n      &lt;string&gt;NSPrivacyAccessedAPICategoryFileTimestamp&lt;\/string&gt;\n\n      &lt;key&gt;NSPrivacyAccessedAPITypeReasons&lt;\/key&gt;\n\n      &lt;array&gt;\n\n        &lt;string&gt;C617.1&lt;\/string&gt;\n\n      &lt;\/array&gt;\n\n    &lt;\/dict&gt;\n\n  &lt;\/array&gt;\n\n  &lt;key&gt;NSPrivacyCollectedDataTypes&lt;\/key&gt;\n\n  &lt;array\/&gt;\n\n  &lt;key&gt;NSPrivacyTracking&lt;\/key&gt;\n\n  &lt;false\/&gt;\n\n&lt;\/dict&gt;\n\n&lt;\/plist&gt;<\/code><\/pre>\n\n<p>If your app uses plugins that also appear on Apple&#8217;s required-SDK list (common ones include path_provider, shared_preferences, and firebase_core), check each plugin&#8217;s repository for its own PrivacyInfo.xcprivacy. Well-maintained plugins bundle this file automatically from Flutter 3.18+. For plugins that don&#8217;t, you either add their required-reason entries to your app-level manifest above, or migrate to a maintained alternative.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Info.plist Permission Strings<\/strong><\/h3>\n\n<p>Every permission your app requests needs a clear usage description string in Info.plist. Camera, microphone, photo library, location (always vs. when-in-use), Bluetooth, contacts, health data \u2014 each requires its own NS*UsageDescription key with a plain-language explanation of why the app needs it.<\/p>\n\n<p>Vague strings like &#8220;This app needs camera access&#8221; are a common rejection reason. Apple&#8217;s reviewers check that the stated reason matches the actual app functionality. Review the app store guidelines to ensure your descriptions are specific and accurate.<\/p>\n\n<p>If your Flutter app or any of its plugins use App Tracking Transparency (advertising identifiers, cross-app tracking), you must implement the ATT prompt and declare it in App Store Connect&#8217;s privacy nutrition labels.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Build and Upload Process<\/strong><\/h3>\n\n<p>Build the iOS release:<\/p>\n\n<p>flutter build ios &#8211;release<\/p>\n\n<p>Open the iOS workspace in Xcode, select &#8220;Any iOS Device (arm64)&#8221; as the target, then go to <strong>Product \u2192 Archive<\/strong>. Once the archive is complete, use the Organizer window to upload via Xcode&#8217;s distribution workflow. Uploading an app using Xcode or the Transporter tool is necessary for iOS deployment.<\/p>\n\n<p>After upload, connect the build to your app version in App Store Connect, complete the App Review information (including test account credentials if your app requires login), fill in the privacy nutrition labels, and submit for review.<\/p>\n<h4 class=\"PDq2pG_selectionAnchorContainer\" data-start=\"707\" data-end=\"779\">Watch: How to Build, Sign, and Publish a Flutter App on Google Play<\/h4>\n<p data-start=\"781\" data-end=\"1185\">Before submitting your Flutter application to Google Play, it&#8217;s helpful to see the complete publishing workflow in action. This step-by-step tutorial demonstrates how to generate a release build, configure app signing, create an Android App Bundle (AAB), and publish your application through the Google Play Console\u2014complementing the publishing checklist and technical requirements covered in this guide.<\/p>\n<p data-start=\"1187\" data-end=\"1209\">\u00a0<\/p>\n\n<p><iframe title=\"YouTube video player\" src=\"https:\/\/www.youtube.com\/embed\/adt9A8125S4?si=Pdp85fvh8EktgGJP\" width=\"560\" height=\"315\" frameborder=\"0\" allowfullscreen=\"allowfullscreen\"><\/iframe><\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Store Listing Requirements: Both Platforms<\/strong><\/h2>\n\n<p>Both stores require a complete and truthful app store listing. The store listing is not just marketing \u2014 it must accurately reflect real app functionality. Misleading screenshots or descriptions are a common cause of rejection.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Screenshots and Visual Assets<\/strong><\/h3>\n\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Asset<\/strong><\/td>\n<td><strong>Google Play<\/strong><\/td>\n<td><strong>Apple App Store<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Phone screenshots<\/td>\n<td>Min 2, max 8<\/td>\n<td>Min 1, max 10 per device size<\/td>\n<\/tr>\n<tr>\n<td>Tablet screenshots<\/td>\n<td>Recommended<\/td>\n<td>Required for iPad if supporting it<\/td>\n<\/tr>\n<tr>\n<td>Feature graphic<\/td>\n<td>1024\u00d7500 px (required)<\/td>\n<td>Not applicable<\/td>\n<\/tr>\n<tr>\n<td>App preview video<\/td>\n<td>Optional<\/td>\n<td>Optional (up to 30 seconds)<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n<p>Use real screenshots from the app, not mockups that don&#8217;t match the actual UI. Promotional videos help users understand functionality, but must show the real product. Keep branding consistent across Android and iOS store listings.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Text, Metadata, and Category<\/strong><\/h3>\n\n<p>Character limits matter and are a common oversight. Here are the hard limits for both stores:<\/p>\n\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Field<\/strong><\/td>\n<td><strong>Google Play<\/strong><\/td>\n<td><strong>Apple App Store<\/strong><\/td>\n<\/tr>\n<tr>\n<td>App title<\/td>\n<td>30 characters<\/td>\n<td>30 characters<\/td>\n<\/tr>\n<tr>\n<td>Short description \/ Subtitle<\/td>\n<td>80 characters<\/td>\n<td>30 characters<\/td>\n<\/tr>\n<tr>\n<td>Full description<\/td>\n<td>4,000 characters<\/td>\n<td>4,000 characters<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n<p>Choose a clear app name that works across both the Apple App Store and Google Play Store. The 30-character title limit is tight \u2014 prioritize the primary value over branding. Use the Google Play short description (80 chars) and Apple App Store subtitle (30 chars) to lead with the core benefit: what the app does and for whom, not features.<\/p>\n\n<p>To illustrate the difference between weak and strong: a weak short description reads &#8220;A powerful tool for managing your tasks efficiently&#8221; \u2014 a generic filler that tells users nothing. A strong one reads &#8220;Offline task manager \u2014 no account needed&#8221; \u2014 specific, differentiating, and scannable in 80 characters.<\/p>\n\n<p>Choose the right category for your app to help users find it easily. Miscategorized apps are harder to discover and can be flagged during review. Answer the age and content rating questionnaire honestly \u2014 wrong content rating is a flagged violation, not just an inconvenience.<\/p>\n\n<p>Provide clear and accurate information about your app during submission, including an up-to-date description that reflects its functionality. Include a privacy policy URL in the appropriate fields on both platforms.<\/p>\n\n<p><strong>Flutter-specific screenshot tip:<\/strong> Use the integration_test package combined with the screenshots package to auto-generate store screenshots across device sizes directly from your test suite. This eliminates manual screenshot capture across multiple simulators and ensures screenshots always reflect the current build \u2014 a significant time-saver for apps targeting multiple form factors.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Common Flutter Publishing Issues and Solutions<\/strong><\/h2>\n\n<h3 class=\"wp-block-heading\"><strong>Build and Signing Problems<\/strong><\/h3>\n\n<p><strong>&#8220;Keystore not found&#8221; on CI\/CD.<\/strong> Your key.properties file references a local path. In automated environments, pass the keystore location and credentials as environment variables and generate the file at build time.<\/p>\n\n<p><strong>iOS archive fails with &#8220;No signing certificate.&#8221;<\/strong> Re-run automatic signing in Xcode, or manually import the Distribution certificate and provisioning profile from the Apple Developer portal. Certificates expire \u2014 check the expiry date in Keychain Access.<\/p>\n\n<p><strong>Flutter build appbundle fails after upgrading Flutter.<\/strong> Run flutter clean and flutter pub get before rebuilding. Plugin version conflicts after SDK upgrades are a frequent cause.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Store Rejection Reasons and Fixes<\/strong><\/h3>\n\n<p><strong>Google Play: &#8220;Your app contains permissions not declared in Data Safety.&#8221;<\/strong> Run the merged manifest audit described above. Add the undeclared permissions to your Data Safety form and resubmit.<\/p>\n\n<p><strong>Apple: &#8220;Missing Privacy Manifest.&#8221;<\/strong> Add PrivacyInfo.xcprivacy to your Xcode project under the Runner target. Verify it covers Flutter&#8217;s required APIs and any listed plugins.<\/p>\n\n<p><strong>Apple: &#8220;App uses private or undocumented APIs.&#8221;<\/strong> Usually caused by a pub.dev plugin using low-level iOS APIs. Check the plugin&#8217;s GitHub issues for App Store compatibility status. Switch to an alternative if the maintainer hasn&#8217;t resolved it.<\/p>\n\n<p><strong>Google Play: targetSdk below 35.<\/strong> Update targetSdk and compileSdk to 35 in build.gradle. Run flutter build appbundle &#8211;release again and upload the new AAB.<\/p>\n\n<p><strong>Both stores: Debug build submitted by mistake.<\/strong> Release builds are required. Confirm with flutter build appbundle &#8211;release or flutter build ios &#8211;release. A debug build is significantly larger and contains debugging artifacts that stores will flag.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>Publishing Timeline and Ongoing Obligations<\/strong><\/h2>\n\n<h3 class=\"wp-block-heading\"><strong>Typical Review Times<\/strong><\/h3>\n\n<figure class=\"wp-block-table\">\n<table class=\"has-fixed-layout\">\n<tbody>\n<tr>\n<td><strong>Platform<\/strong><\/td>\n<td><strong>First submission<\/strong><\/td>\n<td><strong>Update<\/strong><\/td>\n<\/tr>\n<tr>\n<td>Google Play<\/td>\n<td>1\u20133 days (new accounts may take longer)<\/td>\n<td>Hours to 1\u20132 days<\/td>\n<\/tr>\n<tr>\n<td>Apple App Store<\/td>\n<td>24\u201348 hours (standard)<\/td>\n<td>24\u201348 hours<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/figure>\n\n<p>New developer accounts on both platforms typically experience longer initial review times. Factor this into your launch planning, and factor in an additional three to four weeks if you&#8217;re on a personal Google Play account subject to the tester gate.<\/p>\n\n<h3 class=\"wp-block-heading\"><strong>Post-Publishing Requirements<\/strong><\/h3>\n\n<p>App publishing is not a one-time process. Both Google and Apple update their policies and technical requirements regularly.<\/p>\n\n<p><strong>Annual Apple renewal.<\/strong> Your $99 Apple Developer Program membership renews annually. Set a calendar reminder. A lapsed membership delists your app immediately.<\/p>\n\n<p><strong>API level update cadence.<\/strong> Google typically mandates a new minimum targetSdk each year for existing apps in addition to new submissions. Monitor the Google Play policy timeline and budget development time for SDK updates, even if you&#8217;re not adding features.<\/p>\n\n<p><strong>Flutter SDK and plugin updates.<\/strong> The Dart and Flutter teams shipped eight stable releases in 2025, on a quarterly cadence. Staying within one or two major versions helps ensure plugin compatibility and continued store compliance. Audit Flutter pub outdated regularly.<\/p>\n\n<p><strong>Monitor crash reports and user feedback.<\/strong> Use Firebase Crashlytics or a similar tool from launch day. If Google or Apple flags a stability, privacy, or policy issue via email, respond quickly with a fixed release \u2014 unresolved flags can escalate to app suspension.<\/p>\n\n<h2 class=\"wp-block-heading\"><strong>The Flutter App Publishing Requirements Checklist<\/strong><\/h2>\n\n<p>Use this before your first submission on either platform.<\/p>\n\n<p><strong>Universal (both stores)<\/strong><\/p>\n\n<ul class=\"wp-block-list\">\n<li>[ ] Unique application ID \/ bundle identifier set and finalized<\/li>\n\n<li>[ ] pubspec.yaml version incremented correctly<\/li>\n\n<li>[ ] Privacy policy URL is live and accessible<\/li>\n\n<li>[ ] App icon exported at all required sizes (no alpha channel on iOS)<\/li>\n\n<li>[ ] Store screenshots captured from a real device, correct dimensions<\/li>\n\n<li>[ ] App description is accurate and reflects current functionality<\/li>\n\n<li>[ ] Right category selected<\/li>\n\n<li>[ ] Content rating questionnaire completed honestly<\/li>\n\n<li>[ ] Release build tested on physical devices (not only emulator)<\/li>\n<\/ul>\n\n<p><strong>Android \/ Google Play<\/strong><\/p>\n\n<ul class=\"wp-block-list\">\n<li>[ ] Google Play Console account registered<\/li>\n\n<li>[ ] targetSdk 35 set in build.gradle<\/li>\n\n<li>[ ] Upload keystore created and securely backed up (not in source control)<\/li>\n\n<li>[ ] key.properties file configured and in .gitignore<\/li>\n\n<li>[ ] Play App Signing enrolled<\/li>\n\n<li>[ ] Merged manifest audited \u2014 all injected permissions declared in Data Safety<\/li>\n\n<li>[ ] Data Safety form completed (including Android ID if using analytics\/ads)<\/li>\n\n<li>[ ] AAB built with flutter build appbundle &#8211;release<\/li>\n\n<li>[ ] 64-bit compliance verified (arm64-v8a present in AAB)<\/li>\n\n<li>[ ] For personal accounts: 12 testers enrolled in closed testing for 14+ days<\/li>\n<\/ul>\n\n<p><strong>iOS \/ Apple App Store<\/strong><\/p>\n\n<ul class=\"wp-block-list\">\n<li>[ ] Apple Developer Program membership active ($99\/year)<\/li>\n\n<li>[ ] macOS + Xcode environment available (or Codemagic configured)<\/li>\n\n<li>[ ] Bundle identifier registered in Apple Developer portal and App Store Connect<\/li>\n\n<li>[ ] Signing certificates and provisioning profiles are valid<\/li>\n\n<li>[ ] PrivacyInfo.xcprivacy present, covering Flutter framework + all listed plugins<\/li>\n\n<li>[ ] All Info.plist permission strings present and specific<\/li>\n\n<li>[ ] Capabilities in Xcode Signing &amp; Capabilities tab match app entitlements<\/li>\n\n<li>[ ] iOS archive built and uploaded via Xcode or Transporter<\/li>\n\n<li>[ ] Privacy nutrition labels completed in App Store Connect<\/li>\n\n<li>[ ] Test account credentials provided if app requires login<\/li>\n<\/ul>\n<p>\u00a0<\/p>\n<h2 class=\"wp-block-heading\"><strong>FAQ&#8217;s<\/strong><\/h2>\n<p><\/p>\n<p><\/p>\n<p><\/p>\n<p><\/p>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-0507a09 elementor-widget elementor-widget-shortcode\" data-id=\"0507a09\" data-element_type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><style>#sp-ea-30007 .spcollapsing { height: 0; overflow: hidden; transition-property: height;transition-duration: 300ms;}#sp-ea-30007.sp-easy-accordion>.sp-ea-single {margin-bottom: 10px; border: 1px solid #e2e2e2; }#sp-ea-30007.sp-easy-accordion>.sp-ea-single>.ea-header a {color: #444;}#sp-ea-30007.sp-easy-accordion>.sp-ea-single>.sp-collapse>.ea-body {background: #fff; color: #444;}#sp-ea-30007.sp-easy-accordion>.sp-ea-single {background: #eee;}#sp-ea-30007.sp-easy-accordion>.sp-ea-single>.ea-header a .ea-expand-icon { float: left; color: #444;font-size: 16px;}<\/style><div id=\"sp_easy_accordion-1784101107\"><div id=\"sp-ea-30007\" class=\"sp-ea-one sp-easy-accordion\" data-ea-active=\"ea-click\" data-ea-mode=\"vertical\" data-preloader=\"\" data-scroll-active-item=\"\" data-offset-to-scroll=\"0\"><div class=\"ea-card ea-expand sp-ea-single\"><h3 class=\"ea-header\"><a class=\"collapsed\" id=\"ea-header-300070\" role=\"button\" data-sptoggle=\"spcollapse\" data-sptarget=\"#collapse300070\" aria-controls=\"collapse300070\" href=\"#\" aria-expanded=\"true\" tabindex=\"0\"><i aria-hidden=\"true\" role=\"presentation\" class=\"ea-expand-icon eap-icon-ea-expand-minus\"><\/i> How do I publish my Flutter app?<\/a><\/h3><div class=\"sp-collapse spcollapse collapsed show\" id=\"collapse300070\" data-parent=\"#sp-ea-30007\" role=\"region\" aria-labelledby=\"ea-header-300070\"> <div class=\"ea-body\"><p>The short answer: comply first, then build, then submit. Set up your developer accounts (Google Play Console for $25, Apple Developer Program for $99\/year), configure app signing (upload keystore for Android, certificates and provisioning profiles for iOS), build release binaries (flutter build appbundle --release for Android, flutter build ios --release for iOS), complete all store metadata and privacy forms, and submit. On Google Play with a personal account, expect a 14-day closed testing period with 12 testers before production access is granted.<\/p><\/div><\/div><\/div><div class=\"ea-card sp-ea-single\"><h3 class=\"ea-header\"><a class=\"collapsed\" id=\"ea-header-300071\" role=\"button\" data-sptoggle=\"spcollapse\" data-sptarget=\"#collapse300071\" aria-controls=\"collapse300071\" href=\"#\" aria-expanded=\"false\" tabindex=\"0\"><i aria-hidden=\"true\" role=\"presentation\" class=\"ea-expand-icon eap-icon-ea-expand-plus\"><\/i> How do I handle rejected builds on resubmission?<\/a><\/h3><div class=\"sp-collapse spcollapse \" id=\"collapse300071\" data-parent=\"#sp-ea-30007\" role=\"region\" aria-labelledby=\"ea-header-300071\"> <div class=\"ea-body\"><p>Read the rejection message in full before making any changes. Both stores give you a specific policy or technical reason, and addressing the wrong issue wastes time. For Google Play, log in to Google Play Console, open the release that was rejected, and check the policy violation or review message. Fix only the flagged issue, increment your build number, rebuild the release, and upload a new AAB. Do not make unrelated changes alongside a fix \u2014 reviewers compare the new build against the rejected one, and scope creep can trigger additional flags. For App Store rejections, respond directly in App Store Connect via the Resolution Center. If the rejection reason is ambiguous, use the \"Contact Us\" option to request clarification before resubmitting.<\/p><\/div><\/div><\/div><div class=\"ea-card sp-ea-single\"><h3 class=\"ea-header\"><a class=\"collapsed\" id=\"ea-header-300072\" role=\"button\" data-sptoggle=\"spcollapse\" data-sptarget=\"#collapse300072\" aria-controls=\"collapse300072\" href=\"#\" aria-expanded=\"false\" tabindex=\"0\"><i aria-hidden=\"true\" role=\"presentation\" class=\"ea-expand-icon eap-icon-ea-expand-plus\"><\/i> Can I publish to both stores simultaneously from one CI pipeline?<\/a><\/h3><div class=\"sp-collapse spcollapse \" id=\"collapse300072\" data-parent=\"#sp-ea-30007\" role=\"region\" aria-labelledby=\"ea-header-300072\"> <div class=\"ea-body\"><p>Yes, and it is the recommended workflow for Flutter teams shipping to both platforms. Tools like Codemagic, Fastlane with Flutter, or GitHub Actions with separate Android and iOS lanes can trigger parallel builds from a single commit. A typical multi-platform pipeline runs flutter build appbundle --release for Android and flutter build ios --release (on a macOS agent) for iOS, then uploads the AAB to Google Play Console via the Play Developer API and the IPA to App Store Connect via the Transporter CLI or App Store Connect API. Both uploads can be triggered in the same pipeline run, making simultaneous store submissions practical for teams managing frequent release cycles. The main constraint is the macOS build agent for iOS \u2014 this is where Codemagic's cloud Mac environment, or a self-hosted Mac runner, becomes essential for non-Mac development teams.<\/p><\/div><\/div><\/div><div class=\"ea-card sp-ea-single\"><h3 class=\"ea-header\"><a class=\"collapsed\" id=\"ea-header-300073\" role=\"button\" data-sptoggle=\"spcollapse\" data-sptarget=\"#collapse300073\" aria-controls=\"collapse300073\" href=\"#\" aria-expanded=\"false\" tabindex=\"0\"><i aria-hidden=\"true\" role=\"presentation\" class=\"ea-expand-icon eap-icon-ea-expand-plus\"><\/i> What are the prerequisites for Flutter app development?<\/a><\/h3><div class=\"sp-collapse spcollapse \" id=\"collapse300073\" data-parent=\"#sp-ea-30007\" role=\"region\" aria-labelledby=\"ea-header-300073\"> <div class=\"ea-body\"><p>Before you reach the publishing stage: a working Flutter SDK installation, Android Studio (for Android development and the AVD emulator), and Xcode on macOS (required for iOS builds). For publishing specifically, you need a Google Play Console account ($25), an Apple Developer Program membership ($99\/year, requires a macOS machine), app signing credentials for both platforms, and all store metadata \u2014 icons, screenshots, privacy policy URL, descriptions, and completed compliance forms.<\/p><\/div><\/div><\/div><\/div><\/div><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Most Flutter developers hit the same wall: they finish the app, head straight to the store dashboard, and then discover a chain of compliance gates they didn&#8217;t know existed. The right sequence is not built \u2192 submit. It&#8217;s comply \u2192 build \u2192 submit. Get that order wrong, and you&#8217;re looking at rejections, a delayed launch, [&hellip;]<\/p>\n","protected":false},"author":10,"featured_media":30018,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[446],"tags":[],"class_list":["post-28783","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-flutter"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/posts\/28783","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/users\/10"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/comments?post=28783"}],"version-history":[{"count":19,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/posts\/28783\/revisions"}],"predecessor-version":[{"id":30019,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/posts\/28783\/revisions\/30019"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/media\/30018"}],"wp:attachment":[{"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/media?parent=28783"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/categories?post=28783"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tftus.com\/blog\/wp-json\/wp\/v2\/tags?post=28783"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}