Every Build Reported the Same Version (Flutter iOS)
Pubspec said 2.4.0+117, the app showed 2.3.9. The culprit was not Flutter: two hardcoded files nobody ever looks at, and a lesson that sticks for years.
The Symptom: A Version That Refuses to Change
One of the production apps I've worked on shows the build version on its settings screen. I bump pubspec.yaml to 2.4.0+117, build, ship to TestFlight. Open settings: 2.3.9 (98).
Clean the cache. Build again. 2.3.9 (98). Every build, for weeks, reported exactly the same version, no matter what pubspec said.
Where an App Actually Gets Its Version Number
It's worth understanding the whole chain, because the bug can live in any link:
pubspec.yamlhasversion: 2.4.0+117: the part before the plus is the marketing version, after it the build number.- At build time Flutter turns this into
FLUTTER_BUILD_NAMEandFLUTTER_BUILD_NUMBERinGenerated.xcconfig. - Xcode maps those into project settings:
MARKETING_VERSIONandCURRENT_PROJECT_VERSION. - Those land in
Info.plistasCFBundleShortVersionStringandCFBundleVersion. - At runtime,
package_info_plusreads the values from the built Info.plist, not from pubspec.
Point five is the key: what the user sees comes from the Info.plist inside the binary. Pubspec is just the start of the chain, and every link after it can override it.
The Cause: Two Hardcoded Values Nobody Ever Looks At
In that project's ios/Runner/Info.plist, someone once hardcoded the values (probably while debugging something entirely unrelated):
CFBundleShortVersionString=2.3.9instead of$(MARKETING_VERSION)CFBundleVersion=98instead of$(CURRENT_PROJECT_VERSION)
And in project.pbxproj, an additional hardcoded CURRENT_PROJECT_VERSION = 98. The result: the entire chain from pubspec worked correctly right up to the final step, where the hardcode cut everything off. Flutter raised no error, Xcode raised no error, the build passed. Just with an eternal version 2.3.9.
The Fix, and One Trap
The fix is restoring the variables:
- in Info.plist:
<string>$(MARKETING_VERSION)</string>and<string>$(CURRENT_PROJECT_VERSION)</string> - in pbxproj:
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"
The trap: in pbxproj the $(...) value must be quoted. Without quotes Xcode considers the project file corrupted ("project file is damaged") and refuses to open the project at all. My first attempt ended exactly there.
The second lesson is less technical: I fixed it locally, tested it... and didn't commit. A week later the bug "came back", because it had never left the repo. A fix that isn't in git doesn't exist.
What Stays With You
- The version shown in the app comes from the built Info.plist, not from pubspec. When debugging a version issue, start at the end of the chain, not the beginning.
- Generated iOS files drift. Info.plist and pbxproj live for years, pass through dozens of hands, and "temporary" hardcodes stay in them forever. It pays to consciously review them once in a while.
flutter build --config-onlywon't fix hardcodes: it only regenerates the xcconfig, while the problem sits further down.- A hardcoded version isn't cosmetic: App Store Connect rejects an upload with a build number it has already seen, and crash reports tagged with the wrong version can derail an entire production diagnosis.