Flutter Version Management (FVM)
Overview
FVM pins a Flutter project to a specific SDK version so every contributor and CI runner builds against the same toolchain. This skill installs FVM, pins a project to a version, runs Flutter/Dart through FVM, configures IDEs, and wires up CI.
If the project has no pubspec.yaml, this is not a Flutter project — stop and ask the user before proceeding.
Workflow
1. Discover current state
Run these in parallel and use the results to decide which steps below apply:
fvm --version 2>/dev/null || echo "fvm-not-installed"
test -f .fvmrc && cat .fvmrc || echo "no-.fvmrc"
test -d .fvm && ls .fvm || echo "no-.fvm-dir"
test -f pubspec.yaml && grep -E "^\s*sdk:|^\s*flutter:" pubspec.yaml || echo "no-pubspec"
grep -E "(^|/)\.fvm(/|$)" .gitignore 2>/dev/null || echo ".fvm-not-in-gitignore"
Branch on results:
- No FVM installed → Step 2 (Install FVM).
- No
.fvmrc→ Step 3 (Pin a version). .fvmrcexists but.fvm/flutter_sdkmissing → runfvm install(Step 4).- All present, user wants to change version → Step 5 (Switch version).
- CI / IDE setup requested → Steps 6 / 7.
2. Install FVM
Pick one method based on platform and existing tooling. Confirm with the user before installing.
| Platform | Recommended | Command |
|---|---|---|
| macOS (Homebrew user) | brew | brew tap leoafarias/fvm && brew install fvm |
| Any Unix | install script | curl -fsSL https://fvm.app/install.sh | bash |
| Windows | Chocolatey | choco install fvm |
| Has Dart already | pub | dart pub global activate fvm |
After install, verify: fvm --version and fvm doctor.
3. Pin a version (new project or first-time pin)
- Decide which version. Default to the channel/version already in use; otherwise the latest stable. List options with
fvm releases | tail -20or usestable. - Run
fvm use <version>from the project root. This:- Downloads the SDK if missing (cached globally in
~/fvm/versions/). - Creates
.fvmrcwith{"flutter": "<version>"}. - Creates
.fvm/flutter_sdkas a symlink to the cached SDK. - Runs
flutter pub get(skip with--skip-pub-getfor monorepos with custom bootstrap).
- Downloads the SDK if missing (cached globally in
- Add
.fvm/to.gitignore(keep.fvmrctracked):.fvm/ !.fvmrc - Commit
.fvmrcand the updated.gitignore. Do not commit.fvm/flutter_sdk— it is a machine-local symlink to a cached SDK.
4. Install the version a project already pins
fvm install # reads .fvmrc
fvm doctor # confirms symlinks resolve and SDK is healthy
fvm flutter --version
5. Switch versions
- Same project, new version:
fvm use <version>(updates.fvmrc). - Force when contributing to a fork or a non-standard channel:
fvm use <version> --force. - Pin to an exact release rather than the channel head:
fvm use <version> --pin. - System-wide default for ad-hoc work outside any project:
fvm global <version>. Then add the global symlink to PATH:export PATH="$HOME/fvm/default/bin:$PATH"(zsh/bash rc).
6. IDE setup
VS Code — write/merge .vscode/settings.json:
{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
"search.exclude": { "**/.fvm": true },
"files.watcherExclude": { "**/.fvm": true }
}
Android Studio / IntelliJ — point Flutter SDK path to <project>/.fvm/flutter_sdk in Preferences → Languages & Frameworks → Flutter. Restart the IDE after switching versions in .fvmrc.
After IDE setup, confirm Dart Analyzer picked up the change (Command Palette → Dart: Restart Analysis Server).
7. CI configuration
For GitHub Actions with subosito/flutter-action, read the version from .fvmrc so CI never drifts from local:
- name: Read Flutter version
id: fvm
run: echo "version=$(jq -r .flutter .fvmrc)" >> "$GITHUB_OUTPUT"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ steps.fvm.outputs.version }}
channel: stable
Avoid installing FVM itself on CI runners — it adds startup time for no benefit. The .fvmrc is the source of truth; the action consumes it directly.
8. Run Flutter / Dart through FVM
Once pinned, all Flutter and Dart commands must go through FVM in this project, otherwise contributors will silently use their global SDK:
fvm flutter pub get
fvm flutter run
fvm flutter test
fvm dart run build_runner build
fvm dart format .
Encourage a shell alias only at the user level (alias flu="fvm flutter"), never as a project-level script that masks the explicit fvm prefix.
9. Test against multiple versions
For library/package authors validating compatibility:
fvm spawn 3.19.0 test
fvm spawn 3.22.0 test
fvm spawn beta test
spawn runs the command against a specific version without changing the project's pinned version.
10. Maintenance
fvm list— show installed versions and which one each project uses.fvm releases— list available Flutter releases.fvm remove <version>— free disk space (each cached SDK is ~1–2 GB).fvm doctor— diagnose broken symlinks, PATH, or cache corruption.fvm destroy— nuclear option; removes the entire FVM cache. Confirm with the user before running — it deletes all cached SDKs across all projects.
Output
Report back to the user with:
- Current Flutter version (from
fvm flutter --version). - Files created or modified (
.fvmrc,.gitignore,.vscode/settings.json, CI workflow). - Any commands the user must run themselves (e.g., shell PATH updates, IDE restart).
- Verification commands they can run to confirm setup.
Anti-patterns
- Mixing
fvm flutterand bareflutterin the same project. The whole point of FVM is reproducibility; calling the global Flutter even once defeats it. AuditMakefile,package.jsonscripts, shell scripts, and CI for unprefixedflutter/dartcalls. - Committing
.fvm/flutter_sdk. It is a symlink to a machine-local cache — useless on another machine and bloats the repo if the symlink resolves on commit. Only.fvmrcbelongs in version control. - Pinning to a channel name (
stable,beta) on a serious project. Channels move. Two contributors runningfvm installa week apart end up on different SDKs. Use a concrete version (3.22.0) for shipped projects; reserve channels for exploratory work. - Installing FVM on CI just to read
.fvmrc. A one-linejqagainst.fvmrcplussubosito/flutter-actionis faster and has fewer moving parts than installing FVM on every run. - Editing
.fvm/flutter_sdk/directly to patch an SDK bug. The symlink target is shared across projects; edits leak everywhere and disappear onfvm install. Usefvm use <fork>/<branch>to point at a fork instead. - Running
fvm destroyto "clean up". It wipes every cached SDK on the machine, forcing every project to re-download gigabytes. Usefvm remove <version>for targeted cleanup.
Source: jerelvelarde/chalk-skills — distributed by TomeVault.