# `dart_define`

| Package | Kind | Provides the role | Needs | Choose with |
| --- | --- | --- | --- | --- |
| [`smf_dart_define`](https://pub.dev/packages/smf_dart_define) | infrastructure | Values in .env | nothing | `-m dart_define` |

Some modules need values that only you have, such as the public key of a service or the address of a page. They name them as keys, and the role of the values in `.env` writes the keys into `.env` at the root of the app, where you fill them in; see [Values in .env](https://doc.saymyframe.com/guides/env.md). `dart_define` gets those values into the app with `--dart-define-from-file`, an option of the `flutter` tool. It adds no package to the app.

A command of the `flutter` tool that builds, runs or tests the app reads `.env` when it starts, given `--dart-define-from-file=.env`, and compiles each value into the app as a constant. Nothing is generated after you edit `.env`. The analyzer sees the values as empty, so a fresh clone of the app analyzes and builds before anyone fills in `.env`.

## What it adds to the app

| In the app | What it is |
| --- | --- |
| `lib/core/env/app_env.dart` | `AppEnv`, with a static getter for each key of the app. An app whose modules give no key gets the class without members. |
| `.vscode/launch.json` | The run configuration of VS Code, named after the app, which passes `--dart-define-from-file=.env`. |
| `.run/main.run.xml` | The run configuration of Android Studio, named after the app, which passes the same argument. |
| The section "Run, test and build with .env" of the README | The commands with the argument, the run configurations, when the values are read, and how to add a getter. |

Whichever module provides it, the role adds `.env`, with a comment and an empty line for each key, `.env.example`, with the same keys, a line of `.gitignore` that keeps `.env` out of git, and the section "Values in .env" of the README, which lists the keys of the app.

A getter reads its value with `String.fromEnvironment` and returns `null` while the value is empty. `KEY=` in `.env` defines an empty value rather than none, so an empty line of `.env` and a missing one look the same to the app. This is a getter of an app with [`revenuecat`](https://doc.saymyframe.com/modules/revenuecat.md):

```dart title="lib/core/env/app_env.dart"
abstract final class AppEnv {
  /// The public API key of the App Store app in RevenueCat, which starts with
  /// appl_.
  static String? get revenuecatAppleKey {
    const value = String.fromEnvironment('REVENUECAT_APPLE_KEY');
    return value.isEmpty ? null : value;
  }
}
```

The value is a constant of the build, so it is there before any code of the app runs, `main()` included, and a test reads it without starting the app.

In the [guide for coding agents](https://doc.saymyframe.com/guides/coding-agents.md) of the app, the role describes `AppEnv` under "Values in .env". The module adds the commands with the argument, the run configurations, the restart after an edit of `.env`, and how the getter of a new key reads its value.

## Run, test and build

Give the argument to every command of the `flutter` tool that builds, runs or tests the app:

```bash
flutter run --dart-define-from-file=.env
flutter test --dart-define-from-file=.env
flutter build apk --dart-define-from-file=.env
flutter build appbundle --dart-define-from-file=.env
flutter build ipa --dart-define-from-file=.env
```

Without it, every getter of `AppEnv` is `null`, as for a `.env` with no values. The last line of `smf create` is still `flutter run`: right after the app is generated every value of `.env` is empty, so the app is the same without the argument.

A command reads `.env` only when it starts. After you change `.env`, stop `flutter run` and start it again. Hot reload and hot restart keep the values that the app was built with.

The run configurations pass the argument for you:

| IDE | What passes it |
| --- | --- |
| VS Code | `.vscode/launch.json`. Its configuration is the template of every launch of a file of the app (`"templateFor": ""`), so Run and Debug (F5), the Run and Debug links above `main()` and above a test, and the Testing view all pass it. |
| Android Studio | `.run/main.run.xml`, the run configuration named after the app. A test that you run from Android Studio gets no argument, so run tests from a terminal with the argument. |

`.env` stays out of git. A machine that builds the app from a clone, such as a CI runner, needs a `.env` of its own before the build: copy `.env.example` and fill it in, or write it from the secrets of the CI.

## A key of your own

Add the key to `.env.example` and `.env`, under a comment that says what goes there, and give `AppEnv` a getter for it, as for the other keys:

```dart
static String? get myServiceKey {
  const value = String.fromEnvironment('MY_SERVICE_KEY');
  return value.isEmpty ? null : value;
}
```

The getter is the key in lowerCamelCase. Write the name of the key as a literal in `String.fromEnvironment`: the value is read when the app is compiled, so the name has to be a constant.

## What the module does not do

- It loads nothing while the app runs. A change of `.env` reaches the app with the next command that builds or runs it.
- It reads one file, `.env`. Another set of values, such as one for a staging backend, is another file that you pass yourself, as `--dart-define-from-file=.env.staging`.
- It keeps no secret. Every value is compiled into the app, where anyone who has the app can read it, so `.env` holds only values that may be public.

## In tests

`AppEnv` reads constants, so a widget test reads a value without running `main()`. A test gets the values of `.env` only with `flutter test --dart-define-from-file=.env`, and gets `null` from every getter otherwise.

Whichever module provides the role, a getter returns the value that `.env` has for its key, and `null` for a key that `.env` leaves empty or lacks. SMF's CI checks this in running apps with the role: the test parses the `.env` of the app, in both forms that a value may take, and compares each key with its getter. With this module, the apps of [`revenuecat`](https://doc.saymyframe.com/modules/revenuecat.md) and of [`paywall`](https://doc.saymyframe.com/modules/paywall.md) run it with values of the tests in `.env`. See [Contributing](https://doc.saymyframe.com/contributing.md#the-roles-at-run-time).

## Choosing it

`smf create` adds the module when a module of the app needs values of `.env` and no other module provides them:

```text
Adding dart_define: the only provider of the env role, which revenuecat requires.
```

To choose it yourself:

```bash
smf create my_app -m dart_define
```

An app with the module and no module that gives it keys gets `.env` without keys and `AppEnv` without getters, ready for keys of your own.
