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
- 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
Setup your Gitlab secure files.
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