← Articles

Deploying Flutter apps: a production checklist

By Ann Tech · 19 June 2026

Shipping a Flutter app to the App Store and Play Store involves more than running flutter build. Here is the full checklist — organised by phase — to make sure nothing is missed.

Phase 1: Build configuration

  • Version number and build number updated in pubspec.yaml
  • Using --flavor production and -t lib/main.dart for the production build
  • --obfuscate and --split-debug-info=build/debug-info enabled
  • --tree-shake-icons enabled to remove unused Material icons
  • No debug flags or dev tools in production build (kDebugMode guards checked)
  • Production Firebase project connected (not dev/staging)
  • Production API base URL configured via --dart-define from CI secrets
flutter build appbundle \
  --flavor production \
  -t lib/main.dart \
  --obfuscate \
  --split-debug-info=build/debug-info \
  --tree-shake-icons \
  --dart-define=ENVIRONMENT=production \
  --dart-define=API_BASE_URL=$PROD_API_URL

Phase 2: Android signing and Play Store

  • Release keystore exists and is backed up securely (not just on one developer's laptop)
  • build.gradle.kts references signing config from environment variables (not hardcoded)
  • App Bundle (.aab) uploaded, not APK — Play Store requires AAB
  • Target SDK version is current (Google requires targeting within ~1 year of latest)
  • Permissions in AndroidManifest.xml match what the app actually uses
  • App name, short description, full description updated in Play Console
  • Screenshots uploaded for phone, tablet, and Chromebook (if applicable)
  • Privacy policy URL added to Play Console
  • Content rating questionnaire completed

Phase 3: iOS signing and App Store

  • Provisioning profile is App Store distribution type (not AdHoc or Development)
  • Bundle ID matches App Store Connect
  • Info.plist has usage description strings for every permission requested (NSCameraUsageDescription, etc.)
  • ExportOptions.plist specifies method: app-store and correct team ID
  • App version and build number incremented in Xcode / pubspec.yaml
  • Privacy manifest (PrivacyInfo.xcprivacy) added if using required reason APIs
  • App Store Connect metadata updated: screenshots, description, keywords, what's new
  • Age rating set correctly

Phase 4: Monitoring and crash reporting

  • Firebase Crashlytics (or Sentry) initialised in main() with the uncaught error handler
  • Debug symbols uploaded to Crashlytics / Sentry after build (upload-symbols script)
  • Performance monitoring enabled (Firebase Performance or custom)
  • Analytics events verified with Firebase DebugView before shipping
void main() async {
  FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
  PlatformDispatcher.instance.onError = (error, stack) {
    FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
    return true;
  };
  await Firebase.initializeApp();
  runApp(const MyApp());
}

Phase 5: Pre-release testing

  • TestFlight / internal testing track tested by QA team
  • Critical user flows tested on a real device (not simulator)
  • Dark mode tested
  • Accessibility: TalkBack/VoiceOver passes through main flows
  • Offline behaviour tested (airplane mode)
  • Deep links tested from external app and browser
  • Push notifications tested end-to-end (sent from server, received in all app states)
  • App size measured with --analyze-size and within acceptable range

Phase 6: Rollout strategy

  • Phased rollout configured (Play Store: start at 10%; App Store: phased release)
  • Rollout monitoring plan: watch crash rate, ANR rate, and key metrics for 24-48 hours
  • Rollout halt criteria defined: halt if crash rate > X% or revenue drops > Y%
  • Previous release build retained in case rollback is needed
Play Store rollout:  10% → 25% → 50% → 100% over 3-5 days
App Store phased:    1% → 2% → 5% → 10% → 20% → 50% → 100% over 7 days

Phase 7: Post-release

  • Crashlytics / Sentry dashboard checked 1 hour after release
  • Store reviews monitored for new issues
  • Debug symbols retained for the shipped build version (needed for symbolication)

Automating with Fastlane

# fastlane/Fastfile
lane :release do
  # Run tests first
  sh "flutter test"

  # Android
  sh "flutter build appbundle --flavor production -t lib/main.dart --obfuscate --split-debug-info=build/debug-info"
  upload_to_play_store(track: 'internal', aab: 'build/app/outputs/bundle/productionRelease/app-production-release.aab')

  # iOS
  sh "flutter build ipa --flavor production -t lib/main.dart --export-options-plist=ios/ExportOptions.plist"
  upload_to_testflight(ipa: 'build/ios/ipa/Runner.ipa', skip_waiting_for_build_processing: true)

  # Tag the release
  sh "git tag v#{flutter_version_name}"
  sh "git push origin --tags"
end

Common pitfalls

Shipping the wrong Firebase config. Using the dev google-services.json in a production build means prod users write to your dev Firestore. Always verify the Firebase project ID in the build: log it on startup in staging, and check that kDebugMode guards prevent any dev config from reaching production.

Not uploading debug symbols. Obfuscated crash reports are unreadable without symbols. Upload them to Crashlytics/Sentry immediately after every release build — the symbols must match the exact build, so you can't retroactively upload them later.

Skipping phased rollout. Releasing to 100% of users immediately means a bug introduced in this version affects everyone instantly with no containment. Phased rollouts cap the blast radius to a fraction of users while you monitor.

Sign in to like, dislike, or report.

Deploying Flutter apps: a production checklist — ANN Tech