Introduction
Shipping a Flutter application to the stores is a two-part job.
On Android you upload a single .aab bundle. On iOS you upload an .ipa archive built through Xcode and processed by App Store Connect.
In this guide, you'll walk through both flows from a clean project to a submitted release.
Before You Start
Both stores reject builds that are missing basic metadata, so make sure the following is already in place.
| Requirement | Where it lives |
|---|---|
| Bundle ID / package name | android/app/build.gradle.kts and ios/Runner.xcodeproj/project.pbxproj |
| App version and build number | pubspec.yaml for Android, Info.plist / target settings for iOS |
| App name and description | Store listing console |
| Icons and screenshots | assets/, uploaded to each console |
| Privacy policy URL | Store listing console |
| Signing credentials | key.properties and the Xcode team |
| Apple Developer membership | Required for any iOS release |
Install dependencies and confirm a clean local build first.
flutter pub get
flutter doctor
Set the Bundle ID and Package Name
The identifier has to be registered on both consoles, so set it before you build.
Android. Both values live in android/app/build.gradle.kts. namespace sits at the top of the android block, and applicationId sits under defaultConfig.
android {
namespace = "com.example.myapp"
defaultConfig {
applicationId = "com.example.myapp"
}
}
Renaming the package is more than a find-and-replace. The Kotlin sources live under a matching folder path, so you have to move and rename them too, for example from
android/app/src/main/kotlin/com/example/oldname/toandroid/app/src/main/kotlin/com/example/myapp/. Update thepackagedeclaration at the top of each.ktfile to match, or the build fails.
iOS. The identifier is set in ios/Runner.xcodeproj/project.pbxproj, and it appears once per build configuration.
| Configuration | Notes |
|---|---|
| Debug | Local development builds |
| Release | Uploads to the App Store |
| Profile | Used for internal distribution and profiling |
Search the file for PRODUCT_BUNDLE_IDENTIFIER and update every match.
PRODUCT_BUNDLE_IDENTIFIER = com.example.myapp;
Miss one and you end up with a build that installs under a different identifier than the one registered on App Store Connect. If your app uses capabilities or a
Runner.entitlementsfile, the App Groups and Keychain values must use the same prefix too.
Bump the Version
Both stores reject a build whose version or build number was already used. Bump them before every upload.
A Flutter version has two parts in pubspec.yaml.
version: 1.2.4+45
# (1.2.4) user-facing version
# (45) build number (versionCode)
The part before + is the version users see. The part after + is the build number, which must be strictly higher than the previous upload.
Bump the Version Part
dart pub bump handles the semver portion.
dart pub bump patch # 1.2.3 -> 1.2.4
dart pub bump minor # 1.2.3 -> 1.3.0
dart pub bump major # 1.2.3 -> 2.0.0
dart pub bump breaking # 1.2.3 -> 2.0.0
Preview the change without writing it.
dart pub bump patch --dry-run
dart pub bumpdrops the build number. Bumping1.2.3+45produces1.2.4, so you have to add+46back by hand.
You can also change the version manually in
pubspec.yamlinstead of using the command. Edit theversion:field directly and write the exact value you want.
version: 1.2.4+46
Either way, the result is the same.
Keep Xcode in Sync
Info.plist reads the version through Flutter's build variables, so a pubspec.yaml bump covers both platforms.
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
If your Xcode target hardcodes the numbers instead, update them with agvtool.
cd ios
xcrun agvtool new-version -all 2.0.0
xcrun agvtool new-build-number 46 -all
xcrun agvtool what-marketing-version
xcrun agvtool what-version
Check Before You Ship
Confirm both values landed where you expect.
grep '^version:' pubspec.yaml
| Build | Play Store | App Store |
|---|---|---|
1.2.4+45 |
versionCode 45 | CFBundleVersion 45 |
1.2.5+46 |
versionCode 46 | CFBundleVersion 46 |
Android Release
1. Set Up Android Signing
Android release builds must be signed with an upload key. Set this up once, then reuse it for every future release.
You need three things: a keystore file, a properties file holding its passwords, and a Gradle config that reads them.
Generate the keystore. Run this once and store the .jks file somewhere safe and backed up.
keytool -genkey -v \
-keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias upload
If you lose this file, you can never update the app again without requesting a key reset from Google.
Store the passwords. Create android/key.properties and fill in the values you just created.
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=upload
storeFile=docs/release/upload_key/upload-keystore.jks
Add these to your project's .gitignore file so the passwords and the keystore never get committed.
android/key.properties
**/*.jks
**/*.keystore
Point Gradle at it. Open android/app/build.gradle, load the properties, and use them in the release signingConfig.
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias = keystoreProperties['keyAlias']
keyPassword = keystoreProperties['keyPassword']
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword = keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig = signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
file()resolvesstoreFilerelative to theandroiddirectory, not the project root. Sodocs/release/upload_key/upload-keystore.jksmeans<project>/android/docs/release/upload_key/upload-keystore.jks. Gradle reads this path on its own, so you only need to run the build command from the project root.
2. Build the App Bundle
Run the build from the project root.
flutter build appbundle
Run
flutter cleanfirst if you want to clear old build output, then rebuild from scratch. Do it whenever a previous build is being reused, signing changes are not picked up, or the version number looks stale in the output. Skip it for everyday local development, since a full rebuild is slow.
flutter pub get
flutter build appbundle
Use
flutter build appbundle --releasewhen you need to be explicit. Gradle reads the version frompubspec.yaml, so bump theversion:field before every upload.
3. Upload to Google Play
The bundle is written to the Android build output directory.
build/app/outputs/bundle/release/app-release.aab
Upload it in the Play Console.
- Open your app, then go to Release → Testing → Internal testing (or Production).
- Choose Create new release.
- Upload the
.aabfile. - Add release notes describing what changed.
- Save the draft, then Start rollout.
Google processes the bundle, generates listing screenshots, and delivers it to testers before you promote it to production.
iOS Release
1. Test on the Simulator
Run the app on a simulator and exercise the main flows.
flutter devices
flutter run -d <simulator_id>
Running the app in a simulator is a fast way to catch runtime issues before you create a device archive in Xcode.
2. Open the iOS Project in Xcode
open ios/Runner.xcworkspace
For a Flutter project, open Runner.xcworkspace rather than Runner.xcodeproj so Xcode also loads the CocoaPods dependencies.
Confirm the target is set up correctly.
- Signing & Capabilities — select your Apple Developer Team and let Xcode create the bundle identifier.
- General → Identity — set the display name and version, and bump the build number.
- Icons — make sure the App Icon set has every required size.
3. Run Xcode Analysis and a Release Build
Xcode's analyzer catches memory issues and Objective-C errors that a normal build misses.
- Select the Runner scheme.
- Use Product → Analyze (
⌘I) and resolve the reported issues. - Confirm that the scheme points to the existing app and uses the correct bundle identifier.
- Then build for release:
flutter build ipa.
4. Archive the App
If you have the Flutter source project, Xcode is the easiest way to create and upload the archive.
Open the Runner workspace from the Flutter project root.
open ios/Runner.xcworkspace
Xcode
↓
Select the Runner scheme
↓
Select Any iOS Device (arm64)
↓
Product → Archive
↓
Organizer
Follow these steps in Xcode:
- Select the Runner scheme for your existing app.
- Set the run destination to Any iOS Device (arm64). You cannot archive for a simulator.
- Choose Product → Archive.
- Wait for the release build to finish. Xcode opens Organizer automatically.
You can also create an archive from the command line with flutter build ipa, but it is not required when you use Xcode's Archive workflow.
5. Upload to App Store Connect
With the archive selected in Organizer, upload it directly from Xcode.
- Select the latest archive.
- Click Distribute App.
- Choose App Store Connect → Distribute.
- Continue through the signing and distribution options.
- Let Xcode validate the app and fix any reported errors.
- Click Upload.
After the upload, App Store Connect processes the build and adds it to your existing app. When processing finishes, select the build for the App Store version and submit it for review.
Xcode
↓
App Store Connect
↓
Processing
↓
Build appears in the existing app
↓
Select the build
↓
Submit for Review
If You Only Have the .ipa
An existing .ipa cannot normally be opened in Xcode and uploaded as a new archive. If you have only the .ipa and not the source project, upload it with Transporter or Apple's command-line upload tooling.
| What you have | Recommended workflow |
|---|---|
| iOS source project | Xcode → Archive → Upload |
| Flutter project | Open ios/Runner.xcworkspace, then archive and upload with Xcode |
Only an .ipa |
Transporter |
| Expo/EAS project | EAS Build → App Store Connect |
6. Verify with TestFlight
Processing can take anywhere from a few minutes to a few hours.
- Open App Store Connect → My Apps → your app → TestFlight.
- Wait for the build to finish processing and confirm the compliance questions.
- Add an internal or external tester group.
- Verify the install and core flows on a real device.
TestFlight is the last checkpoint before review. It runs the exact binary that users will download.
7. Submit for Review
When everything checks out, create a submission.
- Attach the processed build to the App Store version you want to release.
- Add screenshots, descriptions, keywords, and the review contact details.
- Choose Add for Review, then Submit for Review.
Apple usually responds within one to two days.
Common Pitfalls
| Problem | Cause |
|---|---|
Missing key.properties file |
The key path is relative to android/, not the project root |
| Build fails after renaming the package | The Kotlin sources under android/app/src/main/kotlin/ were not moved and their package declarations not updated |
| Only one iOS configuration builds | Two of the three PRODUCT_BUNDLE_IDENTIFIER entries were left unchanged |
| Play Console rejects the version | The version code in pubspec.yaml was not incremented |
| App Store Connect rejects the build | The version or build number in Xcode was not bumped |
| Archive fails to validate | Distribution certificate, profile, or team is missing |
| Everything works in debug only | No release build was tested before uploading |
Conclusion
Android releases are a single command followed by an upload. iOS releases go through Xcode, App Store Connect, and TestFlight.
Build early, bump the version every time, and test on real devices before submitting for review.