Flutter is a toolkit provided by Google for building cross-platform applications, including Android, iOS, and web apps. As more apps are written using it and Dart (the underlying language), it’s becoming more important to consider how developers can package and deliver their code to their users. Leveraging CircleCI and Fastlane, this is a relatively straightforward process that can be automated.

Installing Flutter and creating your first app can be done by following these guides:

The default “hello world” app will suffice. We won’t be dealing much with the application itself. Push it to a repository on GitHub.

Ensure that you are set up to publish apps using Google Play or the Apple App Store - steps for each will vary, so be sure to follow the steps outlined for the respective provider.


Read more on continuous integration for mobile application development.

Android

When publishing Android apps, there are a few requirements to satisfy before implementing Fastlane. An “upload key” is required so that Google can verify the authenticity of the app before accepting it. Steps for setting this up can be found here. It’s recommended to use AppBundles when building Android apps, as it allows Google to build individual APKs for different platforms while keeping filesize down. Take note of where you save your key.jks file. While it can live in your home directory, having it saved to a git-ignored directory in your project will come in handy when configuring the key.properties file.

Two things from this guide will need to be added to CircleCI as enviornment variables: the key.properties file and the key.jks file. This will allow the continuous integration pipeline to properly build and sign the application. Since one file requires newlines to be present, and another is a binary file, it’s recommended that they are base64-encoded prior to adding them as enviornment variables to preserve the data.

You can encode them by running the following command:

cat <file> | base64 -w 0 # -w 0 will ensure no newlines are present.

These files can then be re-added during the job with the following command:

echo "$ENV_VARIABLE_NAME" | base64 --decode > <file>

You will also want to set up a Fastlane service account while creating your Android setup. You can read more about here. Be sure to keep your Service Account JSON file handy. We’ll be adding that to the pipeline shortly. Also note that commands should be run from the android directory under your Flutter project.

Adding the service account required by Fastlane can be done by adding the service account JSON file contents as the SUPPLY_JSON_KEY_DATA enviornment variable. This is not to be confused with SUPPLY_JSON_KEY which is expected to be a file path to the file.

Fastlane

Now that our enviornment variables are in place and we can build a signed .aab file using Flutter, it’s time to tell Fastlane how to do this for us. We’ll start with Android. Opening android/fastlane/Fastfile will reveal some defaults for building Android apps, but they won’t work for our use case. We’re building with Flutter.

First, let’s define a new “lane”. This is a set of instructions that Fastlane will execute when called. For now, we’ll be pushing our app to the “beta” channel, not to production.

desc "Deploy a new beta build to Google Play"
lane :beta do

end

This is the basic structure of a lane. Next, we’ll want to grab a build number which is an incrementing number used as a reference for the version of the app (which differs from the user-facing semver).

Add the following to the lane:

  build_number = number_of_commits()

This ensures that the build_number is the number of commits that have been made to the Git repository. This can also be manually incremented in the pubspec.yaml file that Flutter uses to build the package.

Next, we’ll do the building. We need to go to the parent directory, run a few shell commands to fetch our packages, clean the previous build (if available), then build the appbundle with our build number.

  Dir.chdir "../.." do
    sh("flutter", "packages", "get")
    sh("flutter", "clean")
    sh("flutter", "build", "appbundle", "--build-number=#{build_number}")
  end

Finally, we can send it to the app store on the beta channel.

  upload_to_play_store(track: 'beta', aab: '../build/app/outputs/bundle/release/app.aab')

In the end, your lane will look like this:

desc "Deploy a new beta build to Google Play"
lane :beta do
  build_number = number_of_commits()
  Dir.chdir "../.." do
    sh("flutter", "packages", "get")
    sh("flutter", "clean")
    sh("flutter", "build", "appbundle", "--build-number=#{build_number}")
  end
  upload_to_play_store(track: 'beta', aab: '../build/app/outputs/bundle/release/app.aab')
end

Go ahead and save the file, and then run fastlane beta. It will run through the steps, building the app and pushing the application for you.

Now, we’ll do the same for iOS, but replacing some of the commands with their respective iOS commands. This should be done in the ios/Fastlane directory.

desc "Deploy a new build to Testflight"
lane :beta do
  Dir.chdir "../.." do
    sh("flutter", "packages", "get")
    sh("flutter", "clean")
    sh("flutter", "build", "ios", "--release", "--no-codesign")
  end
  build_ios_app(scheme: "MyApp")
  upload_to_testflight
end

Note: The app is built twice. The first build builds against the latest Flutter toolchain, while the second build builds the actual .ipa file that will be uploaded to Testflight.

Now, with CircleCI

Now that we have everything in place, it’s time to get our CircleCI configuration written so that a push to a branch is translated to a new deployment.

iOS

For deploying with iOS, CircleCI already has docs to cover this! Steps for setting up Fastlane deployment with Flutter is similar to the steps outlined here and here.

Android

When it comes to the Docker image to pick, there are two options. Either you can build and push the Docker image used by the Flutter project, or you can pull gmemstr/flutter-fastlane-android:29.0, which is built on top of an existing CircleCI Android Docker image and installs Flutter and Fastlane, avoiding additional overhead of installing it during runtime.

Also ensure that the enviornment variables mentioned earlier are added to your project. Namely:

  • PLAY_STORE_UPLOAD_KEY - A base64-encoded key.jks file
  • PLAY_STORE_UPLOAD_KEY_INFO - A base64-encoded file containing the password and other info regarding the upload signing key
  • SUPPLY_JSON_KEY_DATA - Your Google Service Account JSON string for authentication

In your projects root, add a .circleci folder, and in that folder add a circleci.yml file. Add the following configuration to the newly created file:

version: 2.1

executors:
  android-flutter:
    docker:
      - image: gmemstr/flutter-fastlane-android:29.0
    environment:
      TERM: dumb
      _JAVA_OPTIONS: "-Xmx2048m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
      GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m"'

The snippet above defines an executor we can reuse later in our configuration thanks to the use of reusable configuration and orbs in CircleCI 2.1. It also includes some environment variables to ensure Java doesn’t consume all of our memory while building the app. Next, we’ll define the job we want to run the commands.

jobs:
  beta_deploy:
    executor: android-flutter
    steps:
      - checkout
      - run: echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > key.jks
      - run: echo "$PLAY_STORE_UPLOAD_KEY_INFO" | base64 --decode > android/key.properties
      - run: cd android && fastlane beta

Finally, we’ll define our workflow which will only run the job when the new code is pushed to the beta branch.

workflows:
  deploy:
    jobs:
      - beta_deploy:
          filters:
            branches:
              only: beta

In the end, your configuration will look like this:

version: 2.1

executors:
  android-flutter:
    docker:
      - image: gmemstr/flutter-fastlane:latest
    environment:
      TERM: dumb
      _JAVA_OPTIONS: "-Xmx2048m -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap"
      GRADLE_OPTS: '-Dorg.gradle.jvmargs="-Xmx2048m"'
jobs:
  beta_deploy:
    executor: android-flutter
    steps:
      - checkout
      - run: echo "$PLAY_STORE_UPLOAD_KEY" | base64 --decode > key.jks
      - run: echo "$PLAY_STORE_UPLOAD_KEY_INFO" | base64 --decode > android/key.properties
      - run: cd android && fastlane beta

workflows:
  deploy:
    jobs:
      - beta_deploy:
          filters:
            branches:
              only: beta

Set up your project on CircleCI. Once you click Set Up Project, you will be brought to a text editor that will allow you to copy in the config from above. Click Start Building. Now every new commit to the beta branch will result in a deploy.