DeCODE logo

How to automate build and testing in Flutter with Gitlab

Let's use Docker image by Plugfox, which is targeting Flutter and comes with all dependencies required, such as Android SDK, JVM and so on. In this example, we'll be using free tier of Gitlab, and in order to minify the load on the flow, we'll define the GIT_DEPTH variable, which should speed up the cloning of repository alongside other environment variables, which are required during the building of APK/AppBundle.

Prerequisites for this pipeline

  1. Have a android keystore file. You can quickly generate it using following command:
keytool -genkey -v -keystore example.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
  1. Setup your Gitlab secure files.

  2. Prepare integration test, for example

import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('example test', () {
    testWidgets('dummy test', (tester) async {
      expect(10, greaterThanOrEqualTo(2));
    });
  });
}

Building release appbundle

In order to build the Flutter application, we will use the keystore, which you configured at the prerequisite 1 and upload it to the Gitlab secure files. In order to download secure files, Gitlab requires to execute shell script, let's run it:

curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash

Now, let's configure the signing process. In order to do so, we will use key.properties file. Make sure, that your build.gradle inside flutter application. Check the android/app/build.gradle file, and make sure it has something like this:

def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    // rest of configuration...

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }
}

Now, let's prepopulate the key.properties file with necessary details for signing to work

echo "storeFile=$(pwd)/key.keystore" > android/key.properties
echo "storePassword=$KEYSTORE_PASSWORD" >> android/key.properties
echo "keyAlias=$ALIAS" >> android/key.properties                 ## Testing Flutter application in CI system
echo "keyPassword=$KEY_PASSWORD" >> android/key.properties

Finally, you can execute building command like this:

flutter build appbundle --no-pub --obfuscate --split-debug-info=debug-info-appbundle # if you need appbundle format
flutter build apk --no-pub --obfuscate --split-debug-info=debug-info-apk # if you need apk

Implementing CI testing in Flutter

In order to test, make sure that your Android device is visible with the adb devices command. The following steps assume that you have followed this step.

Optional If you would like to precache the Android SDK, you can use following command for extracting the compileSdk value and pass it to sdkmanager for installation

cat android/app/build.gradle | grep compileSdk | tr -d -c 0-9 | /opt/android/cmdline-tools/latest/bin/sdkmanager "platforms;android-$(cat -)"

Usually, tests assume that your screen is unlocked, so we can schedule unlocking of your screen beforehands by inputting following command:

adb shell input keyevent 26 && adb shell input touchscreen swipe 930 880 930 380

Optional If you want to have the screen opened upon installation of your application, you can modify your MainActivity.kt with the following changes:

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState);
  if (BuildConfig.DEBUG) { // we don't want to have this behavior on release builds, so keep it only to tests
    val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
      setShowWhenLocked(true)
      setTurnScreenOn(true)
      keyguardManager.requestDismissKeyguard(this, null)
    } else {
      // For older versions of Android, you can use the deprecated method as a fallback
      keyguardLock?.disableKeyguard()
      window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }
  }
}

We also were having timeouts while connecting to the device and Flutter VM. We solved it using following command:

sed -i '/).timeout(/,/);/{ s/).timeout.*/);/; t; d;}' /opt/flutter/packages/flutter_tools/lib/src/test/integration_test_device.dart

Finally, execute flutter test path/to/your_test.dart and you're ready to go!

Complete .gitlab-ci.yml

Here's the complete .gitlab-ci.yml for building and testing of Flutter applications

image: plugfox/flutter:3.10.6-android

variables:
 GIT_DEPTH: "1"
 KEYSTORE_PASSWORD: your_keystore_password
 ALIAS: your_alias
 KEY_PASSWORD: your_key_password

stages:
  - build
  - test
  - deploy

e2e_test:
  stage: test 
  interruptible: true
  only:
  - merge_requests
  script: 
    - flutter pub get
    - flutter precache --no-web
    - cat android/app/build.gradle | grep compileSdk | tr -d -c 0-9 | /opt/android/cmdline-tools/latest/bin/sdkmanager "platforms;android-$(cat -)"
    - flutter build apk --no-pub --debug
    - adb shell input keyevent 26 && adb shell input touchscreen swipe 930 880 930 380
    - sed -i '/).timeout(/,/);/{ s/).timeout.*/);/; t; d;}' /opt/flutter/packages/flutter_tools/lib/src/test/integration_test_device.dart
    - flutter test integration_test/app_test.dart

build_flutter_bundle:
  stage: build
  timeout: 2 hours
  variables:
    SECURE_FILES_DOWNLOAD_PATH: './'
  script:
    - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
    - echo "storeFile=$(pwd)/key.keystore" > android/key.properties
    - echo "storePassword=$KEYSTORE_PASSWORD" >> android/key.properties
    - echo "keyAlias=$ALIAS" >> android/key.properties
    - echo "keyPassword=$KEY_PASSWORD" >> android/key.properties
    - flutter pub get --verbose
    - flutter precache --no-web
    - cat android/app/build.gradle | grep compileSdk | tr -d -c 0-9 | /opt/android/cmdline-tools/latest/bin/sdkmanager "platforms;android-$(cat -)"
    - apk add openjdk17-jre-headless openjdk17-jdk # you should install the java version that your application was written with during CI
    - flutter build appbundle --no-pub --obfuscate --split-debug-info=debug-info-appbundle
    - flutter build apk --no-pub --obfuscate --split-debug-info=debug-info-apk
    - cp build/app/outputs/bundle/app-release.aab ./app-release.aab
    - cp build/app/outputs/flutter-apk/app-release.apk ./amazon-release.apk
  artifacts:
    paths:
      - app-release.aab
      - app-release.apk
    expire_in: 1 week
  rules:
   - if: $CI_COMMIT_BRANCH == 'master'
     when: manual
   - when: never

Photo by Tolu Olubode on Unsplash