← Articles

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
  • +47versionCode (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.

Semantic versioning for Flutter apps — ANN Tech