Written by Chris Raastad (Product Manager at Codemagic) and Lewis Cianci
Every app you have installed on your phone has a version number. It’s something that most users don’t even think about, but it’s something that means a lot to us as developers. When we’re just getting started as developers, we tend to just increase the version number by one each time we have a new release. And in the early days, that’s probably enough to get our apps to market, and to release updates.
As a developer, I don’t get too excited by topics like app versioning - I’d rather be implementing some new killer feature or adding some visual polish. Traditionally, I’m the kind of person that would completely disregard versioning until I absolutely had to do something about it. But this isn’t the right approach, and if you get app versioning right, you can save a few headaches down the track.
So let’s take a look at some of the best practices app versioning, and also how Codemagic can help us to implement these best practices.
Versioning is important
As you probably already know, both the Play Store and the App Store (hereafter referred to as “the stores”) won’t let you upload a version of your app that has a lower version number than what is already on their server. The stores will only offer the latest version of your app for download, or update. So uploading a lower version of your app wouldn’t work because nobody would ever get it installed anyway.
All of our telemetry to do with our app, like the data that Crashlytics or Sentry.io sends us will be attached to a certain version of the app. So if we don’t put a lot of thought into the versioning process, we could potentially be left puzzled if we start receiving crashes or errors for a certain version of our app.
Android Versioning
On Android, we have our versionCode
, and our versionName
(as per the Android Documentation). Let’s take a better look at what these represent.
versionName
versionName
is just a text based representation of the version of your app. Its sole purpose it to be shown to the user to identify the version of the app in a way that the user could perhaps understand. So you can set this to literally anything that you want. Of course, you should set it to something that means something to you, as this version name will be attached to any telemetry that comes from your app (like crashes, for example). We’ll look at what some good ideas would be for this field a little later on, after we’ve seen how iOS handles versions.
versionCode
versionCode
is the internal version of our application, and only supports integer values. Every time we release our app, we’re supposed to increment this number, whether our release is major or minor. We can’t define major, minor, or patch levels in this version code, and it can only be an integer. This is sometimes called build number.
The maximum version code you can have is 2100000000
. You might think it’s weird to call this out, and that it’s pretty unlikely that you would ever come close to releasing two billion, one hundred million versions of your app. But the real reason why I call this out is because sometimes you can be right at the end of uploading your app, only to have the Play Store knock it back citing an issue with the version you’ve chosen. In that moment, you can be tempted to just throw in a really high number, like 1000. Then, next time, you put in an even higher number like 10,000, and so on and so forth. This is always the wrong thing to do. If you’ve started down this path, work out your current version, and a way to update it incrementally.
So, what should my versionName
and versionCode
be?
Your versionCode
should just increment every time you release your app, and your versionName
should be something that makes sense to you and your users, such as a {major}.{minor}.{patch} versioning scheme.
iOS Versioning
On iOS, we haveCFBundleShortVersionString
(Release Version Number) and CFBundleVersion
(Build Version Number). The relationship between these two properties are explained in Apple’s (slightly outdated, but still relevant) note on Version Numbers and Build Numbers. Let’s take a more detailed look at these two properties.
CFBundleShortVersionString (Release Version Number)
CFBundleShortVersionString is the external user facing release version of your app displayed in the App Store. It must follow the {major}.{minor}.{patch} version format of three period separated integers. This must be incremented every time you release a version to the App Store.
CFBundleVersion (Build Version Number)
CFBundleVersion is the internal build version number of your application used for testing and development. It appears in {major}.{minor}.{patch} format of one to three period separated integers. If {minor}.{patch} are not provided, then they will default to zero. For example, if you specify ‘10’, then your build version will be ‘10.0.0’. Build version number must be incremented with every release candidate submitted to test flight for a particular release version number. For iOS apps, build version number can be reused across different release version numbers. The same cannot be said about MacOS apps, which must have a unique build version number across all release version numbers.
Why would we want different internal build and external release version numbers? This might be a little confusing. Well, your internal version might increment quite frequently with many subsequent submissions to Test Flight for the same release version. This could be because of bugs found from a round of QA or dealing with the issues of App store submission rejections. Your release build number only changes after successful submissions to the App Store.
Now, maybe you’re there, up to this part of this article, completely bored out of your mind and thinking to yourself “Whatever, I’ll just submit whatever version number I want, and if the store accepts it then it’s all good”. Well, technically you can, and some people have, but you might break in-app purchases for your app. Yes, really, as per the documentation.
The fourth point makes this clear. If your CFBundleVersion
isn’t a string consisting of three unsigned integers separated by a period, it might affect your users’ ability to restore in-app purchases. Yikes. So, yes, versioning might be boring, but it’s incredibly important to get it right.
So, what should my CFBundleShortVersionString
and CFBundleVersion
be?
Release version number, CFBundleShortVersionString
, should be incremented after every App Store release, such as a {major}.{minor}.{patch} versioning. Build version number, CFBundleVersion
, should be incremented after every subsequent submission to Test Flight. There is freedom of choice how to keep track of build version number in relation to release build number. The build number could be reset to 0.0.0 or 1.0.0 after release version number is incremented, increment completely independently of release version number, or be set to some other value related to the current release version number.
Aligning the versioning between platforms
If we’re making our apps for more than one platform, especially using a cross-platform framework, we should try to align our versions between those platforms. We don’t want to have version 50 of our app on Android be the same code as version 15 on our iOS app. To make our lives easier, it would be nice to have the same versioning compatible with both iOS and Android versioning schemes. To achieve this, we can set external user facing versions versionName
and CFBundleShortVersionString
to be the same value and the internal versions versionCode
and CFBundleVersion
to be the same value.
For our external versioning (versionName
and CFBundleShortVersionName
), in order to comply with iOS CFBundleShortVersionName
restrictions, we should choose {major}.{minor}.{patch} semantic versioning. You can use something like git version to help set a semantic version automatically for your app.
For our internal versioning (the versionCode
and CFBundleVersion
), in order to comply with Android’s restriction of versionCode
being an integer, we can set the version to the build number. To define a build number, we could use our commit count from our current git repository, or we could use another integer build number that we manage internally.
How does Flutter handle cross-platform app versioning?
In a Flutter application, versioning is specified in the version
field of the pubspec.yaml file. According to the pubspec.yaml template.
A version number is three numbers separated by dots, like 1.2.43
followed by an optional build number separated by a +.
Both the version and the builder number may be overridden in flutter
build by specifying --build-name and --build-number, respectively.
In Android, build-name is used as versionName while build-number used as versionCode.
In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
Flutter gives us the freedom to define our versioning in both pubspec.yaml
or via build parameters --build-name
and --build-number
. During flutter build commands, these are exported to env variables FLUTTER_BUILD_NAME
and FLUTTER_BUILD_NUMBER
which are referenced in the Flutter starter application Runner.xcodeproj or build.gradle files. We could keep both the version and build number committed to version control in pubspec.yaml
, but doing that, it would become soon become unmanageable to update the build number on every single commit, especially with multiple commit authors. Version number, --build-name
, makes sense to commit to version control, since it will only be updated with each app release. But how should we keep track of build number external of code source control? Build number can be managed by CI.
Using Codemagic to version our Flutter apps
For new Flutter projects, one option is to use the BUILD_NUMBER
Codemagic environment variable to set the Flutter version and build number, which will propagate to both the Android and iOS versioning parameters. This number will increment each time we run a build for a Codemagic workflow. If we’re building a Flutter project, we can use these parameters with the flutter build ipa
or flutter build apk
commands to appropriately set the build number for our app:
--build-name=1.0.$BUILD_NUMBER --build-number=$BUILD_NUMBER
This is great to get off the ground with build versioning. But what if you add a new Codemagic workflow for separating your master branch release builds from your other branch builds? BUILD_NUMBER
will be reset to 1 in the new workflow and might result in a version conflict for Android or iOS. We could also consider using the PROJECT_BUILD_NUMBER
Codemagic environment variable, which increments with each build for the entire application. But this has the problem of incrementing for every workflow, giving us potentially non-consecutive build numbers for a particular workflow, which might cause some headaches down the road.
For existing Flutter projects, assuming we are committing the version to pubspec.yaml source control, we can use some ✨magic ✨ provided by Codemagic CLI Tools to get the last build number of our app directly from the stores and use that to set our next build number. This will set the next build number independent of any CI/CD platform, since the CLI tools will work on any CI server that runs python. This takes a little bit of setup to get working, but once it’s done it should work for quite some time. We’ll take a look at how to do this for each of the stores.
Set Flutter iOS next build number from App Store or Test Flight
The Codemagic CLI tool app-store-connect allows you to get the latest build version number from the App Store or Test Flight. Note, if the build numbers are out of sync between Android and iOS, then this only will make sense to do in a workflow building only an iOS artifact, Android will have to be handled separately.
First you’ll need to follow this guide to create App Store Connect API keys. Once that is done, as per the guide, you’ll need to export the credentials into APP_STORE_CONNECT_KEY_IDENTIFIER
, APP_STORE_CONNECT_ISSUER_ID
, and APP_STORE_CONNECT_PRIVATE_KEY
environment variables. Then you’ll need to find your application App ID from App Store Connect. Once that’s done, in a Flutter pre-build script, you use the following commands to get the next build number:
LATEST_BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number 'APPLICATION_APP_ID')
NEXT_BUILD_NUMBER=$(($LATEST_BUILD_NUMBER + 1))
which can then be used in your flutter build ipa
command with the build argument:
--build-number=$NEXT_BUILD_NUMBER
You can do the same thing for an app store build number replacing get-latest-testflight-build-number
with get-latest-app-store-build-number
.
Once this is all set up, the build version number CFBundleVersion
will automatically be set based on what’s uploaded in App Store Connect.
Set iOS app next build version number from App Store or Test Flight
The steps are the same as in Flutter iOS, except, to increment build version number CFBundleVersion
, you’ll use Xcode’s command line agvtool. The commands are as follows:
LATEST_BUILD_NUMBER=$(app-store-connect get-latest-testflight-build-number 'APPLICATION_APP_ID')
NEXT_BUILD_NUMBER=$(($LATEST_BUILD_NUMBER + 1))
agvtool new-version -all $NEXT_BUILD_NUMBER
Set Flutter Android next build number from Google Play
The steps for Android are very similar to iOS, but this time using the Google Play Codemagic CLI tools command.
First you’ll need to follow this guide set up your Google Play service account credentials. Then you’ll need to export your service account credentials to the GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
env variable. You’ll also need to find your app package name from Google Play. Once that’s done, in a Flutter pre-build script, you can use the following command to get the next build number:
LATEST_BUILD_NUMBER=$(google-play get-latest-build-number --package-name 'com.google.example')
NEXT_BUILD_NUMBER=$(($LATEST_BUILD_NUMBER + 1))
which can be used in your flutter build apk
command with the build argument:
--build-number=$NEXT_BUILD_NUMBER
Once this is all set up, the build number versionCode
will automatically be set based on what’s uploaded to the Google Play Store.
Set Android app next version code from Google Play
The steps are the same as Flutter Android, but instead of passing NEXT_BUILD_NUMBER
to build arguments, you can add the following to your app’s build.gradle to set versionCode
:
android {
// try to get build number from `LATEST_GOOGLE_PLAY_BUILD_NUMBER` env var. If not specified, then from `BUILD_NUMBER` env var, else 0
def latestGooglePlayBuildNumber = Integer.valueOf(System.env.LATEST_GOOGLE_PLAY_BUILD_NUMBER ?: System.env.BUILD_NUMBER ?: 0)
defaultConfig {
versionCode latestGooglePlayBuildNumber + 1
}
}
You can see the example Android versioning app for a full working example.
Final words
As you can see, app versioning is incredibly important and complicated to manage, especially in cross app platforms like Flutter. The complication comes from the freedom of choice how generate and keep track of version and build numbers, as well as understanding the versioning nuances of each platform. Hopefully this article, and a little help from Codemagic, help you get a handle on app versioning, so that you can focus on building great apps.
Was this article useful? 🤔 Let us know HERE.