Flutter and fastlane, how to setup an iOS continuous delivery solution

When it comes to iOS development, everybody have their own favorite language and framework: Swift, Objective-C, SwiftUI, React-Native, Flutter and so on. Unlike most of my previous post, today we’re going to leverage some iOS tooling for cross platforms technology: fastlane and Flutter.

Everybody agree to say Flutter is getting more and more popular in mobile development, but when I recently got chance to work on it, I noticed the release process for the iOS version was mostly manual. This sound odd to me when we know the iOS ecosystem largely covered it. Today, we’ll look into how setting up fastlane for a continuous delivery solution of your Flutter app.

fastlane-ios-flutter

If you are new to iOS development, fastlane is one of the most used tool when it comes to automate building and shipping an application. It can do much more, but that’s the part that interest me today.

However, when I checked fastlane and Flutter for continuous delivery, well, it can become quite confusing. Fastlane redirects to Flutter documentation but Flutter doesn’t dive much into it. So let’s see how we setup fastlane for Flutter.

Setup fastlane

The first part is to setup fastlane to the right place. When a Flutter app is built for each platform, it creates an ios folder with the native code. This is where fastlane needs to be initialized.

Assuming we’ve already installed fastlane, we can directly start with fastlane init.

# Build for ios
flutter build ios

# Move to the right folder
cd ios

# Initialize fastlane
fastlane init

From there, the command line takes over and we can follow its documentation. At then end, it will create a Fastfile with some sample setup.

A great advantage of fastlane is to handle iOS certificates and provisioning profiles for you. It’s really handy if you work in a team, it avoids sharing credentials or certificates manually. It would be hosted on your preferred location likw Github, S3 bucket and so on.

Using match command line, we are able to create all the signing certificates and provisioning profiles, those are required later on when we build on physical device, but also uploading our app to th App Store Connect.

So if it’s not done yet, I would advise to set up your repository for those certificates.

Like the first fastlane command, match comes with Matchfile that also help defines a standard url, format, account and other key information for our continuous delivery. A good tip is to use environment variable that later on can be injected in the document. We’ll come back to this.

So far so good, the structure is setup, now we can tweak Flutter for its iOS generated project to fastlane command line.

Continuous delivery

If you are curious and open the Runner project in Xcode, you’ll notice the organization team and signing settings might have been automatically setup.

That sounds like a good idea, but if you don’t know how to navigate those settings, it can be get really messy real fast.

If you are a novice to iOS, that’s the part I would suggest to avoid completely. After all, isn’t it why developers are keen on Flutter? To avoid the trouble of setting up manually all those specific platform-based stuff? That would be my expectations at least.

I looked on flutter command line but I didn’t see a proper way to set this up, and that’s where the documentation came a bit short. The only related parameter app signing is to actually skip it, but we’ll get back to this.

Worry no more, let’s leverage fastlane so we never got to worry about signing again.

So let’s create a new lane (equivalent to a command line) in our Fastfile so we can get going.

First thing I want is to disable the auto signing that is suggested in the project, that sounds an easy for you or a teammate to regenerate files.

default_platform(:ios)

platform :ios do 

    lane delivery do 
        # disable auto signing
        disable_automatic_code_signing(path: "Running.xcodeproj")

        # ...
    end

end

So when running fastlane delivery, it just makes sure we disable the auto signing for the rest of the lane.

Next step is to build the app using gym command line.

default_platform(:ios)

platform :ios do 

    lane delivery do 
        # disable auto signing
        disable_automatic_code_signing(path: "Running.xcodeproj")

        # build for app store / testflight
        gym(scheme: "Runner",
            workspace: "Runner.xcworkspace",
            export_method: "app-store")

        # ...
    end

end

In this case, gym will build the specific scheme and sign it to export for the app-store, which means for production use.

In most cases, the command won’t have issue to detect the right certificates and provisioning profiles for it if you synced it with match before. If not, we have a back up solution with build_app action. It allows you set set specific provisioning profiles name, those would be matching the one you created earlier.

Last step is to upload it to the App Store Connect using upload_to_testflight command line. This command has different aliases, like testflight or pilot, feel free to use the one that makes the most sense to you.

default_platform(:ios)

