Distribute Flutter app to Firebase and stores from Codemagic


For last couple of weeks I was configuring a build pipeline for upcoming Flutter Europe conference app. I decided to use Codemagic to see if it’s possible to handle multiple flavors and fastlane builds there. After few discussions with Codemagic support on their Slack and many build minutes I came up with pretty decent configuration. This post will showcase how to prepare your workflow to build Flutter app in multiple flavors and distribute it to Firebase App Distribution or any of the stores.

Contents

Foreword

The first thing to have in mind is that I wanted my app pipeline be agnostic of CI environment. The app should be able to build on my computer, any of contributors and of course on CI/CD. The best way to achieve that is to use fastlane for as many steps as possible.

Fastlane is a very handy tool that automates almost every task a mobile developer can think of. In my case I use fastlane to handle signing and provisioning of iOS app and distribution for both platforms.

If you want to learn how to prepare your app to use flavors take a look at my other blog post. Flavors allow you to have subtly different configuration for each environment. Each flavor has its own bundle id, icon and can be connected to different Firebase account.

Flavored icons of the app

You can also investigate my Flutter configuration in the project repository. The app is still in its early stage, but overall template is already done.

We’ll be using Codemagic in this tutorial. It’s still a very young CI/CD platform but they came a long way and available tools allow to have complete pipeline with tests, QA distribution and continuous updates to production.

Environment variables

After you successfully connected your app to Codemagic you should set up all necessary environment variables. I like to store all secret values and files right there. Codemagic doesn’t have any generic file storage support (yet, but you can vote for the feature request here), so the only way to pass files to the machine is to encode them in base64, save as env variable, and then decode them during build.

Environment Variables on Codemagic

List of all environment variables used in my setup:

  • ANDROID_KEYSTORE_ALIAS
  • ANDROID_KEYSTORE_PASSWORD
  • ANDROID_KEYSTORE_PATH
  • ANDROID_KEYSTORE_PRIVATE_KEY_PASSWORD
  • APK_PATH - required for Firebase step to find correct apk file
  • APP_ID - bundle id of application e.g. dev.fluttereurope.conferenceapp
  • APP_STORE_CONNECT_TEAM_ID
  • APPLE_DEVELOPER_TEAM_ID - used by fastlane match step, this is similar to U9XX9XXXX9
  • SSH_KEY - key used to access private repository with Apple provisioning profiles and certificates
  • CI - Codemagic theoretically has this variable set, but for some reason in my build fastlane had problems with respecting it, so I added it on my own with some dummy value
  • FASTLANE_PASSWORD - password used to log in into Apple Developer Console by fastlane
  • FASTLANE_USER - user used to log in into Apple Developer Console by fastlane
  • FCI_KEYSTORE_FILE - encoded Android keystore
  • FIREBASE_ANDROID_TEST_APP_ID - take a look at docs here
  • FIREBASE_CLI_TOKEN - token used by Firebase CLI, learn more here
  • FIREBASE_IOS_TEST_APP_ID - take a look at docs here
  • FIREBASE_TESTERS - name of testers group that app should be distributed to
  • FLAVOR - in my case it’s tst and prod
  • GOOGLE_SERVICE_ACCOUNT_KEY_BASE64
  • GOOGLE_SERVICE_ACCOUNT_KEY
  • GOOGLE_SERVICES_JSON_BASE64
  • GOOGLE_SERVICES_JSON_PATH
  • GOOGLE_SERVICES_PLIST_BASE64
  • GOOGLE_SERVICES_PLIST_PATH
  • KEYCHAIN_NAME - we need to create temporary keychain to store Apple credentials
  • MATCH_PASSWORD - password used to encrypt provisioning profiles and certificates in the private repository, learn more here
  • TARGET_FILE - this is the entry point of the app e.g. main_tst.dart
  • VERSION_NUMBER - major and minor part of app version e.g. 1.0

Storing files in environment variables

In order to save some file as env variable you need to encode it in base64. To do this you can simply run:

