Values in .env
Some modules need values that only you have, such as the public API key of a service or the address of a page. A module names each value as a key, and the role of the values in .env puts the keys of every module of the app into .env at the root of the app. You fill in the values there, and code reads them through AppEnv.
| Part | Role | Module | What it brings |
|---|---|---|---|
| Values in .env | Values in .env | dart_define | AppEnv, whose getters return the values of .env, compiled into the app with --dart-define-from-file. |
A module that needs such values brings dart_define with it while that module is the only one that provides the role, as revenuecat and paywall do:
smf create my_app -m paywall,bloc
| File | From | What it holds |
|---|---|---|
.env | the role | A comment and an empty line KEY= for each key of the app. You fill in the values here. It stays out of git. |
.env.example | the role | The same keys without values. It stays in git, so a clone of the app knows which values to fill in. |
.gitignore | the role adds a line | .env, under a comment that tells where its values come from. |
lib/core/env/app_env.dart | the module that provides the role | AppEnv, with a static getter for each key. |
Right after smf create, every value is empty, and the app builds and starts. A module whose value is missing works without it as far as it can: revenuecat runs the app without purchases, and paywall leaves out a link.
Write a value
# The public API key of the Test Store in RevenueCat, which starts with test_, for debug builds only
REVENUECAT_TEST_KEY=test_AbCdEf123
# The address of the terms of use that the paywall links to
PAYWALL_TERMS_URL="https://example.com/terms#refunds"
| You write | The app reads |
|---|---|
KEY=value, a plain value without #, spaces or quotes | value |
KEY="any value", in double quotes | any value. A value with a space, a # or a quote goes in double quotes. |
KEY=value # a comment | value. After a plain value, # starts a comment, so an address with an anchor needs the quotes. |
KEY= | null. An empty value counts as missing. |
A line that starts with # | Nothing: it is a comment. |
The comment at the top of .env and .env.example says this in one line. Every module that provides the role reads both forms of a value alike.
After a clone of the app, copy the example and fill it in:
cp .env.example .env
Every value ends up in the app, where anyone who has the app can read it. So .env holds only values that may be public, such as the public key of a service or the address of a page. Never put a secret there, such as the secret key of a server, a password or a private key.
Read a value
Code reads a value only through the static getter of its key in AppEnv. The getter is the key in lowerCamelCase, and returns a String?, which is null while the value is empty or missing:
import 'core/env/app_env.dart';
final terms = AppEnv.paywallTermsUrl;
if (terms != null) showTermsLink(Uri.parse(terms));
A getter returns its value at once, before any code of the app runs, main() included, so code reads it anywhere without waiting, and so does a test that does not start the app. Handle null: a value that nobody filled in yet is no error. Never read .env or .env.example from code.
The keys of the built-in modules:
| Key | Getter | Module | What goes there |
|---|---|---|---|
REVENUECAT_APPLE_KEY | AppEnv.revenuecatAppleKey | revenuecat | The public API key of the App Store app in RevenueCat, which starts with appl_ |
REVENUECAT_GOOGLE_KEY | AppEnv.revenuecatGoogleKey | revenuecat | The public API key of the Play Store app in RevenueCat, which starts with goog_ |
REVENUECAT_TEST_KEY | AppEnv.revenuecatTestKey | revenuecat | The public API key of the Test Store in RevenueCat, which starts with test_, for debug builds only |
PAYWALL_TERMS_URL | AppEnv.paywallTermsUrl | paywall | The address of the terms of use that the paywall links to |
PAYWALL_PRIVACY_URL | AppEnv.paywallPrivacyUrl | paywall | The address of the privacy policy that the paywall links to |
The keys are in .env in the order of the modules of the app. The section "Values in .env" of the README of the app lists the keys of that app, each with its getter, its module and what goes there.
Get the values into the app
How the values get into the app is up to the module that provides the role. With dart_define, the flutter tool reads .env when a command that builds, runs or tests the app starts, given --dart-define-from-file=.env:
flutter run --dart-define-from-file=.env
Without the argument, every getter is null. The run configurations of VS Code and Android Studio pass it for you, and a change of .env reaches the app once you start flutter run again; see dart_define.
A key of your own
Put the key into both .env.example and .env, in UPPER_SNAKE_CASE, with a comment line above it that says what goes there. Then give AppEnv a getter for it, named by the key in lowerCamelCase, as the module that provides the role writes the others. The section of that module in the README of the app shows the getter.
In a module of your own
A module that needs a value gives the role a key, an EnvKey with a name and a description, and lists the role among the roles it requires or uses:
static const apiKey = EnvKey(
'WEATHER_API_KEY',
description: 'The public API key of the weather service',
);
- The name is in UPPER_SNAKE_CASE, of two words or more, and starts with the id of the module in upper case and an underscore, such as
WEATHER_for a moduleweather. It never starts withFLUTTER_. - The description is one line of at most 100 characters. It becomes the comment of the key in
.envand the doc of its getter. - A brick of the module reads the value through a variable of
envRole.varsOf(id, keys), namedenv_<key in snake_case>, such as{{{env_weather_api_key}}}. It isAppEnv.weatherApiKeyin an app with the role andnullin an app without it, so the code takes aString?there and is noconst. - A module that requires the role may also read
AppEnvin its own code.
The module rule env.keys checks the keys and the variables of a module. env.key_access checks that the files of a module read only its own keys and those of the modules it depends on, and env.keys_rendered that AppEnv has a getter for every key. The template of a role gives no key.
For a provider
A module that provides the role generates AppEnv in lib/core/env/app_env.dart: an abstract final class with a static String? getter for each key of envRole.keysIn(input). Its getters keep to the contract of the role:
- A getter returns the value that
.envhas for its key, ornullwhen the value is empty or the key is missing. - It returns it at once, before any code of the app runs. A provider that loads
.envwhile the app runs, which takes aFuture, or that sets the values inmain(), does not keep the contract. - A command of the
fluttertool that builds, runs or tests the app givesAppEnvthe values that.envhas when the command runs, and nothing else runs after.envchanges. A provider whose values need code generation after an edit of.envdoes not keep the contract either.
A provider that needs arguments on those commands gives them to the role as EnvToolArguments, as dart_define gives --dart-define-from-file=.env. Code that runs the commands, such as a run configuration or a test tool, takes them from envRole.toolArgumentsIn(input) without knowing what they mean.
In tests
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, with a fixture provider, and with a broken provider that the test must fail on: one whose AppEnv returns an empty value as an empty text. See Contributing.