Table of contents
Introduction
After or during the development of a Flutter application, usually you'd want to see how it works on real devices and distribute it to other people.
The de facto distribution service for Android apps is Google Play and deploying to it will be covered in this post using GitHub Actions which are very flexible and customizable.
Preparations
Before even considering to deploy an app to Google Play, you need to generate an upload keystore; a repository of certificates and private keys used to verify your application.
To do this, run the following command:
-
On Windows:
keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
-
On Mac/Linux:
keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
If you want to change the destination of the generated keystore, just change the keystore
parameter. Remember the password used for generating the keystore as it will be needed later.
However, keep the file private and do not check it into source control.
Afterwards, create a file called key.properties
in your android
folder with the following contents:
storePassword=#{STORE_PASSWORD}#
keyPassword=#{KEY_PASSWORD}#
keyAlias=uploadkey
storeFile=./upload-keystore.jks
Also keep the file private and do not check it into source control.
Subsequently, the STORE_PASSWORD
and KEY_PASSWORD
text will be replaced by secrets located in your Github secrets tab.
To make Gradle automatically use your upload key while building in release mode, a few changes need to be made in the android/app/build.gradle
file.
Place the following before the android
block:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
...
}
This'll be used to load in the keystore information later.
Additionally, signing configuration info must be added before the buildTypes
block:
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
...
}
The buildTypes
block has to be changed as well, however:
buildTypes {
release {
signingConfig signingConfigs.release
}
}
The app will now be automatically signed each time it is assembled into an APK or AAB.
Flutter uses auto versioning and updates the Android and iOS settings based on the version entry in the pubspec.yaml
file.
In order to create a dynamic version number, we'll have to edit the version number to something we can change later.
name: app
description: Description.
publish_to: "none"
version: 99.99.99+99
Setting up the workflow
To create a workflow using GitHub actions, create a file with the name of your choosing in your root directory: .github/workflows/<fileName>.yaml
Give the workflow a name and define which branch you want to use to trigger the action:
name: Flutter CI
on:
push:
branches: [master]
Creating a version number
The first step of the whole process is creating a version number using generated git tags.
Since we'll need to access some info on the repository, add your personal GitHub token to the repository secrets.
You can generate your GitHub token at this link.
The steps are defined as such:
jobs:
version:
name: Create version number
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Fetch all history for all tags and branches
run: |
git config remote.origin.url https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git fetch --prune --depth=10000
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
with:
versionSpec: "5.x"
- name: Use GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.7
- name: Create version.txt with nuGetVersion
run: echo ${{ steps.gitversion.outputs.nuGetVersion }} > version.txt
- name: Upload version.txt
uses: actions/upload-artifact@v2
with:
name: gitversion
path: version.txt
To get any info about the repository we need to use the actions/checkout
action and furthermore the additional GitVersion
actions.
GitVersion
is not available by default and needs to be installed beforehand if we want to use the features it offers. The action is available here.
We create the version.txt file with the content being the output of the GitVersion execute
command and then upload it to the artifacts section of the repository using the upload-artifact
action.
This file will be used later in order to replace the version number inside the pubspec.yaml
file.
Building the app
Moving on from the previous step, now we attempt to change the version number and build the application.
jobs:
build:
name: Build APK and Create release
needs: [version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get version.txt
uses: actions/download-artifact@v2
with:
name: gitversion
- name: Create new file without newline char from version.txt
run: tr -d '\n' < version.txt > version1.txt
- name: Read version
id: version
uses: juliangruber/read-file-action@v1
with:
path: version1.txt
- name: Update version in YAML
run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml
- name: Update KeyStore password in gradle properties
run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties
- name: Update KeyStore key password in gradle properties
run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
- uses: actions/setup-java@v1
with:
java-version: "12.x"
- uses: subosito/flutter-action@v1
with:
channel: "beta"
- run: flutter clean
- run: flutter pub get
- run: flutter build apk --release --split-per-abi
- run: flutter build appbundle --release
- name: Create a Release in GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
token: ${{ secrets.GH_TOKEN }}
tag: ${{ steps.version.outputs.content }}
commit: ${{ github.sha }}
- name: Upload app bundle
uses: actions/upload-artifact@v2
with:
name: appbundle
path: build/app/outputs/bundle/release/app-release.aab
Using read-file-action
we read the contents of the previously generated version.txt
file and, using the sed
command, we replace the contents of the pubspec.yaml
file (the app version) and the contents of the key.properties
file with the secrets we entered before.
To build flutter applications though, we need an additional action called flutter-action
- available here.
Specify the version of Flutter you want to use; in this case I use the beta
channel because of the experimental features that are needed in my application.
Firstly we clean the project using the flutter clean
command, then get the required packages with flutter pub get
, build the apk using flutter build apk --release --split-per-abi
and finally finish it off with a bundle generation: flutter build appbundle --release
.
After all of that is done we create a release using the release-action
action, located here and upload the app bundle we just generated as it will be needed in the next workflow step.
Google Play deploy preparations
Before reviewing the final step, a Google service account on the Google Cloud platform needs to be set up in order to deploy to the Google Play Console.
Create a new project and create a service account:
After making an account, click on the three dots on the right side and select Manage keys
to generate a new access key which will be used to deploy the app.
When making the key, the JSON option is encouraged:
Copy the contents of the JSON file to your GitHub secrets tab under the name PLAYSTORE_ACCOUNT_KEY
.
After creating the service account, enable API access on your Google Play Console project and link it with the Google Cloud Platform project.
Service accounts should automatically be displayed under the Service accounts section.
Deploying to Google Play and creating a release
⚠️ Important note: You may need to create an initial release by manually uploading an Android bundle and filling out the rest of the required info
The final step in the workflow is deploying the generated AAB file to the Play Store onto a track of your choosing and then creating a release on GitHub and Google Play.
jobs:
release:
name: Release app to internal track
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get appbundle from artifacts
uses: actions/download-artifact@v2
with:
name: appbundle
- name: Release app to internal track
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: com.app.package_name
releaseFiles: app-release.aab
track: alpha
status: completed
Using the checkout
action we get the AAB from the artifacts section and afterwards use the upload-google-play
action to upload the bundle to the Google Play Console.
The upload-google-play
action is located here. It offers quite a few parameters used to release the app, the most important being:
- serviceAccountJsonPlainText - The JSON record fetched from the Google Cloud Platform service account
- packageName - The package name generated from your
pubspec.yaml
file, you can change this at any point before an initial release, but it needs to stay the same on every subsequent release - releaseFiles - Generated files from the previous step, located in the Artifacts section
- track - The track you want to release the file to, in this instance the Closed testing track
- status - An additional parameter indicating the state of the application
The whole workflow file
name: Flutter CI
on:
push:
branches: [master]
jobs:
version:
name: Create version number
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Fetch all history for all tags and branches
run: |
git config remote.origin.url https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
git fetch --prune --depth=10000
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v0.9.7
with:
versionSpec: "5.x"
- name: Use GitVersion
id: gitversion
uses: gittools/actions/gitversion/execute@v0.9.7
- name: Create version.txt with nuGetVersion
run: echo ${{ steps.gitversion.outputs.nuGetVersion }} > version.txt
- name: Upload version.txt
uses: actions/upload-artifact@v2
with:
name: gitversion
path: version.txt
build:
name: Build APK and Create release
needs: [version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get version.txt
uses: actions/download-artifact@v2
with:
name: gitversion
- name: Create new file without newline char from version.txt
run: tr -d '\n' < version.txt > version1.txt
- name: Read version
id: version
uses: juliangruber/read-file-action@v1
with:
path: version1.txt
- name: Update version in YAML
run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml
- name: Update KeyStore password in gradle properties
run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties
- name: Update KeyStore key password in gradle properties
run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
- uses: actions/setup-java@v1
with:
java-version: "12.x"
- uses: subosito/flutter-action@v1
with:
channel: "beta"
- run: flutter clean
- run: flutter pub get
- run: flutter build apk --release --split-per-abi
- run: flutter build appbundle --release
- name: Create a Release in GitHub
uses: ncipollo/release-action@v1
with:
artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
token: ${{ secrets.GH_TOKEN }}
tag: ${{ steps.version.outputs.content }}
commit: ${{ github.sha }}
- name: Upload app bundle
uses: actions/upload-artifact@v2
with:
name: appbundle
path: build/app/outputs/bundle/release/app-release.aab
release:
name: Release app to internal track
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get appbundle from artifacts
uses: actions/download-artifact@v2
with:
name: appbundle
- name: Release app to internal track
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAYSTORE_ACCOUNT_KEY }}
packageName: com.app.package_name
releaseFiles: app-release.aab
track: alpha
status: completed
Conclusion
Even though setting up automatic deployments is a chore, it is extremely useful in the long run because of the lack of additional work needed to upload apps to external sources.