Continuous Delivery for Flutter: Automating iOS and Android Beta Distribution

By Adaeze Nwosu · 19 August 20266,756 views
Continuous Delivery for Flutter: Automating iOS and Android Beta Distribution

The 'It Works on My Machine' Trap

I remember the afternoon in Onitsha when I pushed a hotfix for our delivery app, thinking I’d save ten minutes by manually archiving the iOS build and uploading it via Transporter. Three hours later, my internet dropped mid-upload, the machine overheated, and the test flight wasn't ready for the riders who were starting their shift. It was a disaster. In the world of logistics, a late app update isn't just a technical debt—it’s a business failure. We missed our delivery window because I was stuck babysitting a progress bar.

Manual distribution is the quickest way to introduce human error into your production lifecycle. When you're dealing with Flutter across both Android and iOS, the overhead of code signing, version bumping, and build management grows exponentially. I learned the hard way that if your delivery process isn't automated, you don't actually have a process; you have a collection of fragile habits. This article is about moving away from that chaos.

The Anatomy of an Automated Pipeline

To move fast without breaking things, you need a deterministic pipeline. I use GitHub Actions combined with Fastlane. Fastlane is the industry standard for a reason: it abstracts away the endless configuration nightmares of Xcode and Gradle. When you combine it with a CI/CD runner, you ensure that every build is generated in a clean, reproducible environment.

The goal here is simple: commit your code, tag your release, and let the cloud handle the heavy lifting of signing, bundling, and distributing to Firebase App Distribution or TestFlight. By moving the heavy build process to a cloud runner, you remove the bottleneck of your local machine's network speed—something I constantly struggle with in Onitsha.

Step 1: Standardizing the Environment

Before you touch Fastlane, your Flutter environment needs to be hermetic. If your build depends on a specific version of Flutter or Cocoapods that only exists on your laptop, the CI will fail. Use a fvm (Flutter Version Management) configuration or a simple script to lock your dependencies.

  1. Initialize Fastlane in your android and ios folders.
  2. Configure the Fastfile to handle versioning. We use pubspec.yaml to manage versioning and inject those values into the build files using a custom script during the CI run.
  3. Create service accounts for Google Play and App Store Connect. Never use your personal credentials in CI. It’s a security nightmare waiting to happen.

Step 2: Configuring Fastlane for Multi-Platform Delivery

Fastlane makes multi-platform builds feel like writing a simple script. Below is how I handle our internal beta distribution.

# fastlane/Fastfile
platform :ios do
  desc "Push to TestFlight"
  lane :beta do
    get_certificates
    get_provisioning_profile
    build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
    upload_to_testflight
  end
end

platform :android do
  desc "Deploy to Firebase App Distribution"
  lane :beta do
    gradle(task: "bundleRelease")
    firebase_app_distribution(
      app: "YOUR_FIREBASE_APP_ID",
      groups: "testers",
      release_notes: "New delivery features"
    )
  end
end

This script is the bedrock. It runs every time we push to the develop branch. It pulls the certificates, compiles the bytecode, and pushes the artifact to the testers. Notice that I use gradle directly for Android—it’s faster and less prone to IDE-specific configurations.

Step 3: CI/CD Integration via GitHub Actions

Once the lane is defined, the CI runner triggers it. I prefer GitHub Actions because it gives us a clean slate every time. We set up a workflow that runs on every pull request, but only performs the distribution when a specific tag is applied.

name: Beta Distribution
on:
  push:
    tags:
      - 'v*'

jobs:
  build-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - name: Install dependencies
        run: flutter pub get
      - name: Fastlane Beta
        run: bundle exec fastlane ios beta
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}

Tips for Reliability in Production

Real-world delivery systems are messy. Here is what I’ve learned from the trenches:

  1. Use fastlane match: Don't manually manage certificates. Use match to store your distribution certificates in a private git repo. It saves hours of debugging 'Provisioning Profile Not Found' errors.
  2. Clean Builds are non-negotiable: In your CI configuration, always run flutter clean before the build. We once spent two days debugging an issue where an old native library was lingering in the Android build folder. Never trust the cache unless you specifically control it.
  3. Handle Network Intermittency: If your CI runner is behind a restrictive firewall or you're pushing large binaries to App Store Connect, use a retry strategy. Fastlane has a built-in --retry flag that handles transient network timeouts elegantly.
  4. Automated Testing is the Guardrail: If your beta build includes broken code, automating the distribution just makes you faster at shipping bugs. Include a step in your pipeline that runs flutter test. If the unit tests fail, the build should stop immediately.

The Cultural Shift: Ownership and Visibility

Automating your delivery isn't just about saving time; it's about changing the culture of your engineering team. When developers know that a merge to main results in a deployable build in 20 minutes, they stop holding onto features for fear of the 'deployment day' headache.

In my logistics startup, we started shipping two updates a week instead of one every month. This pace allowed us to iterate on our geofencing logic based on real feedback from the riders on the ground. We found that the vibration patterns we were using for notifications weren't strong enough for riders on motorbikes in the city. Because we had a smooth CI/CD pipeline, I was able to push a fix for that within an hour of hearing the report. That kind of responsiveness is impossible without automation.

Troubleshooting Common Failures

When your pipeline breaks—and it will—do not panic. Start by inspecting the CI logs. Look for 'Exit Code 1' errors in your build step. Usually, it's a version mismatch or a signing issue. If the log says 'Unable to find provisioning profile,' check your Matchfile and ensure the repository containing your certificates is accessible by the GitHub Action runner.

If the build succeeds but the app fails to install on the phone, look at your flutter build flags. Sometimes, when targeting specific device architectures, we accidentally strip out symbols that are needed for debugging. Keep your builds debug-friendly in the beta track so your testers can provide meaningful crash logs.

Conclusion: Stop Being a Build Monkey

Your time as an engineer is far more valuable than the time it takes to manage Xcode archives. Every minute you spend manually dragging a binary into an upload window is a minute you aren't spending on product logic, performance, or UI improvements. Automating your Flutter pipeline is the single highest-ROI activity you can do for your project’s health.

Start small. Automate the Android build first since it doesn't require the complex signing ceremony that iOS demands. Once that is humming along, tackle the iOS certificates. You will fail, you will struggle with Fastlane's Ruby syntax, and you will eventually get it working. And when you do, the feeling of watching a build deploy automatically while you’re out in the field helping a rider fix a delivery issue is worth every second of configuration work. Go build it.

Comments

No comments yet. Be the first!

Sign in to leave a comment.