Creating a Complete GitHub Workflow for Flutter | by Edson Moisinho | May, 2022

The fastest and easiest way to build and test your Flutter app

Photo by Sebastian Voortman at Pexels

In this article I will show how to set a simple and easy Flutter workflow that will:

  • Build your source code
  • Run unit tests
  • Upload the code coverage
  • Run integrated tests
  • Generate and upload an APK file
  • Take integrated tests screenshots and upload the files

Create our Flutter example app

Before creating our workflow we need one app, and for now, the default template is enough

Create a new Flutter app and run it in an android emulator or physical device.

I have named my project as flutter_workflow_example

Flutter default app

Next, create a new GitHub repository and push all the code to it.

Git Hub Actions

A GitHub Action is a free CI/CD platform that allows workflow creation for building, testing, publishing, and many other possibilities completely integrated with GitHub.

Users can collaboratively create and share new actions on the platform which is a smart and efficient way of promoting collaboration and code reuse.

Creating a GitHub Action for your Flutter app is simple and straightforward, and it could be done in seconds, so let’s create our workflow, and then I will explain it step by step how it works:

First, create the .github/workflows/main.yml file, and paste the code below on it:

name: Flutter Workflow

on: [push, workflow_dispatch]
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '2.10.0'
channel: 'stable'

- name: Install dependencies
run: flutter pub get

Push it this to your repository and it’s done, you already have a workflow in place.

Below we can see the workflow running after the push.

Flutter workflow option in the repo home page

And it was completed with success, great job.

Workflow executed and return success

And this is enough to have your first action that builds and tests your code, that’s all folks, thank you for reading, and have a great week, bye bye see you next time.

Ops, just kidding, there is still a lot to do, then, let’s check step by step what we just did.

name: Flutter Workflow

That first line is quite simple, the nameproperty will define the workflow title, in case of having multiple workflows, provide a good name to exemplify well what it is supposed to do.

on: [push, workflow_dispatch]

The on property will define when to start the workflow, there are multiple options you can choose, and the push option will make the workflow run every time a new push is executed, the workflow_dispatch options allow starting a new workflow directly from the GitHub interface.

jobs:
build:

The jobs section defines the jobs that will be executed in the workflow, in this case, the first job name is build

runs-on: macos-latest

The next section will define the type of machine that will be used in the workflow, in our case I am using a machine with the latest macOS installed

steps:

Here we will define all the steps of the workflow, we usually provide a -name and an uses or run options

- name: Checkout code
uses: actions/checkout@v3

In this first step, we are executing the actions/checkout which is an action responsible for checking out the repository code of the workflow

The text defined in the -name The option will be displayed in the workflow execution.

- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '2.10.0'
channel: 'stable'

- name: Install dependencies
run: flutter pub get

The next action is the subosito/flutter-action that is responsible to set the Flutter environment in our workflow, note that we can customize the Flutter version and execute Flutter commands

We will use other actions in the next sections, feel free to visit their repositories and check all the options available

Running unit tests

The Flutter project default template contains the test/test_widget.dart file with a very simple unit test, but enough to run in our workflow.

Add this line to your main.yml and push it to the repository:

- name: Run unit tests
run: flutter test --coverage

The commandflutter test will run the unit tests, the --coverage option is necessary to generate the .lcov file we will use later in the code coverage report

After pushing this change to the repository, the workflow will execute again but this time it will run the new unit test step:

Unit test step execution in the workflow

Create a code coverage report

There are many ways of creating a code coverage report, I will use Codecov which is a very easy and complete tool to handle code coverage information.

Log into the Codecov using your GitHub credentials and allow access to search for your repositories.

Click on the Not yet setup option to see your recently created project and click on the setup repo option.

Not yet setup option
My recently created project

The second step, we will find the Codecov Token like the image below:

Codecov Token

On the GitHub repository page, click in Settings -> Secrets -> Actions and click in the New repository secretbutton

GitHub’s new secret option

Then, add the codecov/action to your main.yml file:

- name: Upload to code coverage
uses: codecov/codecov-action@v1.2.2
with:
token: ${{secrets.CODECOV_TOKEN}}
file: coverage/lcov.info

After pushing it, the pipeline will execute again, but this time after running your unit tests it will upload the coverage information to Codecov and it will generate a new coverage report.

Codecov upload action executing

Refresh the Codecov portal and you will be able to see a new report and statistics about your project code coverage

Code coverage report and statistics

In the settings section of your Codecov project, you will find the coverage badges to add to your project readme.md file

Codecov badge settings
Code coverage badge in the readme.md file

Running integration tests

Integration tests unit tests will not test a simple piece of code, instead of that, they will execute the unlike whole application and test as if it was a user, in our case the integration test must open the app, tap the plus button and check if the number increased in the screen.