openssl base64 -in ./GoogleService-Info_prod.plist -out ./GoogleService-Info_prod.plist_base64.txt

In order to decode it and save to file on the build machine just execute in custom post clone step:

echo $GOOGLE_SERVICES_JSON_BASE64 | base64 --decode > $GOOGLE_SERVICES_JSON_PATH

My full post-clone step looks like this:

#!/usr/bin/env sh
set -e

echo $FCI_KEYSTORE_FILE | base64 --decode > $ANDROID_KEYSTORE_PATH
echo $GOOGLE_SERVICE_ACCOUNT_KEY_BASE64 | base64 --decode > $GOOGLE_SERVICE_ACCOUNT_KEY
echo $GOOGLE_SERVICES_JSON_BASE64 | base64 --decode > $GOOGLE_SERVICES_JSON_PATH
echo $GOOGLE_SERVICES_PLIST_TST_BASE64 | base64 --decode > $GOOGLE_SERVICES_PLIST_TST_PATH

echo "${SSH_KEY}" > /tmp/bkey

# adding custom ssh key to access private repository
chmod 600 /tmp/bkey
cp /tmp/bkey ~/.ssh/bkey
ssh-add ~/.ssh/bkey

# installing Firebase CLI
curl -sL firebase.tools | bash

# update fastlane and install all dependencies
sudo gem update fastlane
bundle install

Fastlane and Ruby 2.3.6

Codemagic machines have only one version of Ruby installed (2.3.6). This isn’t a problem as we have access to rbenv. However, switching to desired Ruby version takes time and that’s the valuable commodity on codemagic. Feature request for cached multiple Ruby versions is here.

Unfortunately some plugins and gems don’t support Ruby 2.3.6, so a workaround for this is to set some dependencies directly in fastlane’s Pluginfile [Source].

gem 'fastlane-plugin-firebase_app_distribution'
gem 'signet', '0.11.0'
gem 'fastlane-plugin-badge'
gem 'google-cloud-env', '1.2.1'
gem 'google-cloud-core', '1.3.2'

Building the app

This process is pretty straightforward. I’m using the Codemagic step here. Few things to remember are:

  • Firebase App Distribution accepts only apk files at the moment. They might implement support for app bundles next year. AppCenter, however, supports them already.
  • You can use Android app bundles for production build where we deploy app to Google Play
  • I pass some custom parameters to flutter build related to flavor and build numbers, e.g.:
    • Android: --target $TARGET_FILE --flavor $FLAVOR --build-name=$VERSION_NUMBER --build-number=$BUILD_NUMBER
    • iOS: --no-codesign --target $TARGET_FILE --flavor $FLAVOR --build-name=$VERSION_NUMBER --build-number=$BUILD_NUMBER - we’ll sign the app later with fastlane
  • I use gradle config to sign the app. Take a look at it here.

Build step on Codemagic

Distributing the app

Now it’s time for the grande finale. Unfortunately we’ll have to rebuild the iOS app. This is a known limitation of Flutter. We tried to find a workaround for that by building only AOT version of Flutter and then archive it with Xcode, but some problems with pods were arising very randomly. If you have any ideas on how to improve here, please let me know!

In the fastfile there are several lanes configured. In the iOS lane we need to create or update provisioning profiles, set signing style to manual, update the Xcode project and finally archive it. This may take several minutes depending on the plugins you use.

In case of Android we only deploy our app to Firebase or Google Play. Deployment to Google Play requires a service account. Learn more here.

My post-build script is simply:

#!/usr/bin/env sh
set -e

bundle exec fastlane android tst
bundle exec fastlane ios tst

Wrapping up

Successful build on Codemagic

I hope this post will save you some time while configuring the build pipeline on Codemagic. I know my approach is a bit unorthodox but it’s been successful for over a year now on various platforms like custom Mac Mini, Bitrise and now Codemagic.

Special thanks to Maciek for his support in configuring a platform-agnostic multi-flavor build pipeline.