Skip to content
compiler.dev

Speed Up Flutter in GitHub Actions

Last updated

To speed up Flutter in GitHub Actions, cache the Flutter SDK and pub packages with subosito/flutter-action, run analysis and tests on Linux, build Android on Linux, and keep macOS only for the iOS build. macOS minutes cost several times more than Linux minutes, and most of a Flutter pipeline does not need a Mac.

This guide is the Flutter version of our general guide to speeding up GitHub Actions.

Where the time goes

A Flutter pipeline has four parts: install the SDK, flutter pub get, flutter analyze plus flutter test, and platform builds (flutter build apk or appbundle, flutter build ios). On a fresh VM the SDK download is a few hundred megabytes, and the native builds (Gradle for Android, Xcode and CocoaPods for iOS) are the slowest. The test and analyze steps are quick by comparison.

SDK and pub cache with flutter-action

subosito/flutter-action installs Flutter and can cache it. Its README documents these inputs:

  • channel: stable, beta or master.
  • flutter-version: a version like 3.19.0, or a partial like 3.x.
  • flutter-version-file: read the version from pubspec.yaml or .fvmrc. The README warns that the exact version must be in the file, so ranges will not work.
  • cache: caches the Flutter SDK. If pub-cache is not set, it also controls pub dependency caching.
  • cache-key, pub-cache-key and cache-path: custom keys and paths. They accept placeholders such as :os:, :channel:, :version:, :arch: and :hash:.
- uses: actions/checkout@v7
- uses: subosito/flutter-action@v2
  with:
    channel: stable
    flutter-version-file: pubspec.yaml
    cache: true
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage

Pin the version (through the file or flutter-version) rather than following stable blindly. A new stable release changes the SDK cache key, and your build changes under you. A pinned version also makes your cache hit rate predictable.

Test speed

flutter test runs test files in parallel. It uses the --concurrency option, which defaults to the number of cores minus something small, so more cores help automatically. Practical tips:

  • Split fast widget and unit tests (flutter test) from slower integration tests (integration_test/), which need a device or emulator and are slow to start.
  • Use flutter test --coverage only if you upload coverage; it adds time.
  • If your repo has several packages (a monorepo with Melos), run flutter test per package in a matrix, or use Melos to run scripts in parallel.
  • For golden tests, generate and compare on one OS. Fonts differ across platforms, and Linux is the cheapest place to run them.

Android builds on Linux

Android builds run Gradle, and Gradle needs its own caching. Add actions/setup-java and gradle/actions/setup-gradle; see our Gradle guide for the settings:

build-android:
  runs-on: ubuntu-latest
  timeout-minutes: 30
  steps:
    - uses: actions/checkout@v7
    - uses: actions/setup-java@v6
      with:
        distribution: temurin
        java-version: 17
    - uses: gradle/actions/setup-gradle@v6
    - uses: subosito/flutter-action@v2
      with:
        channel: stable
        flutter-version-file: pubspec.yaml
        cache: true
    - run: flutter pub get
    - run: flutter build apk --release
    - uses: actions/upload-artifact@v7
      with:
        name: apk
        path: build/app/outputs/flutter-apk/app-release.apk

Use flutter build apk --split-per-abi only if you need separate APKs; it builds once per ABI and takes longer. For store uploads, build an app bundle (appbundle) once. Check which Java version your Android Gradle Plugin version requires before pinning one.

iOS builds on macOS, as little as possible

flutter build ios needs Xcode, so it needs macOS. Keep that job separate and run it only when needed, for example on main or release tags, not on every PR push. Cache CocoaPods by caching ios/Pods keyed on ios/Podfile.lock:

build-ios:
  runs-on: macos-latest
  steps:
    - uses: actions/checkout@v7
    - uses: subosito/flutter-action@v2
      with:
        channel: stable
        flutter-version-file: pubspec.yaml
        cache: true
    - uses: actions/cache@v6
      with:
        path: ios/Pods
        key: pods-${{ runner.os }}-${{ hashFiles('ios/Podfile.lock') }}
    - run: flutter pub get
    - run: flutter build ios --release --no-codesign

--no-codesign is right for a compile check; use real signing only in the release job. For macOS cost and options, see macOS runners for iOS CI and speed up Xcode builds in CI. Hosted macOS is billed at a higher per-minute rate than Linux, so every minute you move to Linux saves real money.

A job layout that works

  1. analyze-test on Linux for every push: pub get, analyze, tests.
  2. build-android on Linux for every push to PRs touching android/ or lib/.
  3. build-ios on macOS for main and tags only.

Use path filters and concurrency to cancel superseded runs.

What more CPU cores buy you

Flutter tests run in parallel and Gradle uses several cores, so a bigger Linux machine helps analyze, test and Android builds. The Xcode step does not benefit from Linux cores at all. We have no Flutter row in our benchmarks, so we do not quote a speedup. compiler.dev's default Linux machine is 8 vCPU / 16 GB at $0.012 per minute. macOS is in early access, with an Apple M4 Pro 7-core VM at $0.028 per minute and a 14-core VM at $0.055 per minute. See pricing and runner pricing. Switching is one line, runs-on: [self-hosted, Linux, compiler-ci].

Related: the Flutter template, React Native guide and GitHub Actions caching.

FAQ

Does flutter-action cache the pub cache?

Yes. With cache: true it caches the SDK, and pub dependencies too unless you set pub-cache separately. You can set custom keys with pub-cache-key.

Can I build an iOS app on Linux?

No. iOS builds need Xcode on macOS. Build Android and run tests on Linux.

Should I pin the Flutter version?

Yes. Use flutter-version or flutter-version-file so the SDK and cache key only change when you decide.

How do I run Flutter integration tests faster?

Run them in a separate job, use a headless target such as a Linux desktop device when your app supports it, and run only on main if they are slow.

Run this workflow on compiler.dev 200 free minutes

Start free