How BigPay Increased Flutter Developer Velocity: Part 1 — CI/CD for Flutter Android

Bashir Isyaka
bigpay-tech-blog
Published in
12 min readMay 17, 2024

Introduction

In the fast-paced realm of mobile applications, adaptability and efficiency are paramount. At BigPay, we aim to make financial transactions seamless and efficient for our users. Our mobile app, BigPay, provides a simple interface for spending, sending, receiving, and tracking money anywhere in the world. Recently, we transitioned from native mobile apps to a fully Flutter-based application to unify our development process and leverage Flutter’s cross-platform capabilities.

This blog series is targeted at mobile developers, tech enthusiasts, and industry professionals interested in enhancing their development workflows. Our purpose is to share how we enhanced our development workflow for the BigPay mobile app, demonstrating our structured approach to solving complex problems. In Part 1, we’ll guide you through how we implemented a continuous integration and continuous deployment (CI/CD) workflow for our Flutter android app using GitHub Actions, including support for different build flavors for staging and production. You’ll also learn how to automate the process of deploying your builds to testers using Firebase and Diawi.

Furthermore, stay tuned for Part 2, where we’ll delve into setting up a similar CI/CD workflow for our iOS app. We’ll share insights into optimizing the iOS deployment process. Together, these two parts will provide a comprehensive guide to streamlining your development workflow across both Android and iOS platforms.

By detailing our journey, we not only provide practical insights into CI/CD but also reflect the careful planning and execution that characterize our team’s approach to development. Our experience with these best practices can serve as a valuable resource for others looking to refine their processes. As you read through our experiences, we hope you find inspiration and useful strategies that can be applied to your own projects. If you’re passionate about innovation and technology, there’s much to learn and explore with us at BigPay.

Problem

Like many development teams, we used to struggle with time-consuming manual builds, inconsistent build environments, and delays in deployment. These challenges slowed us down and made it hard to deliver new features and updates quickly. We knew there had to be a better way to streamline our workflow and boost our productivity.

Solution

To overcome these challenges, we adopted GitHub Actions for setting up a CI/CD pipeline for our Flutter app. GitHub Actions provided an efficient and flexible solution for automating our build, test, and deployment processes, enabling us to maintain high-quality code and deliver updates more rapidly.

While there are several CI/CD solutions available in the market, including tools like Codemagic, Bitrise, Appcircle and others, we opted for GitHub Actions due to its seamless integration with GitHub itself. Since we already use GitHub as our code repository, leveraging GitHub Actions allowed us to streamline our workflow without the need for additional tools or integrations. This integration not only simplifies our development process but also ensures that our CI/CD pipeline is tightly integrated with our existing codebase, enhancing collaboration and visibility across our development team.

By utilizing GitHub Actions, we were able to centralize our development and deployment processes within the GitHub ecosystem, eliminating the need to manage multiple tools and platforms. This unified approach not only saves time and effort but also promotes consistency and reliability in our development practices.

In addition to its integration with GitHub, GitHub Actions offers a wide range of built-in features and customizable workflows, allowing us to tailor our CI/CD pipeline to suit our specific requirements. Whether it’s automating code testing, managing deployments, or integrating with third-party services, GitHub Actions provides the flexibility and scalability we need to optimize our development workflow.

Prerequisites

Before we dive in, make sure you have the following:
1. A Flutter app hosted on GitHub.
2. Basic understanding of GitHub Actions.
3. GitHub account.
4. Firebase App Distribution
5. Diawi Account

Step 1: Setting Up Your Flutter Project

First, ensure that your Flutter project is properly set up and committed to a GitHub repository. If you haven’t already created a Flutter project, you can do so by running:

flutter create bigpay_app 

Navigate to your project directory and initialize a Git repository if you havne’t done so already:

cd bigpay_app
git init
git remote add origin <your-repository-url>
git add .
git commit -m “Initial commit”
git push -u origin master

Step 2: Configuring Flutter Flavors

For our CI/CD pipeline, we need to handle different build configurations for staging and production environments using flavors. While setting up flavors is beyond the scope of this post, ensure that your Flutter project is configured with the appropriate flavors for staging and production if needed.

Step 3: Creating a GitHub Actions Workflow File

Android

GitHub Actions workflows are defined in YAML files. Create a directory named .github/workflows in the root of your project if it doesn’t already exist. Inside this directory, create a file named build-android.yml (you can name it anything).

now your repository would look like this

Open the newly created build-android.yml and define when the action will start

on:
push:
branches: [master]

This basic configuration will trigger the workflow on every push request to the master branch

Now we need to define the os that will be used to run the action and set up environment for flutter.

on:
push:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

On android we are using ubuntu as the os.
We have to use predefined actions such as checkout@v4, setup-java@v4, flutter-action@v2 to setup java and flutter version 3.19.1.

Now we can run the flutter commands that will build the application.

on:
push:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test

- name: Build staging APK
run: flutter build apk --debug --flavor staging

This configuration will trigger the workflow on every push request to the master branch. It will check out the repository, set up Flutter, install dependencies, run tests, and build the staging APK in debug mode.

