Article

Setting up CI/CD for Flutter Android apps

Last updated 
Apr 10, 2025
 min read
Episode 
 min
Published 
Apr 10, 2025
 min read
Published 
Apr 10, 2025
 min

Whether you're building MVPs, shipping frequent updates, or maintaining stable production releases, setting up a solid CI/CD pipeline can save hours of manual work and eliminate human error. In this guide, we’ll walk through how to fully automate the build and deployment process of your Flutter Android app using Fastlane and GitHub Actions. If you’re looking to set up CI/CD for Flutter iOS apps, we’ve covered that in detail in a separate guide - check it out here.

From Firebase App Distribution for development builds to Google Play Store releases for production, you’ll learn how to set up an efficient, developer-friendly workflow from start to finish.

What you’ll need for flutter android CI/CD setup

Before you get started, make sure you have the following:

Step 1: Install Ruby on mac

Let's install the latest ruby stable version:

brew install ruby
echo 'export PATH="/usr/local/opt/ruby/bin:$PATH"' >> ~/.zshrc

After installation, restart your terminal or run source ~/.zshrc to apply the changes.

Step 2: Install Fastlane for Flutter Android CI/CD

Fastlane is a tool that automates building and releasing iOS and Android apps. Install it using Homebrew:

brew install fastlane

Step 3: Initialize Fastlane in your Flutter android project

Navigate to your project's android directory and initialize Fastlane:

cd android
fastlane init

During initialization:

  1. Provide the package name for your application when asked (e.g. io.fabric.yourapp)
  2. Press enter when asked for the path to your json secret file
  3. Answer 'n' when asked if you plan on uploading info to Google Play via fastlane (we can set this up later)

After completion, you should see a new fastlane folder Gemfile in your android directory.

Step 4: Setup supply

Supply is a fastlane tool that uploads app metadata, screenshots, and binaries to Google Play. You can also select tracks for builds and promote builds to production!

For supply to be able to initialize, you need to have successfully uploaded an APK to your app in the Google Play Console at least once.

Setting it up requires downloading a credentials file from your Google Developers Service Account.

  1. Open the Google Play Console

Click Account Details, and note the Google Cloud Project ID listed there

  1. Enable the Google Play Developer API  by selecting an existing Google Cloud Project that fits your needs and pushing ENABLE

