← Articles

Distributing Flutter apps with Firebase App Distribution

By Ann Tech · 28 October 2024

Firebase App Distribution lets you ship APKs and IPAs to testers without going through the app stores. It's the right tool for QA builds, beta programs, and showing stakeholders a build before release. This guide covers the full pipeline from build to tester.

When to use App Distribution vs TestFlight vs Play Store internal testing

ServiceBest for
Firebase App DistributionCross-platform, flexible groups, any tester email
TestFlightiOS, App Store-approved testers, required for external beta
Play Store internal testingAndroid, must use Google accounts, integrated with Play

App Distribution works for both platforms, accepts any email address (no Google account required for iOS), and integrates with Fastlane and GitHub Actions.

Setup

  1. Enable Firebase App Distribution in the Firebase console
  2. Add firebase_app_distribution to your Fastfile:
bundle exec fastlane add_plugin firebase_app_distribution

Or install the Firebase CLI:

npm install -g firebase-tools
firebase login

Android distribution lane

# android/fastlane/Fastfile

lane :distribute_staging do
  # Build the APK for the staging flavor
  gradle(
    task: 'assemble',
    build_type: 'Release',
    flavor: 'staging',
    properties: {
      'android.injected.signing.store.file' => ENV['KEYSTORE_PATH'],
      'android.injected.signing.store.password' => ENV['KEYSTORE_PASS'],
      'android.injected.signing.key.alias' => ENV['KEY_ALIAS'],
      'android.injected.signing.key.password' => ENV['KEY_PASS'],
    }
  )

  firebase_app_distribution(
    app: ENV['FIREBASE_APP_ID_ANDROID_STAGING'],
    groups: 'qa-team, stakeholders',
    release_notes: release_notes_for_build,
    firebase_cli_token: ENV['FIREBASE_CLI_TOKEN'],
  )
end

private_lane :release_notes_for_build do
  branch = sh('git rev-parse --abbrev-ref HEAD').strip
  commit = sh('git log --oneline -5').strip
  "Branch: #{branch}\n\nRecent commits:\n#{commit}"
end

iOS distribution lane

# ios/fastlane/Fastfile

lane :distribute_staging do
  setup_ci if ENV['CI']

  match(
    type: 'adhoc',
    app_identifier: 'com.example.myapp.staging',
    readonly: ENV['CI'] == 'true',
  )

  build_app(
    workspace: 'Runner.xcworkspace',
    scheme: 'Staging',
    export_method: 'ad-hoc',
  )

  firebase_app_distribution(
    app: ENV['FIREBASE_APP_ID_IOS_STAGING'],
    groups: 'qa-team',
    release_notes: "Staging build from #{`git rev-parse --abbrev-ref HEAD`.strip}",
    firebase_cli_token: ENV['FIREBASE_CLI_TOKEN'],
  )
end

Getting the Firebase CLI token for CI

firebase login:ci
# Outputs a token — store as FIREBASE_CLI_TOKEN secret

GitHub Actions workflow

name: Distribute Staging
on:
  push:
    branches: [staging]

jobs:
  distribute-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.22.0'
      - uses: ruby/setup-ruby@v1
        with:
          working-directory: android
          bundler-cache: true
      - run: flutter pub get
      - name: Decode configs
        run: |
          echo "$GOOGLE_SERVICES" | base64 --decode > android/app/src/staging/google-services.json
          echo "$KEYSTORE" | base64 --decode > android/app/keystore.jks
        env:
          GOOGLE_SERVICES: ${{ secrets.GOOGLE_SERVICES_STAGING }}
          KEYSTORE: ${{ secrets.KEYSTORE_BASE64 }}
      - name: Distribute
        working-directory: android
        run: bundle exec fastlane distribute_staging
        env:
          KEYSTORE_PATH: app/keystore.jks
          KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASS: ${{ secrets.KEY_PASS }}
          FIREBASE_APP_ID_ANDROID_STAGING: ${{ secrets.FIREBASE_APP_ID_ANDROID_STAGING }}
          FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }}

  distribute-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - uses: ruby/setup-ruby@v1
        with:
          working-directory: ios
          bundler-cache: true
      - run: flutter pub get && cd ios && pod install
      - name: Distribute
        working-directory: ios
        run: bundle exec fastlane distribute_staging
        env:
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          FIREBASE_APP_ID_IOS_STAGING: ${{ secrets.FIREBASE_APP_ID_IOS_STAGING }}
          FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }}

Managing tester groups

Create groups in the Firebase console or via CLI:

firebase appdistribution:group:create qa-team "QA Team"
firebase appdistribution:testers:add --emails [email protected] --group qa-team

In Fastlane, reference groups by name:

firebase_app_distribution(
  groups: 'qa-team, design-team, stakeholders',
)

Release notes from changelog

Automate release notes from a CHANGELOG.md:

def latest_changelog_entry
  content = File.read('../CHANGELOG.md')
  entry = content.split(/^## /)[1] # First entry after the heading
  entry.lines.first(10).join # Truncate to 10 lines
end

firebase_app_distribution(
  release_notes: latest_changelog_entry,
)

In-app update prompts

Install the Firebase App Distribution Flutter SDK to prompt testers when a new build is available:

dependencies:
  firebase_app_distribution: ^0.3.0
Future<void> checkForUpdates() async {
  try {
    final tester = await FirebaseAppDistribution.instance.signInTester();
    if (tester == null) return; // Not a registered tester

    final release = await FirebaseAppDistribution.instance
        .checkForUpdate(requireNewerBuild: true);

    if (release != null) {
      // Show update dialog
      showUpdateDialog(release);
    }
  } catch (_) {
    // Not a distribution build — ignore
  }
}

Common pitfalls

Using debug builds. Distribute release builds, even for QA. Debug builds are slow, not representative of production performance, and can't be tested on device farms.

Missing ad-hoc provisioning for iOS. TestFlight uses app-store export. App Distribution needs ad-hoc — different provisioning profiles, different devices list. Testers must have their UDID registered in the profile or in the Firebase tester list.

Token expiry. FIREBASE_CLI_TOKEN from firebase login:ci expires after 1 hour of inactivity on some configurations. Use a service account JSON instead for more reliable CI.

Sign in to like, dislike, or report.

Distributing Flutter apps with Firebase App Distribution — ANN Tech