native_build_env_sdk 0.1.0
native_build_env_sdk: ^0.1.0 copied to clipboard
Loads flavor-specific native build configuration in Flutter without dart-define.
Native Build Env SDK #
Flutter plugin đọc cấu hình công khai được tạo trong native build, không dùng
--dart-define và không thêm .env vào Flutter assets.
Luồng hoạt động #
Android runtime:
.env -> Gradle flavor -> Android string resource -> Kotlin -> MethodChannel
iOS compile-time:
Scheme Pre-action -> .envfile -> Swift generator -> Flutter/tmp.xcconfig
iOS runtime:
.envfile -> CocoaPods before_compile -> generated Objective-C header
-> Objective-C plugin -> MethodChannel
SDK không bảo vệ secret. Mọi giá trị nằm trong APK/IPA đều có thể bị trích xuất. Chỉ dùng cho URL, environment name, feature flag, public client ID và public key đã được giới hạn quyền.
1. Thêm dependency #
Trong giai đoạn beta, nên pin chính xác phiên bản:
dependencies:
native_build_env_sdk: 0.1.0-beta.1
Với package local:
dependencies:
native_build_env_sdk:
path: ../flutter_native_env
Sau đó chạy:
fvm flutter pub get
2. Tạo file môi trường #
Có thể đặt file ở bất kỳ đâu. Ví dụ tại Flutter project root:
.env.development
.env.staging
.env.production
APP_ENV=development
APP_NAME="Example Development"
API_BASE_URL=https://dev-api.example.com
ENABLE_LOGGING=true
Không khai báo các file này trong flutter.assets. Parser hỗ trợ KEY=VALUE,
comment bắt đầu bằng #, single/double quote và tiền tố export. Parser không
hỗ trợ expansion như $OTHER_KEY.
3. Android #
Đặt cấu hình SDK sau khi apply Android/Flutter plugins nhưng trước block
android { ... }. Script đọc env của flavor đang được build và expose map tại
project.env, tương tự flutter_config:
project.ext.nativeBuildEnvFiles = [
//{flavor}: {env path}
development: ".env.development",
staging : ".env.staging",
production : ".env.production",
]
project.ext.nativeBuildEnvDefaultFlavor = "development"
project.ext.nativeBuildEnvRequiredKeys = ["APP_ENV", "API_BASE_URL"]
apply from: project(':native_build_env_sdk').projectDir.getPath() +
"/native_build_env.gradle"
nativeBuildEnvDefaultFlavor được dùng khi Gradle sync hoặc task không chứa tên
flavor. Khi chạy task như assembleProductionRelease, script tự chọn
.env.production. Có thể override rõ ràng bằng
-PnativeBuildEnvFlavor=production hoặc environment variable
NATIVE_BUILD_ENV_FLAVOR.
Đường dẫn tương đối được tính từ Flutter project root; đường dẫn tuyệt đối và
File cũng được hỗ trợ. Key trong map phải khớp tên productFlavors.
Đọc env trong app/build.gradle #
Sau dòng apply from, đọc value trực tiếp:
def verName = project.env.get("VER_NAME")
if (verName == null || verName.isEmpty()) {
verName = "1.0"
}
def verBuild = project.env.get("VER_BUILD")
if (verBuild == null || verBuild.isEmpty()) {
verBuild = "1"
}
App tự quyết định dùng values ở đâu; plugin không tự ghi đè Flutter config:
android {
defaultConfig {
versionName verName
versionCode verBuild.toInteger()
}
flavorDimensions "environment"
productFlavors {
development { dimension "environment" }
staging { dimension "environment" }
production { dimension "environment" }
}
}
Ví dụ env:
VER_NAME=1.2.3-dev
VER_BUILD=12301
Nếu muốn fallback về version Flutter thay vì literal, dùng:
def verName = project.env.get("VER_NAME") ?: flutter.versionName
def verBuild = (project.env.get("VER_BUILD")
?: flutter.versionCode.toString()).toInteger()
Do app tự gán version, precedence hoàn toàn nằm trong code của app; SDK chỉ đọc và expose string values.
Dùng env value ở Android compile-time #
Mỗi env key tự động trở thành string resource, không cần khai báo danh sách:
KEY_NAME -> @string/native_build_env_key_name
Với APP_NAME="Example Development", plugin tạo:
@string/native_build_env_app_name
Dùng trong android/app/src/main/AndroidManifest.xml:
<application
android:label="@string/native_build_env_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- ... -->
</application>
Plugin vẫn serialize toàn bộ map thành Base64 JSON cho runtime Flutter. Nếu hai key chỉ khác hoa/thường tạo cùng Android resource name, build sẽ fail.
Cấu hình cũ ext.nativeBuildEnv = [envDir: ..., files: ...] vẫn được hỗ trợ.
fvm flutter run --flavor development
fvm flutter build appbundle --flavor production
4. iOS #
Mỗi flavor iOS tương ứng với một Xcode Scheme. iOS dùng cùng một file .envfile
cho hai pipeline độc lập:
- Scheme Pre-action tạo
tmp.xcconfigđể dùng env trong Xcode/Info.plist. - CocoaPods script phase tự generate runtime config trước khi compile plugin.
App không cần thêm Runner Build Phase, JSON resource hoặc Info.plist payload.
4.1. Cập nhật CocoaPods #
Sau khi thêm hoặc nâng phiên bản plugin, chạy pod install trong thư mục ios.
Podspec tự thêm phase Generate Native Build Env vào pod target với vị trí
before_compile.
4.2. Chọn env trong từng Scheme #
Trong Xcode, mở:
Product > Scheme > Edit Scheme > Build > Pre-actions
Chọn Runner tại Provide build settings from. Với Scheme development,
thêm script:
set -eu
echo ".env.development" > "${SRCROOT}/.envfile"
/bin/sh \
"${SRCROOT}/.symlinks/plugins/native_build_env_sdk/ios/Scripts/RunNativeBuildEnvGenerator.sh" \
--ios-root "${SRCROOT}" \
--format xcconfig \
--output "${SRCROOT}/Flutter/tmp.xcconfig"
Mỗi Scheme phải có toàn bộ Pre-action trên để luôn tạo lại cả .envfile và
tmp.xcconfig. Với Scheme production, dùng cùng script nhưng chọn file khác:
set -eu
echo ".env.production" > "${SRCROOT}/.envfile"
/bin/sh \
"${SRCROOT}/.symlinks/plugins/native_build_env_sdk/ios/Scripts/RunNativeBuildEnvGenerator.sh" \
--ios-root "${SRCROOT}" \
--format xcconfig \
--output "${SRCROOT}/Flutter/tmp.xcconfig"
Wrapper loại các biến iOS SDK do Xcode truyền vào trước khi chạy Swift bằng
macOS SDK. Việc này tránh lỗi trộn iPhoneSimulator sysroot với macOS target
trong Scheme Pre-action.
.envfile và tmp.xcconfig được dùng chung giữa các Scheme; Pre-action của
Scheme đang build sẽ ghi đè chúng bằng môi trường tương ứng. .envfile chỉ chứa
tên hoặc đường dẫn env, không chứa values. Đường dẫn tương đối được resolve từ
Flutter project root; đường dẫn tuyệt đối cũng được hỗ trợ.
4.3. Dùng env ở compile-time #
Thêm vào cuối các Runner xcconfig, ví dụ Debug.xcconfig và Release.xcconfig:
#include? "tmp.xcconfig"
Sau đó có thể dùng env như Xcode Build Settings. Ví dụ trong Info.plist:
<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>
Hoặc dùng $(API_BASE_URL) trong một build setting khác. Generator escape URL
để // không bị xcconfig hiểu là comment.
Xcode có thể nạp xcconfig trước Scheme Pre-action trong clean build đầu tiên.
Nếu compile-time value chưa cập nhật, build lại một lần. Điều này không ảnh
hưởng runtime config vì pod script phase đọc .envfile trực tiếp sau Pre-action.
4.4. Runtime config tự động #
Không cần cấu hình thêm trong Runner. Khi Xcode build:
Scheme Pre-action ghi ios/.envfile
↓
CocoaPods Generate Native Build Env chạy before_compile
↓
Swift generator đọc đúng .env
↓
DERIVED_FILE_DIR/NativeBuildEnv.generated.h
↓
NativeBuildEnvSdkPlugin.m compile payload vào pod binary
↓
NativeBuildEnv.initialize() nhận map qua MethodChannel
Generated header chỉ nằm trong Xcode DerivedData, không sửa pub cache hoặc source của app. Pod script không log env values.
Pod target được cấu hình ENABLE_USER_SCRIPT_SANDBOXING=NO để phase có thể đọc
file env được chọn bên ngoài thư mục Pods. App Runner không cần tắt sandbox.
4.5. Thứ tự chọn env #
Cả chế độ xcconfig và runtime header dùng cùng Swift generator và precedence:
NATIVE_BUILD_ENV_FILE environment variable
↓
ENVFILE environment variable
↓
ios/.envfile
↓
<Flutter root>/.envfile
↓
<Flutter root>/.env
Scheme Pre-action là cách khuyến nghị vì explicit và không phụ thuộc cách đặt tên Build Configuration.
Có thể khai báo optional build setting:
NATIVE_BUILD_ENV_REQUIRED_KEYS = APP_ENV,API_BASE_URL
Generator validate danh sách này khi setting có trong process environment. Dart
nên tiếp tục validate runtime bằng NativeBuildEnv.initialize(requiredKeys:).
4.6. File generated cần ignore #
Thêm vào .gitignore của app:
ios/.envfile
ios/Flutter/tmp.xcconfig
Không ignore .env theo mặc định nếu project chủ động version-control public
configuration; nếu env chứa dữ liệu nội bộ, quản lý bằng CI hoặc cơ chế phù hợp.
5. Flutter #
Khởi tạo trước runApp:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await NativeBuildEnv.initialize(
requiredKeys: const ['APP_ENV', 'API_BASE_URL'],
);
runApp(const MyApp());
}
Đọc giá trị:
final environment = NativeBuildEnv.get('APP_ENV');
final apiBaseUrl = NativeBuildEnv.get('API_BASE_URL');
final logging = NativeBuildEnv.getBool('ENABLE_LOGGING', defaultValue: false);
Map được cache immutable. MethodChannel chỉ gọi lại khi truyền
forceReload: true.
6. CLI độc lập #
fvm dart run native_build_env_sdk:generate \
--input .env.development \
--output build/native_env/development.json \
--required APP_ENV,API_BASE_URL
CLI fail khi file không tồn tại, assignment sai, key trùng hoặc thiếu required key. CLI không in env values.
Bảo mật #
Mọi config compile vào APK/IPA đều có thể bị trích xuất. Base64 chỉ là encoding. Không đưa private API key, client secret, password, signing key, service account hoặc encryption key cần giữ bí mật vào plugin. Secret phải nằm trên backend hoặc secret manager; token runtime nên ngắn hạn và lưu bằng Keychain/Keystore.
Troubleshooting iOS #
- Nếu pod runtime không cập nhật sau khi nâng plugin: chạy lại
pod install. - Nếu build báo thiếu
.env: kiểm tra nội dungios/.envfilevà đường dẫn từ Flutter project root. - Nếu compile-time
$(KEY)cũ nhưng runtime đúng: build lại để Xcode reloadtmp.xcconfig. - Nếu script fail: xem phase
Generate Native Build Envtrong Pods target; log chỉ hiển thị tên file, không hiển thị values. - Thay đổi
.envluôn yêu cầu build lại app.
English Documentation #
Native Build Env SDK reads public configuration generated during native builds.
It does not use --dart-define or add .env files to Flutter assets.
How it works #
Android runtime:
.env -> Gradle flavor -> Android string resource -> Kotlin -> MethodChannel
iOS compile time:
Scheme Pre-action -> .envfile -> Swift generator -> Flutter/tmp.xcconfig
iOS runtime:
.envfile -> CocoaPods before_compile -> generated Objective-C header
-> Objective-C plugin -> MethodChannel
The SDK does not protect secrets. Anything packaged in an APK or IPA can be extracted. Use it only for public configuration.
1. Add the dependency #
dependencies:
native_build_env_sdk: 0.1.0-beta.1
Then run:
fvm flutter pub get
2. Create environment files #
Files may live anywhere. A common layout at the Flutter project root is:
.env.development
.env.staging
.env.production
APP_ENV=development
APP_NAME="Example Development"
API_BASE_URL=https://dev-api.example.com
ENABLE_LOGGING=true
Do not add these files to flutter.assets. The parser supports assignments,
comments, single/double quotes, and the optional export prefix. Variable
expansion is not supported.
3. Android integration #
Place the SDK configuration after the Android/Flutter plugins are applied but
before android { ... }. The script loads the env selected for the current
build and exposes it as project.env, similar to flutter_config:
project.ext.nativeBuildEnvFiles = [
development: ".env.development",
staging : ".env.staging",
production : ".env.production",
]
project.ext.nativeBuildEnvDefaultFlavor = "development"
project.ext.nativeBuildEnvRequiredKeys = ["APP_ENV", "API_BASE_URL"]
apply from: project(':native_build_env_sdk').projectDir.getPath() +
"/native_build_env.gradle"
nativeBuildEnvDefaultFlavor is used during Gradle sync or when a task does not
contain a flavor name. A task such as assembleProductionRelease automatically
selects .env.production. Override explicitly with
-PnativeBuildEnvFlavor=production or the NATIVE_BUILD_ENV_FLAVOR environment
variable.
Relative paths use the Flutter project root. Absolute paths and File values
are supported. Every map key must match a productFlavor.
Read env in app/build.gradle #
After apply from, read values directly:
def verName = project.env.get("VER_NAME")
if (verName == null || verName.isEmpty()) {
verName = "1.0"
}
def verBuild = project.env.get("VER_BUILD")
if (verBuild == null || verBuild.isEmpty()) {
verBuild = "1"
}
The host app decides where to use them; the plugin never overrides Flutter configuration:
android {
defaultConfig {
versionName verName
versionCode verBuild.toInteger()
}
flavorDimensions "environment"
productFlavors {
development { dimension "environment" }
staging { dimension "environment" }
production { dimension "environment" }
}
}
Example env values:
VER_NAME=1.2.3-dev
VER_BUILD=12301
To fall back to Flutter's version instead of literals:
def verName = project.env.get("VER_NAME") ?: flutter.versionName
def verBuild = (project.env.get("VER_BUILD")
?: flutter.versionCode.toString()).toInteger()
Because the host assigns the version, precedence is entirely controlled by app code; the SDK only reads and exposes string values.
Use an env value at Android compile time #
Every env key automatically becomes a string resource without an allowlist:
KEY_NAME -> @string/native_build_env_key_name
Given APP_NAME="Example Development", the plugin generates:
@string/native_build_env_app_name
Use it in android/app/src/main/AndroidManifest.xml:
<application
android:label="@string/native_build_env_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- ... -->
</application>
The plugin still serializes the complete map as Base64 JSON for Flutter runtime. If keys differing only by letter case produce the same Android resource name, the build fails.
The legacy ext.nativeBuildEnv structure remains supported.
fvm flutter run --flavor development
fvm flutter build appbundle --flavor production
4. iOS integration #
Each iOS flavor maps to an Xcode Scheme. One .envfile drives two independent
pipelines:
- The Scheme Pre-action generates
tmp.xcconfigfor Xcode and Info.plist. - A CocoaPods script phase generates runtime configuration before compilation.
No Runner Build Phase, JSON resource, or Info.plist payload is required.
4.1. Update CocoaPods #
After adding or upgrading the plugin, run pod install in the ios directory.
The podspec installs a Generate Native Build Env phase on the pod target at
before_compile.
4.2. Select an env file in each Scheme #
Open:
Product > Scheme > Edit Scheme > Build > Pre-actions
Select Runner under Provide build settings from. For the development
scheme, add:
set -eu
echo ".env.development" > "${SRCROOT}/.envfile"
/bin/sh \
"${SRCROOT}/.symlinks/plugins/native_build_env_sdk/ios/Scripts/RunNativeBuildEnvGenerator.sh" \
--ios-root "${SRCROOT}" \
--format xcconfig \
--output "${SRCROOT}/Flutter/tmp.xcconfig"
Every Scheme must contain the complete Pre-action so both .envfile and
tmp.xcconfig are regenerated. For the production Scheme, use the same script
with a different selected file:
set -eu
echo ".env.production" > "${SRCROOT}/.envfile"
/bin/sh \
"${SRCROOT}/.symlinks/plugins/native_build_env_sdk/ios/Scripts/RunNativeBuildEnvGenerator.sh" \
--ios-root "${SRCROOT}" \
--format xcconfig \
--output "${SRCROOT}/Flutter/tmp.xcconfig"
The wrapper removes iOS SDK variables inherited from Xcode before invoking
Swift with the macOS SDK. This prevents Scheme Pre-actions from mixing an
iPhoneSimulator sysroot with a macOS target.
.envfile and tmp.xcconfig are shared by all Schemes; the Pre-action of the
Scheme being built overwrites them with the corresponding environment.
.envfile stores only the selected name or path, not environment values.
Relative paths resolve from the Flutter project root; absolute paths are
supported.
4.3. Use env at compile time #
Append this to Runner xcconfig files such as Debug.xcconfig and
Release.xcconfig:
#include? "tmp.xcconfig"
Values then become Xcode Build Settings. For example, in Info.plist:
<key>CFBundleDisplayName</key>
<string>$(APP_NAME)</string>
The generator escapes URL separators so xcconfig does not interpret // as a
comment. Xcode may load xcconfig before the Pre-action during the first clean
build; build once more if a compile-time value has not refreshed. Runtime is
not affected because the pod phase reads .envfile after the Pre-action.
4.4. Automatic runtime configuration #
The host app needs no additional Runner configuration:
Scheme Pre-action writes ios/.envfile
↓
CocoaPods Generate Native Build Env runs before_compile
↓
Swift generator reads the selected .env
↓
DERIVED_FILE_DIR/NativeBuildEnv.generated.h
↓
NativeBuildEnvSdkPlugin.m compiles the payload into the pod binary
↓
NativeBuildEnv.initialize() receives the map through MethodChannel
The generated header stays in DerivedData; the script does not modify the pub cache or host source and never logs values. Script sandboxing is disabled only for the pod target so it can read the selected file outside Pods. Runner does not need this setting.
4.5. Env selection precedence #
Both output modes use the same Swift generator and precedence:
NATIVE_BUILD_ENV_FILE environment variable
↓
ENVFILE environment variable
↓
ios/.envfile
↓
<Flutter root>/.envfile
↓
<Flutter root>/.env
The Scheme Pre-action is recommended because it is explicit and independent of Build Configuration naming. Optional validation can be configured with:
NATIVE_BUILD_ENV_REQUIRED_KEYS = APP_ENV,API_BASE_URL
The generator validates it when available in the process environment. Continue
to validate runtime values through NativeBuildEnv.initialize(requiredKeys:).
4.6. Ignore generated files #
ios/.envfile
ios/Flutter/tmp.xcconfig
5. Flutter integration #
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await NativeBuildEnv.initialize(
requiredKeys: const ['APP_ENV', 'API_BASE_URL'],
);
runApp(const MyApp());
}
final environment = NativeBuildEnv.get('APP_ENV');
final apiBaseUrl = NativeBuildEnv.get('API_BASE_URL');
final logging = NativeBuildEnv.getBool('ENABLE_LOGGING', defaultValue: false);
Values are cached in an immutable map. The MethodChannel reloads only when
forceReload: true is requested.
6. Standalone CLI #
fvm dart run native_build_env_sdk:generate \
--input .env.development \
--output build/native_env/development.json \
--required APP_ENV,API_BASE_URL
The CLI fails on missing files, invalid assignments, duplicate keys, or missing required keys. It never logs values.
Security #
Every value compiled into an APK or IPA can be extracted; Base64 is encoding, not encryption. Never embed private API keys, client secrets, passwords, signing keys, service-account credentials, or encryption keys that must remain secret. Keep secrets on a backend or in a secret manager, and use short-lived runtime tokens stored in Keychain or Keystore.
iOS troubleshooting #
- Run
pod installagain after upgrading the plugin so the podspec phase is refreshed. - If runtime generation reports a missing file, check
ios/.envfileand resolve the selected path from the Flutter project root. - If a compile-time
$(KEY)is stale while runtime is correct, build again so Xcode reloadstmp.xcconfig. - Inspect
Generate Native Build Envin the Pods target when generation fails; logs include only the file name, never values. - Changing
.envalways requires rebuilding the app.