flutter-versioning
Scannednpx machina-cli add skill nylo-core/claude-code/flutter-versioning --openclawFlutter Versioning
Overview
Flutter uses a single version field in pubspec.yaml with the format x.y.z+build to define both the user-facing version string and the internal build number. This value propagates to iOS (CFBundleShortVersionString / CFBundleVersion) and Android (versionName / versionCode) during the build process.
When to Use
- User needs to set or bump the app version before a release
- User is confused about the
versionformat inpubspec.yaml - User needs to understand how Flutter versions map to platform-specific values
- User is setting up CI/CD and needs automated version bumping
- App was rejected because the version or build number was not incremented
- NOT for Dart package versioning on pub.dev (similar format but different context)
Quick Reference
| Concept | pubspec.yaml | iOS | Android |
|---|---|---|---|
| Version string | x.y.z | CFBundleShortVersionString | versionName |
| Build number | +build | CFBundleVersion | versionCode |
| Full format | version: 1.2.3+4 | 1.2.3 / 4 | 1.2.3 / 4 |
| Where set | pubspec.yaml | Auto from pubspec | Auto from pubspec |
| Override | N/A | --build-name / --build-number flags | Same flags |
Version Format in pubspec.yaml
version: 1.2.3+4
Breaking this down:
1— Major version: Incremented for breaking changes or major milestones2— Minor version: Incremented for new features (backwards compatible)3— Patch version: Incremented for bug fixes+4— Build number: Incremented for every build submitted to stores
Rules
- The version string (
x.y.z) follows Semantic Versioning - The build number (
+build) must be a positive integer - The build number is optional in pubspec.yaml but required for store submissions
- If omitted, the build number defaults to
0
How Versions Map to Platforms
iOS
Flutter reads pubspec.yaml and sets these in the generated Xcode build:
| pubspec.yaml | iOS Key | Location |
|---|---|---|
1.2.3 (before +) | CFBundleShortVersionString | Info.plist / Build Settings |
4 (after +) | CFBundleVersion | Info.plist / Build Settings |
The mapping happens through ios/Flutter/Generated.xcconfig:
FLUTTER_BUILD_NAME=1.2.3
FLUTTER_BUILD_NUMBER=4
And ios/Runner/Info.plist references these:
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
iOS Version Rules:
CFBundleShortVersionString— user-visible version, must bex.y.zformatCFBundleVersion— build number, must be a string of period-separated positive integers (e.g.,4or1.2.3.4)- Each new upload to App Store Connect must have a higher
CFBundleVersionthan the previous upload for the sameCFBundleShortVersionString - You can reset the build number when you increment the version string
Android
Flutter reads pubspec.yaml and sets these in the Gradle build:
| pubspec.yaml | Android Property | Location |
|---|---|---|
1.2.3 (before +) | versionName | build.gradle (via flutter config) |
4 (after +) | versionCode | build.gradle (via flutter config) |
The mapping happens through android/app/build.gradle:
android {
defaultConfig {
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
}
These values are read from android/local.properties (generated by Flutter):
flutter.versionName=1.2.3
flutter.versionCode=4
Android Version Rules:
versionName— user-visible version string, can be any format (butx.y.zis standard)versionCode— must be a positive integer- Each new upload to Google Play must have a strictly higher
versionCodethan any previously uploaded version versionCodecan never be reused or decreased, even for different tracks (internal, alpha, beta, production)- Maximum
versionCodevalue:2100000000
Semantic Versioning (SemVer)
Follow semantic versioning for the x.y.z part:
MAJOR.MINOR.PATCH
1.0.0 → Initial release
1.0.1 → Bug fix
1.1.0 → New feature added
2.0.0 → Breaking change or major redesign
When to Increment What
| Change Type | Bump | Example |
|---|---|---|
| Bug fix, small tweak | Patch | 1.2.3 → 1.2.4 |
| New feature, UI change | Minor | 1.2.3 → 1.3.0 |
| Redesign, breaking change | Major | 1.2.3 → 2.0.0 |
| Every store submission | Build number | 1.2.3+4 → 1.2.3+5 |
Pre-Release Versions
For apps not yet at 1.0.0:
0.1.0 → Early development, API may change
0.9.0 → Feature-complete, pre-release
1.0.0 → First stable release
Build Number Strategies
Simple Sequential
Increment by 1 for every build submitted to any store:
1.0.0+1
1.0.0+2
1.0.1+3
1.1.0+4
2.0.0+5
Pros: Simple, always increasing. Cons: Build number has no intrinsic meaning.
Date-Based
Use the date as the build number (format: YYYYMMDD or YYMMDDhh):
1.2.3+20260224 → February 24, 2026
1.2.3+2602241400 → February 24, 2026, 2:00 PM
Pros: Build number encodes when it was built. Cons: Multiple builds per day need hour/minute precision. Value can exceed Android's max integer for very long formats.
Computed from Version
Encode the version in the build number:
# Format: MAJOR * 10000 + MINOR * 100 + PATCH
1.2.3 → build number 10203
2.1.0 → build number 20100
12.3.1 → build number 120301
Pros: Build number is predictable from version. Cons: Limits patch versions to 99, cannot submit multiple builds for same version.
Per-Version Reset (iOS-friendly)
Reset build number to 1 with each new version:
1.0.0+1
1.0.0+2 → Fix before release
1.0.1+1 → Reset for new version
1.1.0+1 → Reset for new version
Pros: Clean, easy to understand.
Cons: Does NOT work for Android — versionCode must always increase globally, never reset.
Overriding Version at Build Time
You can override the pubspec.yaml version when building:
# Override both version name and build number
flutter build apk --build-name=1.2.3 --build-number=42
flutter build ipa --build-name=1.2.3 --build-number=42
flutter build appbundle --build-name=1.2.3 --build-number=42
This is especially useful in CI/CD where the build number is determined by the pipeline.
CI/CD Version Bumping
Reading the Current Version
# Extract version from pubspec.yaml
grep '^version:' pubspec.yaml
# Output: version: 1.2.3+4
Automated Bumping Script (Shell)
#!/bin/bash
# bump_version.sh — Increment build number in pubspec.yaml
CURRENT=$(grep '^version:' pubspec.yaml | sed 's/version: //')
VERSION_NAME=$(echo "$CURRENT" | cut -d'+' -f1)
BUILD_NUMBER=$(echo "$CURRENT" | cut -d'+' -f2)
NEW_BUILD=$((BUILD_NUMBER + 1))
NEW_VERSION="${VERSION_NAME}+${NEW_BUILD}"
sed -i '' "s/^version: .*/version: ${NEW_VERSION}/" pubspec.yaml
echo "Bumped version to ${NEW_VERSION}"
Using cider Package
The cider package provides structured version bumping:
# Install
dart pub global activate cider
# Bump commands
cider bump patch # 1.2.3+4 → 1.2.4+4
cider bump minor # 1.2.3+4 → 1.3.0+4
cider bump major # 1.2.3+4 → 2.0.0+4
cider bump build # 1.2.3+4 → 1.2.3+5
cider bump patch+build # 1.2.3+4 → 1.2.4+5
# Set exact version
cider version 2.0.0+1
GitHub Actions Example
name: Build and Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: |
TAG=${GITHUB_REF#refs/tags/v}
echo "version=$TAG" >> $GITHUB_OUTPUT
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.24.0'
- name: Build APK
run: |
flutter build apk \
--build-name=${{ steps.version.outputs.version }} \
--build-number=${{ github.run_number }}
- name: Build IPA
run: |
flutter build ipa \
--build-name=${{ steps.version.outputs.version }} \
--build-number=${{ github.run_number }}
Using ${{ github.run_number }} as the build number guarantees a unique, always-increasing integer for each CI run.
Platform-Specific Overrides
Hardcoded iOS Version (Not Recommended)
If ios/Runner/Info.plist has hardcoded values instead of variables:
<!-- BAD: Hardcoded — won't sync with pubspec.yaml -->
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<!-- GOOD: Dynamic — reads from Flutter build config -->
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
Ensure the Info.plist uses $(FLUTTER_BUILD_NAME) and $(FLUTTER_BUILD_NUMBER).
Hardcoded Android Version
If android/app/build.gradle has hardcoded values:
// BAD: Hardcoded — ignores pubspec.yaml
defaultConfig {
versionCode 1
versionName "1.0"
}
// GOOD: Dynamic — reads from Flutter local.properties
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
def flutterVersionName = localProperties.getProperty('flutter.versionName')
defaultConfig {
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
Common Mistakes
-
Not incrementing Android versionCode — Google Play requires every upload to have a strictly higher
versionCodethan all previous uploads across all tracks. Uploading with the same or lowerversionCodeis rejected. -
Resetting build number on Android — Unlike iOS, Android's
versionCodecan never go backwards. If you shipped1.0.0+10and try to upload1.1.0+1, Google Play will reject it. Always keep Android build numbers strictly increasing. -
Forgetting the build number entirely — If you omit the
+buildpart (version: 1.2.3), the build number defaults to0. This works for development but will cause store submission issues since0is a valid but low starting point that leaves no room to go lower. -
Exceeding Android versionCode max — The maximum
versionCodeis2100000000. Date-based schemes likeYYYYMMDDHHmmcan exceed this limit. Use shorter date formats or simple sequential numbers. -
Hardcoded versions in Info.plist or build.gradle — If platform files have hardcoded versions instead of reading from Flutter's generated config, changes to
pubspec.yamlare silently ignored. Verify that Info.plist uses$(FLUTTER_BUILD_NAME)and build.gradle reads fromlocalProperties. -
Misunderstanding version vs build number — The version (
1.2.3) is what users see in the store. The build number (+4) is the internal identifier. You can submit multiple builds with the same version (incrementing only the build number) for testing, but you cannot submit the same build number twice. -
Inconsistent versions across platforms — If you override versions via
--build-name/--build-numberflags for one platform but not the other, iOS and Android can end up with different version strings. Use the same flags or rely on pubspec.yaml for both. -
Not updating version before release builds — Running
flutter build appbundlewithout bumping the version submits the same version as the last upload. Always check and increment before building for store submission.
Source
git clone https://github.com/nylo-core/claude-code/blob/main/skills/flutter-versioning/SKILL.mdView on GitHub Overview
Flutter uses a single version field in pubspec.yaml with the format x.y.z+build to define both the user-facing version and the internal build number. This value propagates to iOS (CFBundleShortVersionString / CFBundleVersion) and Android (versionName / versionCode) during the build process, so you can manage releases and store submissions from one source of truth.
How This Skill Works
Flutter reads the pubspec.yaml version and splits it into versionName (x.y.z) and build (the +build part). The build system then writes these values into platform-specific locations: iOS uses CFBundleShortVersionString and CFBundleVersion via the Generated.xcconfig and Info.plist, while Android uses versionName and versionCode through Gradle configuration. This mapping ensures consistent versioning across Flutter, iOS, and Android builds.
When to Use It
- You need to bump the user-visible version before a release
- You're unclear about the pubspec.yaml version format and its parts
- You want to understand how Flutter versions map to iOS and Android values
- You're setting up CI/CD and want automated version bumps
- Your app was rejected due to not incrementing the version or build number
Quick Start
- Step 1: In pubspec.yaml, set version: 1.2.3+4
- Step 2: Run flutter pub get and build (flutter build ios or flutter build apk)
- Step 3: Check Xcode and Gradle outputs to confirm FLUTTER_BUILD_NAME / FLUTTER_BUILD_NUMBER and the corresponding Info.plist / Gradle values
Best Practices
- Follow semantic versioning for the x.y.z part in pubspec.yaml
- Increment the build number (+build) for every store submission
- Keep iOS and Android versionName/versionCode in sync with pubspec.yaml
- Automate version bumps in CI/CD and gate deployments on tests
- Reset the build number when you increment the version string
Example Use Cases
- Increment 1.2.3+4 to 1.3.0+5 for a feature release
- A CI script automatically bumps the minor version on PR merge
- Each App Store/Play Store submission uses a higher CFBundleVersion/versionCode
- Reset build number when increasing the major version (1.x.y+0 -> 2.0.0+1)
- Verify flutter.versionName and flutter.versionCode in local.properties during audits