Semantic versioning for Flutter apps
By John · 29 January 2026
Semantic versioning (semver) gives version numbers a predictable meaning: major.minor.patch. In Flutter, the version string in pubspec.yaml follows this convention and communicates the scope of changes to users and package consumers.
The three numbers
2.5.1
^ ^ ^
| | └── Patch: bug fixes. No new features, no breaking changes.
| └───── Minor: new features. Backwards compatible. No breaking changes.
└──────── Major: breaking changes. Users/packages need to update their code.
For apps (not libraries):
- Users don't write code against your app, so "breaking changes" means significantly different UX or removed features
- A new major version signals a significant redesign or migration requirement
For packages (published to pub.dev):
- Strictly follow semver — a breaking API change without a major bump breaks downstream packages
Flutter's version format
# pubspec.yaml
version: 2.5.1+47
# ----- --
# semver build number (internal, not shown to users)
2.5.1→ shown in App Store/Play Store listings and About screens+47→versionCode(Android) /CFBundleVersion(iOS) — must be strictly increasing
When to bump which number
Bug fix in existing functionality:
2.5.1 → 2.5.2
New feature (checkout with PayPal):
2.5.1 → 2.6.0
Significant redesign or major workflow change:
2.5.1 → 3.0.0
Emergency hotfix after a release:
2.5.0 → 2.5.1 (patch bump)
Automating version bumps
#!/bin/bash
# scripts/bump.sh
set -e
CURRENT=$(grep '^version:' pubspec.yaml | sed 's/version: //')
NAME=$(echo "$CURRENT" | cut -d'+' -f1)
BUILD=$(echo "$CURRENT" | cut -d'+' -f2)
MAJOR=$(echo "$NAME" | cut -d. -f1)
MINOR=$(echo "$NAME" | cut -d. -f2)
PATCH=$(echo "$NAME" | cut -d. -f3)
case "$1" in
major) MAJOR=$((MAJOR+1)); MINOR=0; PATCH=0 ;;
minor) MINOR=$((MINOR+1)); PATCH=0 ;;
patch) PATCH=$((PATCH+1)) ;;
*) echo "Usage: ./scripts/bump.sh [major|minor|patch]"; exit 1 ;;
esac
NEW_BUILD=$((BUILD+1))
NEW="$MAJOR.$MINOR.$PATCH+$NEW_BUILD"
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/^version: .*/version: $NEW/" pubspec.yaml
else
sed -i "s/^version: .*/version: $NEW/" pubspec.yaml
fi
echo "Bumped: $CURRENT → $NEW"
Usage:
bash scripts/bump.sh patch # 2.5.1+47 → 2.5.2+48
bash scripts/bump.sh minor # 2.5.1+47 → 2.6.0+48
bash scripts/bump.sh major # 2.5.1+47 → 3.0.0+48
Git tagging strategy
# After bumping
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | cut -d'+' -f1)
git add pubspec.yaml
git commit -m "chore: bump version to $VERSION"
git tag "v$VERSION"
git push origin main --tags
For packages published to pub.dev
Follow semver strictly:
// v1.0.0 public API:
class MyWidget extends StatelessWidget {
const MyWidget({super.key, required this.label});
final String label;
}
// Adding an optional param = minor bump (backwards compatible):
const MyWidget({super.key, required this.label, this.color}); // 1.1.0
// Renaming 'label' to 'text' = breaking change → major bump (2.0.0)
Pre-release versions
For beta versions of packages:
version: 2.0.0-beta.1 # Pre-release
version: 2.0.0-rc.1 # Release candidate
version: 2.0.0 # Stable release
Users adding ^1.0.0 in their pubspec.yaml won't get 2.0.0-beta automatically — they have to explicitly specify the pre-release version.
Reading the version at runtime
import 'package:package_info_plus/package_info_plus.dart';
final info = await PackageInfo.fromPlatform();
print('${info.version}+${info.buildNumber}'); // "2.5.1+47"
Common pitfalls
Not bumping the build number when bumping the version. App stores require a unique build number. If you bump 2.5.0 → 2.5.1 but keep the same +47, the upload is rejected.
Calling internal refactors a minor version. Refactoring without API changes is not a version bump at all (or at most a patch for fixing a subtle bug). Save minor bumps for real feature additions.
Pub packages: adding required parameters without a major bump. Adding a required constructor parameter is a breaking change. Existing code calling MyWidget(label: 'x') breaks. Bump major or make the parameter optional with a default.
Sign in to like, dislike, or report.