[Flutter] Deploy with Fastlane automatically

2023-03-18 hit count image

Let's see how to deploy the app that is developed by Flutter with Fastlane automatically.

Outline

If you develop the app by Flutter, you’ll deploy the app to make users use it. The Flutter official documentation introduces how to deploy the Flutter app. So, you can deploy the app by referring to it.

Fastlane is a tool to help you deploy a native app or hybrid app(Flutter, React Native, etc).

In this blog post, I will show you how to deploy the Flutter app with Fastlane.

Fastlane

fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.

In this blog post, we will assume that screenshots and app store registration information are already registered, and will only cover the part that automates deployment for testing and release.

If you want to know other information like how to create the screenshots and app store registration information, see the following official site link.

On the Flutter official site, you can see how to use Fastlane for CI/CD, so it’s also useful to refer to it.

Install Fastlane

To deploy the app developed with Flutter via Fastlane, you need to install Fastlane first. Execute the following command to install Fastlane.

# Using RubyGems
sudo gem install fastlane -NV

# Alternatively using Homebrew
brew cask install fastlane

The official site introduces how to install with Homebrew and how to install with RubyGems.

In my case, I tried to install it with Homebrew first, but I failed to deploy the app when I tested it. So, I recommend you install it via RubyGems. When you have any issues with Homebrew, try to use RubyGems to install it again.

iOS

Let’s see how to deploy the Flutter app to iOS via Fastlane.

initialize Fastlane for iOS

Execute the following commands to initialize Fastlane for iOS.

cd ios
fastlane init

After executing the commands, you can see the following screen.

Deploy with Fastlane automatically - iOS initialization

In this blog post, I will introduce how to deploy the Flutter app to Testflight and App store. So, select 2 or 3 to go to the next.

2. Automate beta distribution to TestFlight
3. Automate App Store distribution

In here, I selected 2 option.

Deploy with Fastlane automatically - select iOS project

After selecting 2, you can see the screen to select the iOS project. We will deploy the Flutter app, so select 2 to go to the next step.

Deploy with Fastlane automatically - login Apple

When you select 2, you can see the screen to login Apple like the above. To deploy the app to iOS, you should login with Apple store connect account.

Deploy with Fastlane automatically - iOS configuration

I already logged in with Fastlane, so the screen above is shown up. If you’re the first time to configure Fastlane, you will see the two-factor authentication procedure.

In addition to the above screen, you can see Continue by pressing Enter several times. Press the Enter key to finish the configuration.

Fastlane folders and files for iOS

After the configuration, you can see the folders and files are created in the ios folder of the Flutter project like the following.

|- fastlane
|  |- Appfile
|  |- Fastfile
|- Gemfile
|- Gemfile.lock

Let’s see the details about the folders and files.

  • fastlane folder: The folder has Fastlane configuration and execution files.
  • Gemfile, Gemfile.lock: Fastlane is developed by Ruby. These files manage the dependencies of Ruby for Fastlane.

If you uncomment the fastlane/Appfile file that contains the settings to run Fastlane, it looks like the following.

app_identifier("io.github.dev-yakuza.kumoncho")
apple_id("[email protected]")

itc_team_id("119423059")
team_id("WFDJCJXQZ6")

The Appfile file is the Fastlane configuration file for iOS deployment automation. The file is simple, so I omit the details.

Next, if you uncomment the fastlane/Fastfile file that is the execution file to deploy the app, it looks like the following.

default_platform(:ios)

platform :ios do
  desc "Push a new beta build to TestFlight"
  lane :beta do
    increment_build_number(xcodeproj: "kumoncho.xcodeproj")
    build_app(workspace: "kumoncho.xcworkspace", scheme: "kumoncho")
    upload_to_testflight
  end
end

You can execute it by executing the Fastlane command like the following.

# cd ios
fastlane beta

If you execute the Fastlane, the build number of iOS is increased(increment_build_number), the app is built(build_app), and the app is deployed to Testflight(upload_to_testflight).

Modify iOS execution file

You can’t fully automate deployment with the fastlane file provided by default. So, you need to modify the fastlane/Fastfile like the following.

# frozen_string_literal: true

default_platform(:ios)