Building release mode
To publish on the Play Store, you need to sign your app with a digital certificate.

To sign your app, use the following instructions. If you have done so already, you can skip this part to 3.1 Set up your GitHub Secrets.

Create an upload keystore
On macOS or Linux, use the following command:

keytool -genkey -v -keystore ~/keystore.jks -keyalg RSA \
-keysize 2048 -validity 10000 -alias upload

On Windows, use the following command in PowerShell:

keytool -genkey -v -keystore %userprofile%\keystore.jks ^
-storetype JKS -keyalg RSA -keysize 2048 -validity 10000 ^
-alias upload

This command stores the keystore.jks file in your home directory. If you want to store it elsewhere, change the argument you pass to the -keystore parameter. However, keep the keystore file private; don't check it into public source control!

Gradle Set-Up
Before adding the command to build the release mode, we must first configure the signing config in our app-level build.gradle file in order to utilize secrets variables from our GitHub secrets. We’ll set up these secrets later on.

We’ll move the keystore created earlier into android/app folder as shown below.

Create a file in our android root folder called signing.properties with the following: RELEASE_STORE_FILE, RELEASE_STORE_PASSWORD,RELEASE_KEY_ALIAS and RELEASE_KEY_PASSWORD of our keystore.

Update android gitignore to not check the keystore and /signing.properties to repo.

Add the following code in our app-level build.gradle

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file(“signing.properties”)
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

signingConfigs {
release {
if (keystoreProperties["RELEASE_STORE_FILE"] != null) {
storeFile file(keystoreProperties['RELEASE_STORE_FILE'])
storePassword keystoreProperties['RELEASE_STORE_PASSWORD']
keyAlias keystoreProperties['RELEASE_KEY_ALIAS']
keyPassword keystoreProperties['RELEASE_KEY_PASSWORD']
} else {
project.logger.error("Release signing config not found, release build will fail! Please check signing.properties file")
}
}
}

The code snippet loads the keystore properties from signing.properties for signing the app: It checks if signing.properties exists, then loads its contents into keystoreProperties.

Update buildTypes for release

  buildTypes { 
release {
signingConfig signingConfigs.release
}
debug {
signingConfig signingConfigs.debug
debuggable true
}
}

signingConfig signingConfigs.release: Uses the signing configuration defined in signingConfigs.release we created earlier for the release build.

By using this approach we can locally leave the KeyStore file in the respective path, add it to the .gitignore, and keep being able to build our gradle file.

Encoding the KeyStore
The next step treats the encoding of the KeyStore file we just created. For encoding, we will make use of the popular Base64 encoding scheme. The encoding of the Key store file will allow us to store the file as text in our GitHub Secrets and later on in the GitHub Workflow process decode it back to our original Key store file.

Navigate to the android/app folder that contains the .jks file. Within the respective folder, on macOS execute the following command in terminal
base64 keystore.jks > keystore_base64.txt. If everything went right, you should see a newly created file keystore_base64.txt which contains the encoded text that represents your key store file.
On windows the encoding step can easily be done by using OpenSSL. Download and install it and perform the encoding.

3.1 Set up your GitHub Secrets
The first secret we will add is the encoded Base64 representation of our key store file. To do so, go into your project’s GitHub secrets and add a new GitHub Secret called KEYSTORE_BASE64, copy the content from the keystore_base64.txt file and paste it into the value field.

Next, create another secret called ANDROID_SIGNING_PROPERTIES that contains the text of signing.properties

3.2 Build Release Mode
Now that we have set up our secrets, we can proceed with the actual workflow of building the release mode

on:
push:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

- name: Retrieve signing.properties
env:
ANDROID_SIGNING_PROPERTIES: ${{ secrets.ANDROID_SIGNING_PROPERTIES }}
run: echo "$ANDROID_SIGNING_PROPERTIES" >> ./android/signing.properties

- name: Retrieve keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 --decode > ./android/app/keystore.jks

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test

- name: Build staging APK
run: flutter build apk --release --flavor staging

The newly added code snippet is designed to retrieve and set up Android signing properties and the keystore file required for signing the Android application. Let’s break down what each part does:

Step-by-Step Breakdown
Retrieve signing.properties

- name: Retrieve signing.properties
env:
ANDROID_SIGNING_PROPERTIES: ${{ secrets.ANDROID_SIGNING_PROPERTIES }}
run: echo "$ANDROID_SIGNING_PROPERTIES" >> ./android/signing.properties

Explanation:
env: Sets up an environment variable ANDROID_SIGNING_PROPERTIES with the value from the GitHub Actions secret ANDROID_SIGNING_PROPERTIES.

run: Executes a shell command to echo the value of ANDROID_SIGNING_PROPERTIES into a file named signing.properties located in the ./android directory.
>> appends the value to the file. If the file does not exist, it will be created.

Retrieve keystore

- name: Retrieve keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: echo “$KEYSTORE_BASE64” | base64 --decode > ./android/app/keystore.jks

env: Sets up an environment variable KEYSTORE_BASE64 with the value from the GitHub Actions secret KEYSTORE_BASE64.

