Flutter: Multi-Platform Publishing and Store Review
📋 Prerequisites: You should have mastered the following first
- Lesson 24: CI/CD Automation
1. What You Will Learn
2. A Real Story of a Rejected Submission
3. Multi-Platform Publishing Process
4. Android Publishing
5. iOS Publishing
6. Web Publishing
7. Privacy Compliance
8. Complete Example: ShopApp Publishing Checklist
9. ShopApp Release Checklist v1.2.3
After Bob prepared using the checklist, both App Store and Google Play passed on the first submission, and the Web version went live within 1 hour.
graph TD
REL[Release Process] --> ANDROID2[Android AAB]
REL --> IOS2[iOS IPA]
REL --> WEB2[Web PWA]
ANDROID2 --> PC[Play Console]
IOS2 --> ASC[App Store Connect]
WEB2 --> FH[Firebase Hosting / Vercel]
PC --> REVIEW1[Google Review]
ASC --> REVIEW2[Apple Review]
REVIEW1 --> LIVE1[Live on Google Play]
REVIEW2 --> LIVE2[Live on App Store]
FH --> LIVE3[Live on Web]
(1) Signing and Build
▶ Example: Generate Keystore
BASH
# Generate signing keystore
keytool -genkey -v -keystore shopapp-release.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias shopapp
# Verify keystore
keytool -list -v -keystore shopapp-release.jks -alias shopapp
build.gradle Signing Configuration
GROOVY
// android/app/build.gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
defaultConfig {
applicationId "com.shopapp.app"
minSdkVersion 21
targetSdkVersion 34
versionCode 42
versionName "1.2.3"
}
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['keyFile'] ? file(keystoreProperties['keyFile']) : null
storePassword keystoreProperties['keyPassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Build and Publish
BASH
# Build release AAB (Android App Bundle)
flutter build appbundle --release
# Output: build/app/outputs/bundle/release/app-release.aab
# Build release APK (for testing)
flutter build apk --release
# Upload to Play Console manually or via CI/CD
(2) Play Console Launch Checklist
| Item | Requirement |
|---|---|
| Application ID | Unique package name com.shopapp.app |
| AAB file | Signed release bundle |
| Icon | 512x512 PNG |
| Screenshots | 2-8 each for phone + 7-inch tablet |
| Privacy Policy URL | Must be accessible |
| Content rating | Complete IARC questionnaire |
| Target audience | Select age range |
(1) Apple Developer Configuration
| Step | Action |
|---|---|
| 1. Register | Apple Developer Program ($99/year) |
| 2. Certificate | Create Distribution Certificate |
| 3. Profile | Create App Store Provisioning Profile |
| 4. App ID | Register Bundle Identifier |
| 5. App Record | Create app in App Store Connect |
Xcode Build Configuration
XML
<!-- ios/Runner/Info.plist -->
`<key>`CFBundleShortVersionString</key>
`<string>`1.2.3</string>
`<key>`CFBundleVersion</key>
`<string>`42</string>
`<key>`NSAppTransportSecurity</key>
`<dict>`
`<key>`NSAllowsArbitraryLoads</key>
`<false/>`
`</dict>`
`<key>`NSUserTrackingUsageDescription</key>
`<string>`We use tracking to personalize your shopping experience.</string>
Build and Upload
BASH
# Build IPA
flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist
# Output: build/ios/ipa/shopapp.ipa
# Upload via Xcode or command line
xcrun altool --upload-app \
--type ios \
--file build/ios/ipa/shopapp.ipa \
--apiKey YOUR_API_KEY \
--apiIssuer YOUR_ISSUER_ID
# Or via Transporter app
(2) App Store Connect Launch Checklist
| Bundle ID | Must match Xcode project | | IPA file | Release build | | Screenshots | 6.7"/6.5"/5.5" three sizes | | Preview video | Optional, ≤30s | | Description | ≤4000 characters |
▶ Example: Apple Privacy List Compliance
DART
import 'package:flutter/material.dart';
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
// Dependencies: app_tracking_transparency: ^0.2.0, firebase_analytics: ^10.0.0 (add to dependencies)
Future<void> requestTrackingPermission() async {
final status = await AppTrackingTransparency.requestTrackingAuthorization();
if (status == TrackingStatus.authorized) {
await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(true);
} else {
await FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(false);
}
}
Output:
TEXT
App Tracking Transparency dialog displayed
Status: authorized (or denied)
Analytics collection enabled/disabled accordingly
(1) Compliance Requirements Comparison
| Platform | Required Items |
|---|---|
| Google Play | Privacy Policy / Data Safety Form / Content Rating |
| App Store | Privacy Policy / ATT / Encryption Compliance Declaration |
| Web (PWA) | GDPR Cookie Consent / Privacy Policy |
(2) Privacy Compliance Checklist
Pre-release Checks
- [ ] Privacy policy page deployed and link accessible
- [ ] Data collection declarations are clear (purpose, retention period, third-party sharing)
- [ ] Users can request data export and deletion
- [ ] Cookie Banner / ATT popup implemented
- [ ] Special protections for users under 13 (COPPA)
- [ ] App Store data type labels checked
- [ ] Google Play Data Safety Form submitted
- [ ] GDPR / CCPA compliance review passed
Three-Platform Publishing Checklist
(1) Android
- [ ] Application ID is unique
- [ ] Keystore signing verified
- [ ] AAB file ≤150MB
- [ ] Privacy Policy URL accessible
- [ ] contentRating questionnaire completed
- [ ] Target API Level 34
- [ ] Test AAB on device via
bundletool - [ ] Upload AAB to Play Console
- [ ] Fill store listing (screenshots, description, privacy policy)
- [ ] Start rollout to 10% → 50% → 100%
(2) iOS
- [ ] Distribution certificate valid
- [ ] Provisioning profile matches Bundle ID
- [ ] Build IPA:
flutter build ipa --release - [ ] Upload IPA via Transporter
- [ ] Fill App Store Connect listing
- [ ] Submit for review (allow 24-48 hours)
(3) Web
- [ ] Build:
flutter build web --wasm - [ ] Test in Chrome/Safari/Firefox
- [ ] Deploy to Firebase Hosting
- [ ] Verify URL routing works
- [ ] Test PWA install prompt
(4) Post-release
- [ ] Create GitHub Release with notes
- [ ] Notify team via Slack
- [ ] Monitor Crashlytics for 24 hours
- [ ] Monitor user reviews
❓ FAQ
Q What's the difference between AAB and APK?
A AAB (Android App Bundle) is the upload format — Google Play automatically generates optimized APKs from it. AABs are smaller because they only include resources needed by the device.
Q How long does App Store review usually take?
A First submissions typically take 24-48 hours, updates usually 12-24 hours. Re-submissions after rejection require another wait.
Q How should I manage versionCode and versionName?
A versionCode must be an incrementing integer (+1 for each release); versionName is user-visible (e.g., 1.2.3). Use SemVer conventions.
Q Does the Web version need ICP filing?
A Servers in mainland China require ICP filing. Overseas servers (Firebase/Vercel) do not.
Q What does GDPR compliance require?
A 1) Show privacy consent on first launch; 2) Provide a data deletion option; 3) Privacy policy link; 4) Minimize data collection; 5) Users can revoke consent.
Q What should I do if the App Store rejects my submission?
A Carefully read the rejection reason, fix the issues, and resubmit. Common reasons: missing privacy policy, ATT popup not shown, crashes, content policy violations.
📖 Summary
(1) Example: Version Number Release Cadence
TEXT
1.0.0 → MVP launch (first release)
1.0.1 → Urgent bug fix (patch)
1.1.0 → New feature (minor)
1.2.3 → ShopApp release (minor + patch)
2.0.0 → Major refactoring (major)
Output:
TEXT
SemVer: MAJOR.MINOR.PATCH
- MAJOR breaking changes
- MINOR compatible additions
- PATCH compatible fixes
(2) Example: Firebase Crashlytics Integration
DART
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'dart:ui' show PlatformDispatcher;
// Dependencies: firebase_core: ^2.0.0, firebase_crashlytics: ^3.0.0 (add to dependencies)
// ShopApp comes from project lib/app.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FlutterError.onError = (details) {
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
};
PlatformDispatcher.instance.onError = (error, stack) {
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
return true;
};
runApp(const ShopApp());
}
Output:
TEXT
Crashlytics initialized
Fatal errors auto-reported
Non-fatal errors auto-reported
(3) Example: Release Monitoring Dashboard
BASH
# Firebase Crashlytics
firebase functions:log --only publishMonitor
# Expected output:
# {"status":"ok","version":"1.2.3","crashRate":"0.05%"}
Output:
TEXT
Crash rate after rollout:
- 1h: 0.05% (baseline 0.10%)
- 24h: 0.04% (healthy)
- ANR rate: 0.01% (acceptable)
📝 Exercises
- Basic (Difficulty ⭐): Generate an Android Keystore for ShopApp, configure build.gradle signing, and successfully build a release AAB.
- Intermediate (Difficulty ⭐⭐): Complete ShopApp dual-platform publishing preparation — Android AAB signing + iOS certificate configuration, and write a multi-platform publishing checklist.
- Challenge (Difficulty ⭐⭐⭐): Implement the full ShopApp cross-platform publishing process (Android + iOS + Web), including privacy compliance declarations, multi-channel screenshot generation, and App Store description writing.