Skip to main content

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.

PartRoleModuleWhat it brings
Values in .envValues in .envdart_defineAppEnv, 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
FileFromWhat it holds
.envthe roleA comment and an empty line KEY= for each key of the app. You fill in the values here. It stays out of git.
.env.examplethe roleThe same keys without values. It stays in git, so a clone of the app knows which values to fill in.
.gitignorethe role adds a line.env, under a comment that tells where its values come from.
lib/core/env/app_env.dartthe module that provides the roleAppEnv, 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​

.env
# 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 writeThe app reads
KEY=value, a plain value without #, spaces or quotesvalue
KEY="any value", in double quotesany value. A value with a space, a # or a quote goes in double quotes.
KEY=value # a commentvalue. 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
warning

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:

KeyGetterModuleWhat goes there
REVENUECAT_APPLE_KEYAppEnv.revenuecatAppleKeyrevenuecatThe public API key of the App Store app in RevenueCat, which starts with appl_
REVENUECAT_GOOGLE_KEYAppEnv.revenuecatGoogleKeyrevenuecatThe public API key of the Play Store app in RevenueCat, which starts with goog_
REVENUECAT_TEST_KEYAppEnv.revenuecatTestKeyrevenuecatThe public API key of the Test Store in RevenueCat, which starts with test_, for debug builds only
PAYWALL_TERMS_URLAppEnv.paywallTermsUrlpaywallThe address of the terms of use that the paywall links to
PAYWALL_PRIVACY_URLAppEnv.paywallPrivacyUrlpaywallThe 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 module weather. It never starts with FLUTTER_.
  • The description is one line of at most 100 characters. It becomes the comment of the key in .env and the doc of its getter.
  • A brick of the module reads the value through a variable of envRole.varsOf(id, keys), named env_<key in snake_case>, such as {{{env_weather_api_key}}}. It is AppEnv.weatherApiKey in an app with the role and null in an app without it, so the code takes a String? there and is no const.
  • A module that requires the role may also read AppEnv in 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 .env has for its key, or null when 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 .env while the app runs, which takes a Future, or that sets the values in main(), does not keep the contract.
  • A command of the flutter tool that builds, runs or tests the app gives AppEnv the values that .env has when the command runs, and nothing else runs after .env changes. A provider whose values need code generation after an edit of .env does 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.