Skip to content

Test an add-to-app patch before publishing it

In an add-to-app setup, the host app (Android or iOS) is often built by another team, and a full QA build can take a while. You don’t need one to try a patch. Build the host app yourself against the module release, publish the patch to your own track, and only promote it to stable once it works.

This page assumes you have already followed the Android or iOS add-to-app guide.

Patches published with --track <name> reach only devices that ask for that track. Your users’ devices ask for stable, so a patch on any other track is invisible to them. Your module chooses its track at runtime with package:shorebird_code_push, and the host app tells it which track to use when it starts the Flutter engine. A host app you build locally asks for your track. The host app you ship passes no track, so it keeps checking the default track (stable, or the channel: set in shorebird.yaml).

Set up the module once, before you release

Section titled “Set up the module once, before you release”

The code that chooses the track has to be in the release, because a patch can’t reach a device until the device asks for its track. Make these changes before you run shorebird release aar or shorebird release ios-framework.

  1. Add shorebird_code_push to your Flutter module:

    flutter pub add shorebird_code_push
  2. Turn off the automatic update check in the module’s shorebird.yaml, so that your code decides which track to check:

    app_id: your-app-id-here
    auto_update: false
  3. Read the track from the entrypoint arguments in the module’s main, and check for updates on it. With no argument, track is null and the updater checks the default track, so the host app you ship needs no change. Requires shorebird_code_push 2.0.4 or newer.

    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:shorebird_code_push/shorebird_code_push.dart';
    
    void main(List<String> args) {
      checkForPatch(args);
      runApp(const MyApp());
    }
    
    /// Checks for a patch on the track passed as `--shorebird-track=<name>`,
    /// or on the default track if there is none.
    void checkForPatch(List<String> args) {
      const prefix = '--shorebird-track=';
      final trackArg = args.where((arg) => arg.startsWith(prefix)).firstOrNull;
      final track = trackArg == null
          ? null
          : UpdateTrack(trackArg.substring(prefix.length));
      unawaited(
        ShorebirdUpdater().update(track: track).catchError((Object error) {
          debugPrint('Shorebird update failed: $error');
        }),
      );
    }

    Because auto_update is off, this update call is what checks for patches in every build, including the one your users run. Don’t remove it. If the host app starts the module through other Dart entrypoints (functions marked @pragma('vm:entry-point')), call checkForPatch from each of them too, or those engines never check for patches.

    update throws when the check fails, for example when the device is offline. The catchError keeps that from reaching your error reporting as an unhandled error.

Then create the release as usual:

shorebird release aar --release-version 1.2.3+1

Build the host app locally with your track

Section titled “Build the host app locally with your track”

Build the host app the way the add-to-app guide describes, from the module’s release directory: on Android it’s the local Maven repository with the aar, on iOS it holds the .xcframeworks. Three things matter:

  • Use the exact release. You need the release directory that shorebird release produced (from your CI, or from whoever ran it). If you rebuild the module with flutter build, the result doesn’t match the release and patches won’t apply to it.
  • Use a release build. Shorebird only patches release builds: the Android release build type, or the iOS Release configuration on a device (not a simulator).
  • Keep the version. The host app’s version must match the --release-version you released with, or the device asks for patches to a release that doesn’t exist.

Pass your track to the module when the host app starts the Flutter engine. Pick a name that is yours, such as dev-<your-name>.

On Android, add a build config field that is empty unless you set a Gradle property:

android {
    buildFeatures {
        buildConfig true
    }
    defaultConfig {
        buildConfigField "String", "SHOREBIRD_TRACK",
            "\"${project.findProperty('shorebirdTrack') ?: ''}\""
    }
}

On iOS, add a user-defined build setting SHOREBIRD_TRACK (empty by default) to your app target, and an Info.plist entry ShorebirdTrack with the value $(SHOREBIRD_TRACK).

Then pass the track to the module only when it’s set:

val track = BuildConfig.SHOREBIRD_TRACK
val args = if (track.isEmpty()) emptyList() else listOf("--shorebird-track=$track")
val engine = FlutterEngine(context)
engine.dartExecutor.executeDartEntrypoint(
    DartExecutor.DartEntrypoint.createDefault(),
    args,
)
FlutterEngineCache.getInstance().put("my_engine_id", engine)

If your Android app starts Flutter another way, pass the same list to FlutterActivity.withNewEngine().dartEntrypointArgs(...), FlutterFragment.withNewEngine().dartEntrypointArgs(...), or FlutterEngineGroup.Options.setDartEntrypointArgs(...).

Build and install your local host app with the track set:

./gradlew installRelease -PshorebirdTrack=dev-alice

installRelease exists only when the release build type has a signingConfig. If your host app signs release builds only in CI, sign your local build with the debug key by adding signingConfig signingConfigs.debug to the release build type, and don’t commit that change. On iOS, devicectl needs Xcode 15 or newer and a device on iOS 17 or newer. Run xcrun devicectl list devices to find the device ID.

Publish the patch to your track and try it

Section titled “Publish the patch to your track and try it”

Make your change in the module, then publish it to your track:

shorebird patch aar --release-version 1.2.3+1 --track dev-alice

The track is created the first time you publish to it. Launch your local build so the module checks for the patch. If your app starts the Flutter engine only when a Flutter screen opens, open one. Then fully quit the app (force-stop it on Android) and launch it again to run the patch. In logcat or the Xcode console, the patch check names your track and your release version (1.2.3+4 in the iOS example):

[shorebird] Sending patch check request: ... channel: dev-alice, release_version: 1.2.3+1 ...

To try another change, publish another patch to the same track.

When the patch works, move it to stable without rebuilding. Your users get the exact patch you tested. Run this from the module directory, with the patch number that shorebird patch printed (or find it with shorebird patches list --release-version <version>):

shorebird patches promote --release-version 1.2.3+1 --patch-number 3

Promoting moves the patch from your track to stable.

See Tracks for more on tracks, including how to list and delete them.