So, let`s create our integrated test:

In the pubspec.yaml add the integration_test dependency

dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter

Then, add a new test to the file integration_test/app_test.dart

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

import '../lib/main.dart' as app;

void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () {
testWidgets('tap on the floating action button, verify counter', (tester) async {
app.main();
await tester.pumpAndSettle();

// Verify the counter starts at 0.
expect(find.text('0'), findsOneWidget);

// Finds the floating action button to tap on.
final Finder fab = find.byTooltip('Increment');

// Emulate a tap on the floating action button.
await tester.tap(fab);

// Trigger a frame.
await tester.pumpAndSettle();

// Verify the counter increments by 1.
expect(find.text('1'), findsOneWidget);
});
});
}

Flutter provides a very easy and light way to run integration tests using ChromeDriver and we can use it following the Flutter cookbook integration test introduction, (where I got the test above) but in our case, we will use something different, instead of using a fake device, we will boot an android emulator inside our workflow and then, execute our tests.

To do that we will use the reactivecircus/android-emulator-runner action, providing the api-level of our project, and the integration test script to be executed when the emulator becomes available.

- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v1
with:
api-level: 29
script: flutter test integration_test

After pushing it, we can see in the logs the emulator booting and the execution of the tests:

Integration tests starting
Integration tests executed successfully

Building and uploading the APK

For every build a Flutter APK file will be generated, you can take advantage of the workflow and upload this APK in the build if you want to download and install it on any Android device or emulator.

To do this, we first need to run the flutter build apk command to our already defined subposito flutter action

- name: Install Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '2.10.0'
channel: 'stable'
- run: flutter build apk

And then, add the upload-artifact action and provide the APK physical path

- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: release-apk
path: build/app/outputs/apk/release/app-release.apk

In the workflow execution logs we can see the upload action details:

And we can find the APK file in the artifact section of the job execution:

Job summary and uploaded artifact

Taking tests screenshots

To take screenshots during our integration tests we will need a test driver, so, add the file test_driver/main_test.dart to your project and paste the following code:

import 'dart:io';

import 'package:integration_test/integration_test_driver_extended.dart';

Future<void> main() async {
try {
await integrationDriver(
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
final File image = await File('screenshots/$screenshotName.png').create(recursive: true);

image.writeAsBytesSync(screenshotBytes);
return true;
},
);
} catch (e) {
print('onScreenshot - error - $e');
}
}

Now, let’s adapt the integration test to take the screenshot when executing the tests:

First, add this statement in the first line of the main method

final binding = IntegrationTestWidgetsFlutterBinding();

Add the following takeScreenshot method to your test file

takeScreenshot(tester, binding, name) async {
if (Platform.isAndroid) {
try {
await binding.convertFlutterSurfaceToImage();
} catch (e) {
print("TakeScreenshot exception $e");
}
await tester.pumpAndSettle();
}

await binding.takeScreenshot(name);
}

You will also need to import the dart.io package

import 'dart:io';

Now we can call the method takeScreenshot every time we want, I am adding it twice to the existing test below:

testWidgets('tap on the floating action button, verify counter', (tester) async {
app.main();
await tester.pumpAndSettle();

// Verify the counter starts at 0.
expect(find.text('0'), findsOneWidget);
await takeScreenshot(tester, binding, 'shot-1');

// Finds the floating action button to tap on.
final Finder fab = find.byTooltip('Increment');

// Emulate a tap on the floating action button.
await tester.tap(fab);

// Trigger a frame.
await tester.pumpAndSettle();
await takeScreenshot(tester, binding, 'shot-2');

// Verify the counter increments by 1.
expect(find.text('1'), findsOneWidget);
});

Next, in the action script parameter, change it to execute the following new script:

- name: Run integration tests
uses: reactivecircus/android-emulator-runner@v1
with:
api-level: 29
script: flutter drive --driver=test_driver/main_test.dart --target integration_test/app_test.dart

executing the integration tests, so, start the emulator or attach a physical device locally and execute the script above to test if the screenshots will be generated.

Last, but not least, add the code to upload the screenshots to the artifact section

- name: Upload Screenshoots
uses: actions/upload-artifact@v3
with:
name: Test result screenshots
path: screenshots/*.png

After pushing it to the repository, we can check the artifact section and see the screenshots zip file with all screenshots generated during the integration tests.

Artifact section with the Test result screenshots

Download the file, unzip e check the results:

Generated screenshot files

In the end our complete main.yml file must look like this:

Full main.yml file

Now… that’s it

If you want to check the project I have created, feel free to copy it to my GitHub page.

There are many other ways you can set a workflow or pipeline for Flutter but using a GitHub action is free, easy, and fast — and I strongly recommend it.

I really hope it can help you in your projects, thank you for reading, and see you next.

Leave a Comment