Continuous delivery is the ability to get application updates into production or into the hands of users, safely, quickly and in a sustainable way. App Center is a tool that can help developers deliver their app to the end user. In this blog post I will show you how to use App Center in conjunction with Flutter and Azure Pipelines.
App Center
Visual Studio App Center is a tool that lets users automate and manage the lifecycle of their iOS, Android, Windows, and macOS apps. A user can automate his builds, test on real devices in the cloud, distribute apps to beta testers, and monitor real-world usage with crash and analytics data.
As of now (29-06-2020) App Center has no official support for Flutter. There is an issue on the GitHub page of App Center where users are asking for support but there is no official timeline yet. However, there are some workarounds to get support for Flutter in App Center.
Pipeline
You must first install the Azure Pipelines Flutter extension to distribute Flutter apps using App Center Distribute and Azure Pipelines. It is also a good idea to setup a service connection for App Center in Azure Pipelines, you can read more about that here.
Step 1: Define triggers
The following code snippet makes sure that the pipeline only triggers on pushes to the master branch and tags. The advantage of having multiple triggers is that you can send builds that are triggered by a push to the beta testers group and builds that are triggered by a tag to the public group.
trigger: branches: include: - master - refs/tags/* pr: none
Step 2: Define variables
It’s a best practice use a variables section to make it easy for other developers to update the pipeline in the future.
variables: FlutterChannel: 'stable' FlutterVersion: 'latest' ProjectSlug: '<APP_CENTER_USERNAME>/<APP_CENTER_APP>' ProjectDirectory: '$(Build.SourcesDirectory)' BuildNumber: '$(Build.BuildNumber)' BuildMessage: '$(Build.SourceVersionMessage)' BuildDirectory: '$(ProjectDirectory)/build/app/outputs/apk/release/app-release.apk'
Step 3: Setup environment
You must first install Flutter and setup some environment variables before you can build and distribute the app. It’s recommended to use the macOS-latest
image for the pipeline.
The macOS-latest
image already has Java 7, 8, 11, 12, 13 and 14 installed. You can select the version you want by appending JAVA_HOME_<VERSION>_X64
to the $PATH
variable and setting the $JAVA_HOME
variable to JAVA_HOME_<VERSION>_X64
. You also need to append the FlutterToolPath
variable (exposed by the extension) to the $PATH
variable to allow the other steps in the pipeline to use the flutter command line tools.
jobs: - job: Build pool: vmImage: 'macOS-latest' steps: - task: FlutterInstall@0 displayName: Setup flutter inputs: channel: '$(FlutterChannel)' version: '$(FlutterVersion)' - task: PowerShell@2 displayName: Setup environment inputs: targetType: 'inline' script: | Write-Host "##vso[task.prependpath]$(JAVA_HOME_11_X64)" Write-Host "##vso[task.setvariable variable=JAVA_HOME;]$(JAVA_HOME_11_X64)" Write-Host "##vso[task.prependpath]$(FlutterToolPath)" Write-Host "##vso[task.prependpath]$(FlutterToolPath)/cache/dart-sdk/bin"
Step 4: Changing environment based on triggers
You also need to set two variables to build and distribute the app. The build name which refers to the version number and the distribution group.
It’s nice to use the first 7 characters of the commit sha as the build name for builds that are triggered by a push and the tag for builds that are triggered by a tag. You can set the variables by creating two steps and using conditions to run them on the right trigger.
- task: PowerShell@2 displayName: Setup variables (beta) condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/') inputs: targetType: 'inline' script: | $buildName = "$(Build.SourceVersion)".SubString(0,7) $distributionGroup = "<APP_CENTER_BETA_DISTRIBUTION_GROUP_ID>" Write-Host "##vso[task.setvariable variable=BuildName;]$buildName" Write-Host "##vso[task.setvariable variable=DistributionGroup;]$distributionGroup" - task: PowerShell@2 displayName: Setup variables (public) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/') inputs: targetType: 'inline' script: | $buildName = "$(Build.SourceBranch)".SubString(11) $distributionGroup = "<APP_CENTER_PUBLIC_DISTRIBUTION_GROUP_ID>" Write-Host "##vso[task.setvariable variable=BuildName;]$buildName" Write-Host "##vso[task.setvariable variable=DistributionGroup;]$distributionGroup"
Step 5: Build & distribute
All the dependencies must be installed by using `flutter pub get` before you can build and distribute the app.
- task: CmdLine@2 displayName: Run install inputs: script: 'flutter pub get' - task: FlutterBuild@0 displayName: Run build inputs: target: 'apk' projectDirectory: '$(ProjectDirectory)' buildName: '$(BuildName)' buildNumber: '$(BuildNumber)' - task: AppCenterDistribute@3 displayName: Run distribute inputs: serverEndpoint: 'AppCenter Service Connection' appSlug: '$(ProjectSlug)' appFile: '$(BuildDirectory)' releaseNotesOption: 'input' releaseNotesInput: '$(BuildMessage)' destinationType: 'groups' distributionGroupId: '$(DistributionGroup)'
Step 6: Complete pipeline
The complete pipeline should look somewhat like this:
trigger: branches: include: - master - refs/tags/* pr: none variables: FlutterChannel: 'stable' FlutterVersion: 'latest' ProjectSlug: '<APP_CENTER_USERNAME>/<APP_CENTER_APP>' ProjectDirectory: '$(Build.SourcesDirectory)' BuildNumber: '$(Build.BuildNumber)' BuildMessage: '$(Build.SourceVersionMessage)' BuildDirectory: '$(ProjectDirectory)/build/app/outputs/apk/release/app-release.apk' jobs: - job: Build pool: vmImage: 'macOS-latest' steps: - task: FlutterInstall@0 displayName: Setup flutter inputs: channel: '$(FlutterChannel)' version: '$(FlutterVersion)' - task: PowerShell@2 displayName: Setup environment inputs: targetType: 'inline' script: | Write-Host "##vso[task.prependpath]$(JAVA_HOME_11_X64)" Write-Host "##vso[task.setvariable variable=JAVA_HOME;]$(JAVA_HOME_11_X64)" Write-Host "##vso[task.prependpath]$(FlutterToolPath)" Write-Host "##vso[task.prependpath]$(FlutterToolPath)/cache/dart-sdk/bin" - task: PowerShell@2 displayName: Setup variables (beta) condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/') inputs: targetType: 'inline' script: | $buildName = "$(Build.SourceVersion)".SubString(0,7) $distributionGroup = "<APP_CENTER_BETA_DISTRIBUTION_GROUP_ID>" Write-Host "##vso[task.setvariable variable=BuildName;]$buildName" Write-Host "##vso[task.setvariable variable=DistributionGroup;]$distributionGroup" - task: PowerShell@2 displayName: Setup variables (public) condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/') inputs: targetType: 'inline' script: | $buildName = "$(Build.SourceBranch)".SubString(11) $distributionGroup = "<APP_CENTER_PUBLIC_DISTRIBUTION_GROUP_ID>" Write-Host "##vso[task.setvariable variable=BuildName;]$buildName" Write-Host "##vso[task.setvariable variable=DistributionGroup;]$distributionGroup" - task: CmdLine@2 displayName: Run install inputs: script: 'flutter pub get' - task: FlutterBuild@0 displayName: Run build inputs: target: 'apk' projectDirectory: '$(ProjectDirectory)' buildName: '$(BuildName)' buildNumber: '$(BuildNumber)' - task: AppCenterDistribute@3 displayName: Run distribute inputs: serverEndpoint: 'AppCenter Service Connection' appSlug: '$(ProjectSlug)' appFile: '$(BuildDirectory)' releaseNotesOption: 'input' releaseNotesInput: '$(BuildMessage)' destinationType: 'groups' distributionGroupId: '$(DistributionGroup)'
Conclusion
By using Azure Pipelines and App Center you can easily setup a continuous delivery pipeline for your Flutter project. I hope this post helps you implement your own pipeline. Feel free to leave a comment below if you have any questions.