The fastest and easiest way to build and test your Flutter app
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

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 Workflowon: [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.

And it was completed with success, great job.

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 name
property 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:

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.


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

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

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.

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

In the settings section of your Codecov project, you will find the coverage badges to add to your project 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:


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:

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.

Download the file, unzip e check the results:

In the end our complete main.yml
file must look like this:
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.