Skip to main content

Native Flavors, Automatic app_id Detection & Multi-Channel Matrix

In enterprise mobile engineering, a single Flutter codebase is frequently compiled into several build variants (Flavors: dev, staging, prod), deployed across two OS platforms (Android and iOS), and distributed over multiple rollout tracks (Channels: staging, beta, stable).

This page details how tk_ota_push resolves the application identifier (app_id) and handles the Flavor × Platform × Channel matrix.

1. 9-Level app_id Resolution Cascade​

The FlutterProject.resolveEffectiveAppId(...) method (lib/src/cli/project.dart) determines the effective app_id following a strict precedence order from 1 (highest priority) down to 8/9 (automatic native inspection):

Application ID Resolution Hierarchy
  1. Explicit CLI Flag: --app-id <id> command option (highest priority).
  2. Dart Define Flag: Compile-time variable --dart-define=APP_ID=....
  3. Define Config File: Structured file --dart-define-from-file.
  4. Configuration File: Default app_id set in root shorebird.yaml.

Automatic Injection into shorebird.yaml (ensureShorebirdYamlAppId)​

Even if you leave app_id blank in your source shorebird.yaml (Zero-Config paradigm), the CLI calls project.ensureShorebirdYamlAppId(effectiveAppId) before running compilation so that the native Rust engine embedded within the mobile package always finds the required app_id key in flutter_assets/shorebird.yaml.

2. Automatic Native Detection on Android and iOS (detectNativeAppId)​

When no app_id is supplied via flag or shorebird.yaml, detectNativeAppId(platform, flavor, environment) directly inspects the project's native build scripts:

On Android (_detectAndroidApplicationId)​

  1. Opens android/app/build.gradle.kts (preferred) or android/app/build.gradle.
  2. Reads the base applicationId (or fallback namespace).
  3. If a flavor or environment is provided (e.g., dev or staging), locates the matching block in productFlavors or buildTypes (supporting Groovy dev { ... } and Kotlin DSL create("dev") { ... }):
    • If the block defines an explicit applicationId (e.g., applicationId "com.enterprise.staging.custom"), uses that ID directly.
    • If the block defines an applicationIdSuffix (e.g., applicationIdSuffix ".dev"), appends it to base applicationId (e.g., com.saramoney.retail.dev).
  4. As a final fallback, reads package="..." from android/app/src/main/AndroidManifest.xml.
// Example android/app/build.gradle
android {
defaultConfig {
applicationId "com.saramoney.retail"
}
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev" // Inferred as: com.saramoney.retail.dev
}
staging {
dimension "env"
applicationId "com.saramoney.staging.custom" // Inferred as: com.saramoney.staging.custom
}
prod {
dimension "env" // Inferred as: com.saramoney.retail
}
}
}

On iOS (_detectIosBundleId)​

  1. Opens ios/Runner.xcodeproj/project.pbxproj.
  2. Collects all PRODUCT_BUNDLE_IDENTIFIER values, excluding test targets (RunnerTests, Tests) and unresolved variables ($(...)).
  3. If a flavor is provided, selects the identifier containing the flavor name (case-insensitive), otherwise selects the first valid identifier.
  4. As a final fallback, reads <key>CFBundleIdentifier</key> from ios/Runner/Info.plist.

3. Explicit Declarations in shorebird.yaml (environments & flavors)​

If your backend application IDs differ from native package names, declare them in shorebird.yaml:

tenant_id: sara-money
base_url: https://ota.saramoney.internal
channel: stable
auto_update: false
patch_verification: strict

# Fallback default ID if no flavor/env is specified
app_id: com.saramoney.retail

# Priority 1 in shorebird.yaml
environments:
dev: sara-money-dev
staging: sara-money-staging
prod: sara-money-prod

# Priority 2 in shorebird.yaml
flavors:
dev: sara-money-dev
staging: sara-money-staging
prod: sara-money-prod

4. Understanding Flavor × Platform × Channel Combinations​

Distinguish clearly between Flavor (applied at binary compilation), Platform, and Channel / Track (applied during patch rollout):

DimensionAction LayerWhat it IsolatesExample
Flavor (--flavor)Binary Build (Gradle / Xcode)Separate apps with distinct app_id, icons, API backends, and keystores.dev (com.app.dev), prod (com.app)
Platform (-p) & Arch (-a)OS CPU binary formatExecutable format specific to OS architecture.android (aarch64, arm), ios (aarch64)
Channel (-c, --channel / UpdateTrack)OTA rollout within one app_idSubset of users on the same Release receiving patches earlier.staging (QA / Canary), beta, stable (Public)

CLI Commands Combining Flavor, Platform, and Channel​

# 1. Publish Android Production Release (AAB) on 'prod' flavor
tk-ota-push release -p android \
--flavor prod \
-t lib/main_prod.dart \
--dart-define-from-file=env/prod.json \
--artifact aab

# 2. Deploy a patch on 'prod' flavor, but target only the 'staging' channel (Canary QA)
tk-ota-push patch -p android \
--flavor prod \
-t lib/main_prod.dart \
--dart-define-from-file=env/prod.json \
--artifact aab \
--channel staging \
-m "QA validation on production binary"

# 3. Once validated by QA on 'staging', promote patch to 'stable'
tk-ota-push promote --patch-id <PATCH_UUID> --to-channel stable