platform :ios do 

    lane delivery do 
        # disable auto signing
        disable_automatic_code_signing(path: "Running.xcodeproj")

        # build for app store / testflight
        gym(scheme: "Runner",
            workspace: "Runner.xcworkspace",
            export_method: "app-store")

        upload_to_testflight
    end

end

If you want to automatically set your Apple ID account when uploading to TestFlight, you can use Appfile which is similar to other fastlane configuration files to preset your environment.

Almost there!

Before we give it a go, let’s create a separate lane to sync our provisioning profiles for development and production use, it might come handy

default_platform(:ios)

platform :ios do 

    lane sync_match do 
        match(type: "appstore", readonly: true)
        match(type: "development", readonly: true)
    end

end

Our lanes is almost ready for a full use, only one thing is missing: environment variables.

If you try to run them, you’ll notice there are still few manual step that required an user entry, it could be for your Apple ID, the passphrase to decrypt your certificates, or else.

In this case, I would suggest to create a separate bash script to import all the environment variables that would be required. Here is an example secrets-ios.sh:

#!/bin/bash

# fastlane match passphrase
export MATCH_PASSWORD="my secret passphrase"

# Apple access
export FASTLANE_USER="my apple id"
export FASTLANE_PASSWORD="my secret"

# 2FA access
export FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD="secret key"

# ...

Before moving one, make sure to NOT commit this file. Add it to your .gitignore or move it to another folder. This is holding all your secrets, it shouldn’t never be accessible through git history or published on the same repository.

Alright, we’re really close to be done. Let’s step back to Flutter level.

Flutter and fastlane

So far, we’ve mostly tweaked the tooling to make it easy to export our amazing app to the App Store / TestFlight, but it’s not yet completely done.

If match handles all the iOS platforms signing in a separate repository, we still have other secrets variables in our bash that we don’t want to let hang around.

The solution is pretty simple and can be quite handy: we can do exactly the same for Flutter that we do for iOS. We can manually create a separate repository for all our Flutter secrets, that’s where the secrets-ios.sh would belong.

The great part? We can do the same for Android too, creating a similar secrets-android.sh with whatever will be useful in it. Although, I won’t cover this bit today.

So we would have

- ios/
    - secrets-ios.sh
- android/
    - secrets-android.sh

Why having a separate repository? Well, it makes your app a bit safer to not share manually passwords around and every “release manager” can download when required. You can clone it separately, or you can try to “hook it” with another homemade script, for instance here my setup-secrets.sh

#!/bin/bash

# Temporary clone and copy to right destination
git clone {GIT_URL_FLUTTER_SECRETS} .tmp
cp -RT .tmp/ios ios
cp -RT .tmp/android android
rm -rf .tmp

So let’s put this all together now into a one release-ios.sh script.

#!/bin/bash

# Download and move secrets
./setup-secrets.sh

# Generate latest ios build, with release properties, no signing
flutter build ios --release --no-codesign

# Move into ios
cd ios

# Import environment variables to current session
source secrets-ios.sh

# Sync certificates
fastlane sync_match

# Build and upload to App Store / TestFlight
fastlane delivery

That’s it 🎉

Now we can just run our ./release-ios.sh to sync our private environment variables, generate native code with flutter, sync certificates, and finally build and upload for the App Store.

It means that the next time there is a new contributor joining you team, you can focus on giving access to the repository of the flutter codebase, the iOS certificates repository and the flutter secrets, and nothing more.

Even if we made it straightforward, there might be more optimization that could be bring to make it top notch. For instance the secret repository? It could be part of the main repository if your team has an encrypted system.

Another thing I noticed is that we’re building the app twice: Flutter builds for iOS and generate an .app but then fastlane rebuild it again with the right signing. There might be a way for fastlane to piggyback on the first build.


If Flutter is getting more and more maturity, the tooling around it can be a bit obscure if you don’t have enough background on the mobile platform.

When it comes to iOS distribution, it’s something that’s the community had largely covered and fastlane definitely makes it easier once everything is aligned.

On top of this, fastlane also works with Android and can be the right tool for a cross-platform delivery system.

Today, we went from setting up a shared and secured secret environments, building the flutter app, to signing and uploading the iOS app to the App Store. All of this, only with few scripts.

© 2023 Benoit Pasquier. All Rights Reserved
Author's picture

Benoit Pasquier

Software Engineer 🇫🇷, writing about career development, mobile engineering and self-improvement

ShopBack 💰

Singapore 🇸🇬