platform :ios do
  def updateVersion(options)
    if options[:version]
      version = options[:version]
    else
      version = prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")
    end

    re = /\d+.\d+.\d+/
    versionNum = version[re, 0]

    if versionNum
      increment_version_number(
        version_number: versionNum
      )
    elsif version == 'major' || version == 'minor' || version == 'patch'
      increment_version_number(
        bump_type: version
      )
    else
      UI.user_error!('[ERROR] Wrong version!!!!!!')
    end
  end

  def certificate(options)
    if options[:type] == 'github'
      create_keychain(
        name: 'ios_app_keychain',
        password: '****************',
        timeout: 1800,
        default_keychain: true,
        unlock: true,
        lock_when_sleeps: false
      )
      import_certificate(
        certificate_path: 'distribution.p12',
        certificate_password: '****************',
        keychain_name: 'ios_app_keychain',
        keychain_password: '****************'
      )
    end
    install_provisioning_profile(path: 'distribution.mobileprovision')
    update_project_provisioning(
      xcodeproj: 'Runner.xcodeproj',
      target_filter: 'github',
      profile: 'distribution.mobileprovision',
      build_configuration: 'Release'
    )
    api_key = app_store_connect_api_key(
      key_id: '**************',
      issuer_id: '***********************',
      key_filepath: 'distribution.p8'
    )
    api_key
  end

  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
    increment_build_number(xcodeproj: 'Runner.xcodeproj')
  end

  desc 'Submit review only'
  lane :submit_review do |_options|
    upload_to_app_store(
      submit_for_review: true,
      automatic_release: true,
      force: true,
      skip_metadata: true,
      skip_screenshots: true,
      skip_binary_upload: true
    )
  end

  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end

  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_app_store(
      force: true,
      reject_if_possible: true,
      skip_metadata: false,
      skip_screenshots: true,
      languages: ['en-US', 'ja','ko'],
      release_notes: {
        "default" => "bug fixed",
        "en-US" => "bug fixed",
        "ja" => "バグ修正",
        "ko" => "버그 수정"
      },
      submit_for_review: true,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

사용자가 입력한 버전 상황에 맞게, 버전을 수정하도록 하는 함수를 정의하였습니다.

platform :ios do
  def updateVersion(options)
    ...
  end
  ...
  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
    increment_build_number(xcodeproj: 'Runner.xcodeproj')
  end
  ...
end

I’ve created a Fastlane command called version that allows you update the version of the app. You can execute the following command to update the app version.

# fastlane version version:1.0.0
# fastlane version version:major
# fastlane version version:minor
fastlane version version:patch

If you don’t insert the version, the command will wait for the user input.

def updateVersion(options)
  if options[:version]
    version = options[:version]
  else
    version = prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")
  end
...

You need Certification, Provisioning file and App store connect API key to deploy the app with Fastlane.

After preparing the files, you can use the certificate function to register them and return the API key where you want to use it.

# frozen_string_literal: true

default_platform(:ios)

platform :ios do
  ...
  def certificate(options)
    if options[:type] == 'github'
      create_keychain(
        name: 'ios_app_keychain',
        password: '****************',
        timeout: 1800,
        default_keychain: true,
        unlock: true,
        lock_when_sleeps: false
      )
      import_certificate(
        certificate_path: 'distribution.p12',
        certificate_password: '****************',
        keychain_name: 'ios_app_keychain',
        keychain_password: '****************'
      )
    end
    install_provisioning_profile(path: 'distribution.mobileprovision')
    update_project_provisioning(
      xcodeproj: 'Runner.xcodeproj',
      target_filter: 'github',
      profile: 'distribution.mobileprovision',
      build_configuration: 'Release'
    )
    api_key = app_store_connect_api_key(
      key_id: '**************',
      issuer_id: '***********************',
      key_filepath: 'distribution.p8'
    )
    api_key
  end
  ...
end

The API key that is made with the certificate function is used for deploying the app to Testflight or App store like the following.

# frozen_string_literal: true

default_platform(:ios)

platform :ios do
  ...
  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end

  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_app_store(
      force: true,
      reject_if_possible: true,
      skip_metadata: false,
      skip_screenshots: true,
      languages: ['en-US', 'ja','ko'],
      release_notes: {
        "default" => "bug fixed",
        "en-US" => "bug fixed",
        "ja" => "バグ修正",
        "ko" => "버그 수정"
      },
      submit_for_review: true,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

Testflight용으로 배포하는 스크립트를 보면, 인증서를 등록한 후, 앱을 빌드하고, Fastlane가 제공하는 upload_to_testflight 함수를 사용하여 배포하는 것을 확인할 수 있습니다.

...
platform :ios do
  ...
  desc 'Push a new beta build to TestFlight'
  lane :beta do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_testflight(api_key: api_key)
  end
  ...
end

The code for App store also registers the certification like the code for Testflight, and then builds the app. After then, the upload_to_app_store function will be called for deploying the app.

...
platform :ios do
  ...
  desc 'Push a new release build to the App Store'
  lane :release do |options|
    api_key = certificate(options)
    build_app(
      workspace: 'Runner.xcworkspace',
      scheme: 'Runner',
    )
    upload_to_app_store(
      force: true,
      reject_if_possible: true,
      skip_metadata: false,
      skip_screenshots: true,
      languages: ['en-US', 'ja','ko'],
      release_notes: {
        "default" => "bug fixed",
        "en-US" => "bug fixed",
        "ja" => "バグ修正",
        "ko" => "버그 수정"
      },
      submit_for_review: true,
      precheck_include_in_app_purchases: false,
      automatic_release: true,
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_encryption_updated: false,
        export_compliance_uses_encryption: false
      },
      api_key: api_key
    )
  end
end

The options of the upload_to_app_store functions are like the following.

  • force: The HTML report created by Fastlane won’t be generated.
  • reject_if_possible: If there is a version pending review, it will be canceled.
  • skip_metadata: You can decide whether to register the App store information or not. In the deployment automation, you need to write the release notes, so false should be set.
  • skip_screenshots: I already registered the screenshots, so I don’t need to upload the screenshots again.
  • languages: You can set the localization of the registered app. ar-SA, ca, cs, da, de-DE, el, en-AU, en-CA, en-GB, en-US, es-ES, es-MX, fi, fr-CA, fr-FR, he, hi, hr, hu, id, it, ja, ko, ms, nl-NL, no, pl, pt-BR, pt-PT, ro, ru, sk, sv, th, tr, uk, vi, zh-Hans, zh-Hant can be used. If you wnat to know more details, see the offical site. (Official site)
  • release_notes: When you deploy the app to iOS, you need to write the release notes. My app supports the 3-languages, so the default and 3-languages of the release notes are configured here.
  • submit_for_review: The app will be submitted to review.
  • automatic_release: After reviewing, the app will be deployed automatically. If this option is not set, the developer will deployt the app after reviewing.
  • submission_information: This option is for the submission information like the encryption and the app including the ads.

There are many other options. You can see the details on the offical site.

Execute Fastlane to deploy Testflight for iOS

We’re ready to deploy the app via Fastlane for iOS. Next, let’s deploy the app using Fastlane.

Execute the Fastlane command below to deploy the Flutter app to Testflight.

# cd ios
fastlane beta version:patch

It takes an extremely long time for the deployment to complete. When the deployment is done, you can see the screen like the following.

Deploy with Fastlane automatically - iOS Testflight deployment

Of course, you can see the app is deployed well to Testflight on App store connect.

Execute Fastlane to deploy App store for iOS

Next, execute the Fastlane command below to deploy the Flutter app to App store.

# cd ios
fastlane release version:patch

When the deployment is done, you can see the screen like following.

Deploy with Fastlane automatically - iOS app deployment

Also, you can see the app is deployed well on App store connect.

Android

Now, let’s automate the deployment of Android apps developed with Flutter using Fastlane.

Create Service Account for API access

When you deploy the app with Fastlane for Android, you need to create Google Developer Service Account to use the Google API.

To create Google Developer Service Account, click the link below to go to Google Play Console.

When you go to Google Play Console, you can see the screen like the following.

Deploy with Fastlane automatically - Google Play Console

Click Settings on the left menu. And the, click the API access menu on the bottom of Developer account.

Deploy with Fastlane automatically - Google Play Console api access menu

When you see the screen above, click the CREATE NEW PROJECT button to create a new project.

Deploy with Fastlane automatically - Google Play Console api access, service account

After creating the new project, you can see the screen above. Click the CREATE SERVICE ACCOUNT button on the bottom of the screen, and you can see the screen like the following.

Deploy with Fastlane automatically - Google Play Console api access, how to create service account

Click the Google API Console link on the screen above. after clicking, you can see the screen like the following.

Deploy with Fastlane automatically - Google API Console

click the CREATE SERVICE ACCOUNT button on the top of the screen. After clicking, you can see the screen to create a new Service account.

Deploy with Fastlane automatically - Google API Console, create service account

On the screen above, insert a name to Service account name, and click the CREATE button to create the Service account. (In my case, I inserted google-play-fastlane-deployment to Service account name.)

Deploy with Fastlane automatically - Google API Console, configure role of service account

When you see the screen above, click Role and then, search and select Service Account User. After selecting Service Account User, click the CONTINUE button on the bottom of the screen to go to the next step.

Deploy with Fastlane automatically - Google API Console, create service account key

When you see the screen above, select CREATE KEY on the bottom, and click the CREATE button with selecting JSON to create the key.

Deploy with Fastlane automatically - Google API Console, create service account JSON key

When you click the CREATE button to create key, the JSON file is downloaded automatically. Copy this file to the android folder in the Flutter project. And, click the DONE button to create Service Account.

After then, go bck to the original screen and click the DONE button on the right bottom of the screen to finish the Service Account creation.

Deploy with Fastlane automatically - Google Play Console api access, how to create service account

And then, you can see the screen like the following unlike before.

Deploy with Fastlane automatically - Google Play Console, done creating Service Account

Next, click the GRANT ACCESS button on the right bottom of the screen to authorize the account.

Deploy with Fastlane automatically - Google Play Console, cofigure Service Account permission

When you see the screen above, scroll down and click the ADD USER button to register the user.

Initialize Fastlane for Android

Next, let’s create Fastlane for Android. Execute the command below to create Fastlane for Android.

cd android
fastlane init

After executing the command above, you can see the screen like the following.

Deploy with Fastlane automatically - Android initialization: package name

Insert the Android project Package Name. (ex> io.github.dev.yakuza.kumoncho) And then, you can see the screen asking you to enter the path of the JSON file as the following.

Deploy with Fastlane automatically - Android initialization: JSON path

When you created Service Account, you copied the JSON file to the android folder. Set this file path. (ex> app-xxx.json)

Deploy with Fastlane automatically - Android initialization: 다운로드 metadata

Next, when deploying Android, it asks if you want to download the registered the store information (metadata). I already deployed the app, so I don’t need to update the store information. So, I inserted n to skip downloading the store information.

After the screen above, you can see several times of the Continue by pressing Enter screen. Press Enter key to go procedure.

Fastlane folders and files for Android

After finishing the configuration, you can see the folders and files under the android folder of the Flutter project like the following.

|- fastlane
|  |- Appfile
|  |- Fastfile
|- Gemfile
|- Gemfile.lock

Let’s see the details of each folder and file.

  • fastlane folder: The folder has Fastlane configuration and execution files.
  • Gemfile, Gemfile.lock: Fastlane is developed by Ruby. These files manage the dependencies of Ruby for Fastlane.

If you uncomment the fastlane/Appfile file that contains the settings to run Fastlane, it looks like the following.

json_key_file("api-xxx.json")
package_name("io.github.dev.yakuza.kumoncho")

You can see the Package Name and the JSON file path that we’ve configured above. Next, if you uncomment the fastlane/Fastfile file that is the execution file to deploy the app, it looks like the following.

default_platform(:android)

platform :android do
  desc "Runs all the tests"
  lane :test do
    gradle(task: "test")
  end

  desc "Submit a new Beta"
  lane :beta do
    gradle(task: "clean assembleRelease")
    crashlytics
  end

  desc "Deploy a new version to the Google Play"
  lane :deploy do
    gradle(task: "clean assembleRelease")
    upload_to_play_store
  end
end

Unlike iOS, you can see two lanes named beta and deploy. Of course, you can execute the commands below to execute the Fastlane above.

# cd android
fastlane beta
fastlane deploy

However, you can’t automate deployment with this script, so you need to modify Fastfile.

Modify execution file for Android

You can’t automate deployment with the Fastlane file provided basically. So, we need to modify the fastlane/Fastfile file as follows to automate deployment for Android.

# frozen_string_literal: true

default_platform(:android)

platform :android do
  def increment_version_code
    path = '../app/build.gradle'
    re = /versionCode\s+(\d+)/

    s = File.read(path)
    versionCode = s[re, 1].to_i
    s[re, 1] = (versionCode + 1).to_s

    f = File.new(path, 'w')
    f.write(s)
    f.close
  end

  def increment_version_number(bump_type: nil, version_number: nil)
    path = '../app/build.gradle'
    re = /versionName\s+("\d+.\d+.\d+")/
    s = File.read(path)
    versionName = s[re, 1].gsub!('"', '').split('.')

    major = versionName[0].to_i
    minor = versionName[1].to_i
    patch = versionName[2].to_i

    if bump_type == 'major'
      major += 1
      minor = 0
      patch = 0
    elsif bump_type == 'minor'
      minor += 1
      patch = 0
    elsif bump_type == 'patch'
      patch += 1
    end

    s[re, 1] = if version_number
                 "\"#{version_number}\""
               else
                 "\"#{major}.#{minor}.#{patch}\""
               end

    f = File.new(path, 'w')
    f.write(s)
    f.close
    increment_version_code
  end

  def updateVersion(options)
    version = options[:version] || prompt(text: "Enter the version type or specific version\n(major, minor, patch or 1.0.0): ")

    re = /\d+.\d+.\d+/
    versionNum = version[re, 0]

    if versionNum
      increment_version_number(
        version_number: versionNum
      )
    elsif %w[major minor patch].include?(version)
      increment_version_number(
        bump_type: version
      )
    else
      UI.user_error!('[ERROR] Wrong version!!!!!!')
    end
  end

  desc 'Update version'
  lane :version do |options|
    updateVersion(options)
  end

  desc 'Submit a new Beta'
  lane :beta do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end

  desc 'Deploy a new version to the Google Play'
  lane :release do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
end

Let’s see the details of the file. There is no updating the app version feature in Android, unlike iOS. (I couldn’t find it, if you know, please give me a feedback.) So, I implement updating the versionCode and versionName of Android.

platform :android do
  def increment_version_code
    ...
  end

  def increment_version_number(bump_type: nil, version_number: nil)
    ...
  end

  def updateVersion(options)
    ...
  end
  ...
end

The updateVersion function to update the version has been described in iOS, so the detailed description will be omitted.

Fastlane for Android to deploy for internal test

Let’s see the details of the Fastlane code for the internal test.

platform :android do
  ...
  desc 'Submit a new Beta'
  lane :beta do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      track: 'internal',
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
  ...
end

To build the app for Android, the clean and bundleRelease (not assembleRelease) commands of Gradle is used. Also, the track: 'internal' option is configured into the upload_to_play_store function to deploy the app to the internal test.

Release notes(change log) is not required on Android, unlike iOS. So, set all options of registering the store option to skip.

Lastly, we’ll use the bundleRelease command to make the aab file and upload it, so set true to the skip_upload_apk option.

Fastlane for Android to deploy for Google Play

Next, let’s see the details of the Fastlane code for Google Play.

...
platform :android do
  ...
  desc 'Deploy a new version to the Google Play'
  lane :release do |_options|
    gradle(task: 'clean bundleRelease')
    upload_to_play_store(
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
      skip_upload_images: true,
      skip_upload_apk: true,
      aab: '../build/app/outputs/bundle/release/app-release.aab'
    )
  end
end

I change the name from lane :deploy to lane :release to make it as same as iOS. The code is the same as the internal test except the track option is not configured to the upload_to_play_store function, so I omit the description of the code.

Execute Fastlane for Android

We’re ready to automate deploy for Android via Fastlane. Now, let’s execute the Fastlane to deploy.

You can deploy the app to the internal test by executing the command below.

# cd android
fastlane beta version:patch

It takes an extremely long time for the deployment to complete. When the deployment is done, you can see the screen like the following.

Deploy with Fastlane automatically - Android internal test deployment

Of course, you can see the app is deployed well to the internal test on Play store console.

Next, execute the command below to deploy the app to the production.

# cd android
fastlane release version:patch

When the deployment is done, you can see the screen like the following.

Deploy with Fastlane automatically - Android play store deployment

Also, you can see the app is deployed well on Google Play store.

gitignore

When you deploy the app with Fastlane, some files are generated. And, you don’t need to manage some of them on Git. So, open the .gitignore file and modify it like the following.

...
# fastlane
ios/*.mobileprovision
ios/*.cer
ios/*.dSYM.zip
android/fastlane/README.md
ios/fastlane/README.md

Completed

Done! we’ve seen how to automate the deployment of the Flutter app by Fastlane. Fastlane can be used for the Native app, so I hope this blog post helps many developers.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts