livery

Declarative build-time config for Flutter apps. One livery.yaml holds the values your app's builds need, such as the bundle identifier, the application id, the display name or the API URL per environment, and livery generates the files that Xcode, Gradle and your Dart code read them from.

Why

A Flutter app's build-time values have to agree in three places: Xcode build settings, Gradle build scripts and Dart code. None of them can read the others' files. Native builds cannot read Dart, Flutter hands its --dart-define values to native builds only as an encoded blob, and Android Gradle Plugin 9 makes BuildConfig opt-in. So the same values end up copied into xcconfig files, Gradle scripts and Dart constants, and drift apart.

livery keeps them in one manifest and generates the rest:

  • an xcconfig that the Xcode project refers to as $(APP_BUNDLE_ID);
  • a Kotlin object that build.gradle.kts reads as typed constants, LiveryConfig.APP_ID;
  • a Dart library with typed constants and an enum per define, AppConfig.apiUrl and EnvDefine.current;
  • and properties, env or json files for anything else.

Values change with defines, such as ENV=production, which livery reads from the same --dart-define values you pass to Flutter. Every value has a type, inferred from YAML, and the manifest is checked in full before any file is written: an unknown key, a misspelt define value or an override of the wrong type fails the run instead of building the wrong app.

livery is a command you run before the Flutter build. It never edits your Xcode or Gradle project; the guides show the one-time wiring.

Install

Add livery as a dev dependency of your Flutter app:

flutter pub add --dev livery

It needs Dart 3.7 or later, which is Flutter 3.29 or later.

Quick start

Create livery.yaml at the root of the app:

# livery.yaml
version: 1

defines:
  ENV:
    values: [dev, staging, production]
    default: dev

config:
  common:
    APP_NAME: Demo Dev
  ios:
    APP_BUNDLE_ID: com.example.demo.dev
  android:
    APP_ID: com.example.demo
    APP_ID_SUFFIX: .dev
  dart:
    api_url: https://dev.api.example.com

overrides:
  - when: {ENV: [staging, production]}
    set:
      dart: {api_url: https://api.example.com}
  - when: {ENV: production}
    set:
      common: {APP_NAME: Demo}
      ios: {APP_BUNDLE_ID: com.example.demo}
      android: {APP_ID_SUFFIX: ""}

outputs:
  ios:
    format: xcconfig
    merge: [common, ios]
    files: ios/Flutter/livery.xcconfig
  android:
    format: kotlin
    merge: [common, android]
    files: android/livery/src/main/kotlin/LiveryConfig.kt
  dart:
    format: dart
    merge: [common, dart]
    files: lib/src/app_config.g.dart

Generate, passing the same defines as to Flutter:

dart run livery -D ENV=production
flutter run --dart-define=ENV=production

ios/Flutter/livery.xcconfig now holds:

// GENERATED BY livery. Do not edit by hand.
// Regenerate with `dart run livery`.

APP_NAME=Demo
APP_BUNDLE_ID=com.example.demo

android/livery/src/main/kotlin/LiveryConfig.kt:

// GENERATED BY livery. Do not edit by hand.
// Regenerate with `dart run livery`.

object LiveryConfig {
    enum class Env { DEV, STAGING, PRODUCTION }

    val ENV: Env = Env.PRODUCTION

    const val APP_NAME: String = "Demo"
    const val APP_ID: String = "com.example.demo"
    const val APP_ID_SUFFIX: String = ""
}

And lib/src/app_config.g.dart gives your app EnvDefine.current, AppConfig.appName and AppConfig.apiUrl.

Then wire each platform once:

  • iOS: include livery.xcconfig from Flutter's xcconfig files and refer to $(APP_BUNDLE_ID) in the project. See the iOS guide.
  • Android: add a small included build so android/app/build.gradle.kts can read LiveryConfig.APP_ID. See the Android guide.
  • Dart: import the generated file. See the Dart guide.

Neither Flutter, Xcode nor Gradle runs livery, so run it before every build that should see new values. The generated files are per environment: gitignore them, and run livery once after a fresh clone.

Multiple apps

One codebase can ship several apps that share one schema. Each app gets its own config file, and a define picks the app:

version: 1

defines:
  APP:
    values: [demo, kiosk]
    required: true
  ENV:
    values: [dev, production]
    default: dev

app_pattern: config/apps/{APP}.yaml

outputs:
  ios:
    format: xcconfig
    merge: [common, ios]
    files: ios/Flutter/livery.xcconfig
dart run livery -D APP=kiosk -D ENV=production   # reads config/apps/kiosk.yaml

Every run checks every app, not only the one it builds, and every app must have the same keys with the same types. So a key you add to one app and forget in another fails right away, whichever app you build. See Multiple apps and the example's multi_app/.

Define sources

livery reads define values from where you already pass them to Flutter. Later sources win, key by key:

  1. dart_defines_file: the DART_DEFINES line of Flutter's Generated.xcconfig, which flutter build writes.
  2. The DART_DEFINES environment variable, which Flutter sets for Xcode build phases.
  3. --dart-defines <payload>, in Flutter's encoding.
  4. -D KEY=VALUE, repeatable.

Only declared defines are read, and names and values are matched exactly, as String.fromEnvironment matches them. See Define sources.

Command line

dart run livery [options]
Option Meaning
-c, --config <path> The manifest to read. Defaults to the nearest livery.yaml, searched upwards from the working directory.
-r, --root <path> Directory output paths resolve against, in place of the manifest root.
-D, --define <KEY=VALUE> A define value. Repeatable. Wins over every other source.
--dart-defines <payload> Define values in Flutter's encoding. Wins over $DART_DEFINES.
--only <output> Write only the files of this output. Repeatable. Every output is still checked.
--dry-run Print what would be generated instead of writing it.
-v, --verbose Report what was resolved and which files were written.
-h, --help Print usage.
Exit code Meaning
0 Success.
1 The manifest or a define value is invalid, --only names an unknown output, or a file could not be read or written.
64 Bad command line usage, such as an unknown option.

A file whose content has not changed is not rewritten, so Xcode and Gradle do not rebuild for nothing.

Agent skill

livery ships an agent skill, livery-setup, that teaches a coding agent to set up livery in an existing Flutter app and to maintain it: it drafts livery.yaml from the project's current identity values, asks you for the defines rather than inventing them, wires iOS and Android by the guides' recipes, and later adds keys, defines, overrides and apps.

Install it, at the version of livery your project depends on, with the skills tool. Run it from the project root, or from the workspace root if the app is part of a pub workspace, so the skill lands next to your agent's other skills (for Claude Code, in .claude/skills/). With Dart 3.12 or later:

dart run skills@ get

dart run <package>@ needs Dart 3.12. With Dart 3.10 or 3.11, activate the tool instead:

dart pub global activate skills
skills get

The tool records what it installed in .config/dart_skills/skills_config.json. Commit it along with the skill if you keep your agent's config in git; otherwise, gitignore .config/dart_skills/.

Documentation

  • Manifest reference: every key, format and option, with defaults.
  • iOS guide: bundle identifier, display name and signing from an xcconfig.
  • Android guide: application id, version code and manifest placeholders from a Kotlin object.
  • Dart guide: typed constants and define enums in the app.
  • Example app: a Flutter app with iOS, Android and Dart wired to livery, and a multi-app manifest.

Contributing

Issues and pull requests are welcome; see CONTRIBUTING.md. Report security issues privately, as SECURITY.md describes.

Libraries