run: Executes a shell command to decode the Base64-encoded keystore and save it as a binary file.
echo "$KEYSTORE_BASE64" outputs the Base64-encoded string stored in the KEYSTORE_BASE64 environment variable.

| base64 --decode pipes this output to the base64 command, which decodes the Base64 string back to its original binary format.

> ./android/app/keystore.jks writes the decoded binary content to a file named keystore.jks located in the ./android/app directory.

Build staging apk

- name: Build staging APK
run: flutter build apk — release — flavor staging

The code snippet built the staging apk in release mode.
In the next step of our Workflow, we will upload our outputs apk with the help of Firebase-Distribution-Github-Action.

Step 4 Firebase app distribution

Before adding firebase action you need to setup FIREBASE_APP_ID and CREDENTIAL_FILE_CONTENT in the secrets.

The FIREBASE_APP_ID can be found in the general settings in firebase after you have setup an application.

For CREDENTIAL_FILE_CONTENT Follow the instructions described in the official documentation until reaching point 3 (create and download private JSON key).

Create 2 new secrets FIREBASE_APP_ID with value of firebase app id and CREDENTIAL_FILE_CONTENT with the value of the content of the file generated in the previous point (in the sample below, the name of this secrets are FIREBASE_ANDROID_STAGING_APP_ID and CREDENTIAL_FILE_CONTENT).

Adding firebase action

  - name: Upload staging APK to Firebase
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ vars.FIREBASE_ANDROID_STAGING_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: "android-qa"
file: ./flutter/build/app/outputs/flutter-apk/app-staging-release.apk

In this step of our Workflow, we upload our apk output with the help of Firebase-Distribution-Github-Action to our group qa testers called android-qa

Step 5 Diawi Upload

  • To utilize Diawi for uploading artifacts, you’ll need to have a Diawi.com account. If you haven’t already, please create one by visiting diawi.com.
  • Once you’ve logged in to your Diawi account, navigate to the following link to generate your Diawi API access token: Diawi API access token.
  • Upon generating the token, it will be displayed in your browser. Remember to save the token immediately as it is only viewable ones. This token is essential for authentication when uploading artifacts to Diawi
  • Create 1 new secrets DIAWI_TOKEN with value of the diawi token generated

Adding Diawi action

- name: Upload staging APK to Diawi
uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi-upload-staging
with:
token: ${{ secrets.DIAWI_TOKEN }}
file: ./flutter/build/app/outputs/flutter-apk/app-staging-release.apk

- name: Get Diawi link of uploaded staging APK
run: echo "Diawi link ${{ steps.diawi-upload-staging.outputs.url }}"

In the last step of our Workflow, we upload our apk output to diawi and print the diawi link.

The summarized Workflow looks like the following:

name: build-android
on:
push:
branches: [master]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.1'

- name: Retrieve signing.properties
env:
ANDROID_SIGNING_PROPERTIES: ${{ secrets.ANDROID_SIGNING_PROPERTIES }}
run: echo "$ANDROID_SIGNING_PROPERTIES" >> ./android/signing.properties

- name: Retrieve keystore
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: echo "$KEYSTORE_BASE64" | base64 --decode > ./android/app/keystore.jks

- name: Install dependencies
run: flutter pub get

- name: Run tests
run: flutter test

- name: Build staging APK
run: flutter build apk --release --flavor staging

- name: Upload staging APK to Firebase
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ vars.FIREBASE_ANDROID_STAGING_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.CREDENTIAL_FILE_CONTENT }}
groups: "android-qa"
file: ./flutter/build/app/outputs/flutter-apk/app-staging-release.apk

- name: Upload staging APK to Diawi
uses: rnkdsh/action-upload-diawi@v1.5.5
id: diawi-upload-staging
with:
token: ${{ secrets.DIAWI_TOKEN }}
file: ./flutter/build/app/outputs/flutter-apk/app-staging-release.apk

- name: Get Diawi link of uploaded staging APK
run: echo "Diawi link ${{ steps.diawi-upload-staging.outputs.url }}"

The Workflow in Action

Now that we’ve completed our Workflow, accessing it is straightforward. Just head to the “Actions” tab in our GitHub repository, and you’ll find it there!

Moving forward, whenever you need to share a new build with your internal testers, all it takes is a simple push to the main branch. That’s it! No more hassle or stress about deploying your build to testers. Just one push, and you’re done!

Results

Since implementing the CI/CD pipeline, we have seen significant improvements in our development workflow. Build times have decreased, deployments are more reliable, and our team can focus more on coding new features rather than managing builds and deployments. These changes have substantially increased our developer velocity and overall productivity.

Conclusions

By adopting GitHub Actions for our CI/CD pipeline, we’ve streamlined our development process and improved efficiency. Implementing CI/CD is crucial for ensuring software quality and efficient deployment. By automating build, test, and deployment processes, we can deliver updates to our users quickly and reliably, enhancing their overall experience with BigPay.

Stay tuned for Part 2, where we’ll dive into setting up a similar CI/CD workflow for our iOS app.

References

--

--

Bashir Isyaka
bigpay-tech-blog

I live, breathe and sometimes eat code 24/7. Yes, that is not impossible