If you don't have an existing project or prefer to have a dedicated one for fastlane, create a new one here  and follow the instructions

  1. Open Service Accounts on Google Cloud  and select the project you'd like to use
    • Click the CREATE SERVICE ACCOUNT button at the top of the Google Cloud Platform Console page
    • Verify that you are on the correct Google Cloud Platform Project by looking for the Google Cloud Project ID from earlier within the light gray text in the second input, preceding .iam.gserviceaccount.com , or by checking the project name in the navigation bar. If not, open the picker in the top navigation bar, and find the right one.
    • Provide a Service account name  (e.g. fastlane-supply)
    • Copy the generated email address that is noted below the Service account-ID  field for later use
    • Click DONE (don't click CREATE AND CONTINUE as the optional steps such as granting access are not needed):
    • Click on the Actions vertical three-dot icon of the service account you just created
    • Select Manage keys on the menu
    • Click ADD KEY → Create New Key
    • Make sure JSON is selected as the Key type , and click CREATE
    • Save the file on your computer when prompted and remember where it was saved at
  1. Open the Google Play Console  and select Users and Permissions
    • Click Invite new users
    • Paste the email address you saved for later use into the email address field
    • Click on Account Permissions
    • Choose the permissions you'd like this account to have. We recommend Admin (all permissions), but you may want to manually select all checkboxes and leave out some of the Releases permissions such as Release to production, exclude devices, and use Play App Signing
    • Click on Invite User
  2. Test Your API Connection
    fastlane run validate_play_store_json_key
    json_key:/path/to/your/supply.json
  3. Configure the Appfile Create or edit the file android/fastlane/Appfile  with the following:
    • content: json_key_file("fastlane/supply.json")
    • package_name("app.package.name")
  4. Download Existing Metadata (if your app is already on Google Play)  This will download your app's metadata to fastlane/metadata/android
fastlane supply init

Step 5: Configure firebase distribution

Add the Firebase App Distribution plugin to Fastlane:

fastlane add_plugin firebase_app_distribution

Next:

  1. Create and configure a Firebase project for your android app
  2. Generate a service account file
  3. Save the service account JSON file to /android/fastlane/service-account.json

Step 6: Update your fastfile

Edit android/fastlane/Fastfile with the following content:

1default_platform(:android)
2
3platform :android do
4  desc "Push a new development build to Firebase"
5  lane :development do |options|
6    # Handle input parameters
7    version = options[:version] || "1.0.0" 
8    build_number = options[:build_number] || 1
9    release_notes = options[:release_notes] || "Lots of amazing new features to test out!"
10    groups = options[:groups] || "Internal"
11    testers = options[:testers] || ""
12
13    # Clean and build the app
14    sh "flutter clean"
15    sh "flutter build apk --build-name=#{version} --build-number=#{build_number} --flavor development --target=lib/main_development.dart"
16
17    # Upload to Firebase App Distribution
18    firebase_app_distribution(
19      app: "YOUR_FIREBASE_APP_ID",
20      service_credentials_file: "fastlane/service-account.json",
21      apk_path: "../build/app/outputs/flutter-apk/app-development-release.apk",
22      testers: testers,
23      groups: groups,
24      release_notes: release_notes,
25    )
26  end
27
28  desc "Push a new production build to Google Play"
29  lane :production do |options|
30    # Handle input parameters
31    version = options[:version] || "1.0.0" 
32    build_number = options[:build_number] || 1
33    release_notes = options[:release_notes] || "Lots of amazing new features to test out!"
34
35    # Create changelog file
36    changelog_path = "metadata/android/en-US/changelogs/#{build_number}.txt"
37    FileUtils.mkdir_p(File.dirname(changelog_path))
38    File.write(changelog_path, release_notes)
39
40    # Clean and build the app bundle
41    sh "flutter clean"
42    sh "flutter build appbundle --build-name=#{version} --build-number=#{build_number} --flavor production --target lib/main_production.dart"
43    
44    # Upload to Google Play Store
45    # Tracks: "internal", "alpha", "beta", or "production"
46    upload_to_play_store(
47      track: "internal", 
48      aab: "../build/app/outputs/bundle/productionRelease/app-production-release.aab",
49      key: "fastlane/supply.json",
50      metadata_path: "fastlane/metadata/android",
51    )
52  end
53end

Replace YOUR_FIREBASE_APP_ID with your actual Firebase App ID. Also, ensure that file paths match your project's structure.

Step 7: Set up Android app signing

For publishing to the Google Play Store, you need a signing key:

  1. Generate a release keystore file.
  2. Create key.properties file Create a file at android/key.properties  with: storePassword=YOUR_KEYSTORE_PASSWORD
    keyPassword=YOUR_KEY_PASSWORD
    keyAlias=upload
    storeFile=release.keystore
  3. Configure Gradle for signing Update your android/app/build.gradle  file to use the signing configuration. Add this near the top:  And in the android  block, add:
1signingConfigs {
2    release{
3        keyAlias keystoreProperties['keyAlias']
4        keyPassword keystoreProperties['keyPassword']
5        storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
6        storePassword keystoreProperties['storePassword']
7    }
8}
9
10flavorDimensions "default"
11productFlavors { 
12    production {
13        dimension "default"
14        applicationIdSuffix ""
15        manifestPlaceholders = [appName: "<<app-name>>"]
16        signingConfig signingConfigs.release  // Always use release signing for production
17    }
18    development {
19        dimension "default"
20        applicationIdSuffix ".dev"
21        manifestPlaceholders = [appName: "<<[DEV] app-name>>"]
22        signingConfig signingConfigs.debug
23    }
24}
25
26buildTypes {
27    release {
28        if (project.hasProperty('target') && project.property('target').toString().equals('lib/main_production.dart')) {
29            println "Using release signing configuration"
30            signingConfig signingConfigs.release
31        } else if (project.hasProperty('target') && project.property('target').toString().equals('lib/main_development.dart')) {
32            println "Using debug signing configuration"
33            signingConfig signingConfigs.debug
34        } else {
35            println "Using debug signing configuration"
36            signingConfig signingConfigs.debug
37        }
38        minifyEnabled true
39        proguardFiles getDefaultProguardFile('proguard-android.txt')
40    }
41}

Step 8: Setup testing groups

  1. In Firebase Console, create tester groups for your development builds
  2. In Google Play Console, set up closed testing tracks for beta testers

Step 9: Test locally

Before setting up GitHub Actions, test your Fastlane setup locally:

cd android
fastlane production version:"1.0.0" build_number:1 release_notes:"First release"
fastlane development version:"1.0.0" build_number:1 testers:"email@example.com" groups:"<<tester_group>>" release_notes:"First release"

Step 10: Set up GitHub actions workflows for CI/CD

Create the following workflow files in your repository's .github/workflows directory:

Main Workflow (main.yaml)

This workflow runs tests when code is pushed to the main branch or when a pull request is created:

1name: Flutter CI Tests
2on:
3  push:
4    branches:
5      - main
6  pull_request:
7    branches:
8      - main
9jobs:
10  test:
11    name: Run Flutter Tests
12    runs-on: ubuntu-latest
13    steps:
14      - uses: actions/checkout@v3
15      - uses: subosito/flutter-action@v2
16        with:
17          flutter-version: "3.24.5"
18          channel: "stable"
19      - run: flutter --version
20      - name: Get Packages
21        run: flutter pub get
22      - name: Run Tests
23        run: flutter test

Development workflow (deploy_android_development.yaml)

1name: Deploy Android App to Development
2on:
3  workflow_dispatch:
4    inputs:
5      version:
6        description: "App Version (e.g., 1.0.0)"
7        required: true
8        default: "1.0.0"
9      build_number:
10        description: "Build Number (e.g., 42)"
11        required: true
12        default: "1"
13      release_notes:
14        description: "Release Notes"
15        required: true
16        default: "Development Build"
17      groups:
18        description: "Firebase Tester Groups"
19        required: true
20        default: "Internal"
21      testers:
22        description: "Firebase Testers"
23        required: false
24        default: ""
25jobs:
26  deployAndroidDevelopment:
27    runs-on: ubuntu-latest
28    env:
29      FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
30    steps:
31      - name: Checkout repository
32        uses: actions/checkout@v3
33      - name: Setup Flutter
34        uses: subosito/flutter-action@v2
35        with:
36          flutter-version: "3.24.5"
37          channel: "stable"
38          cache: true
39        id: flutter
40      - name: Get Flutter Dependencies
41        run: flutter pub get
42      - name: Setup Ruby and Fastlane
43        uses: ruby/setup-ruby@v1
44        with:
45          ruby-version: "3.3"
46          bundler-cache: true
47          working-directory: android
48      - name: Set up Firebase authentication
49        run: echo "$FIREBASE_SERVICE_ACCOUNT" > android/fastlane/service-account.json
50      - name: Build and Deploy to Firebase App Distribution
51        run: |
52          cd ./android
53          bundle exec fastlane android development \
54            version:${{ github.event.inputs.version }} \
55            build_number:${{ github.event.inputs.build_number }} \
56            release_notes:"${{ github.event.inputs.release_notes }}" \
57            groups:${{ github.event.inputs.groups }} \
58            testers:${{ github.event.inputs.testers }}

Production workflow (deploy_android_production.yaml)

1name: Deploy Android App to Production
2on:
3  workflow_dispatch:
4    inputs:
5      version:
6        description: "App Version (e.g., 1.0.0)"
7        required: true
8        default: "1.0.0"
9      build_number:
10        description: "Build Number (e.g., 42)"
11        required: true
12        default: "1"
13      release_notes:
14        description: "Release Notes"
15        required: true
16        default: "Production Release"
17jobs:
18  deployAndroidProduction:
19    runs-on: ubuntu-latest
20    env:
21      FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
22      RELEASE_SIGNING_KEY: ${{ secrets.RELEASE_SIGNING_KEY }}
23      RELEASE_KEYSTORE_PROPERTIES: ${{ secrets.RELEASE_KEYSTORE_PROPERTIES }}
24      SUPPLY_KEY: ${{ secrets.SUPPLY_KEY }}
25    steps:
26      - name: Checkout repository
27        uses: actions/checkout@v3
28      - name: Setup Java
29        uses: actions/setup-java@v1
30        with:
31          java-version: "17"
32      - name: Setup Flutter
33        uses: subosito/flutter-action@v2
34        with:
35          flutter-version: "3.24.5"
36          channel: "stable"
37          cache: true
38        id: flutter
39      - name: Setup Ruby and Fastlane
40        uses: ruby/setup-ruby@v1
41        with:
42          ruby-version: "3.3"
43          bundler-cache: true
44          working-directory: android
45      - name: Configure Keystore
46        run: |
47          echo "$RELEASE_SIGNING_KEY" > release.jks.b64
48          base64 -d -i release.jks.b64 > android/app/release.keystore
49      - name: Set up Supply Key file
50        working-directory: android
51        run: echo "$SUPPLY_KEY" > fastlane/supply.json
52      - name: Setup store key properties
53        working-directory: android
54        run: echo "$RELEASE_KEYSTORE_PROPERTIES" > key.properties
55      - name: Ensure Fastlane Metadata Directory Exists
56        working-directory: android
57        run: mkdir -p fastlane/metadata/android/en-US/changelogs
58      - name: Build and Deploy to Play Store
59        working-directory: android
60        run: |
61          bundle exec fastlane android production \
62            version:${{ github.event.inputs.version }} \
63            build_number:${{ github.event.inputs.build_number }} \
64            release_notes:"${{ github.event.inputs.release_notes }}"

Step 11: Add GitHub secrets

In your GitHub repository settings, add the following secrets:

  1. FIREBASE_SERVICE_ACCOUNT: The contents of your Firebase service account JSON file
  2. RELEASE_SIGNING_KEY: Base64-encoded contents of your keystore file

base64 -i path/to/your.keystore -o output.txt

  1. RELEASE_KEYSTORE_PROPERTIES: Contents of your key.properties file
  2. SUPPLY_KEY: Contents of your Google Play service account JSON file (supply.json)

Step 12: Test your CI/CD pipeline

Commit and push your changes to GitHub, then:

  1. Go to the "Actions" tab in your GitHub repository
  2. Select the "Deploy Android App to Development" workflow
  3. Click "Run workflow" and fill out the required parameters
  4. Monitor the workflow execution

If everything is set up correctly, your app should be automatically built and deployed to Firebase App Distribution. Once you're ready for a production release, run the "Deploy Android App to Production" workflow to send the app to Google Play.

Conclusion

With your CI/CD pipeline up and running, you’ve taken a major step toward delivering faster, more reliable Android releases. From automatically distributing test builds to your team via Firebase to pushing production-ready versions to the Play Store with confidence, your app’s release process is now streamlined and scalable.

This setup is especially powerful when paired with the right partner. Looking to set up or scale your Flutter app delivery pipeline? Explore our Flutter app development services or contact us to get started.

Authors

Yash Savsani

Senior Software Engineer
A Senior Software Engineer specializing in mobile app development, dedicated to building seamless and high-performing applications in a hybrid environment. Yash emphasizes thorough planning before execution, ensuring every project is efficient and well-structured from inception to completion. Beyond work, he has a passion for travel and exploring new destinations with friends, believing that the best memories are created on the road.

Podcast Transcript

Episode
 - 
minutes

Host

No items found.

Guests

No items found.

Tags

No items found.

Have a project in mind?

Read