“Bro, did you just ship your API key in plain text inside the APK?”
That’s literally what I asked a friend recently when I cracked open their Flutter app’s .apk
.
Spoiler: the .env
file was right there, readable like a TODO comment.
And honestly? It’s not just them. I’ve seen this mistake across multiple teams. If you’re using flutter_dotenv
without understanding what it does in production, you’re probably exposing secrets too.
So, let’s sit down, dev-to-dev, and go through:
- Why this happens
- How to properly pass secrets
- When to use
--dart-define
vs.--dart-define-from-file
- And how to secure your Flutter apps like a pro
😱 The Mistake: Trusting .env
Files in Production
Here’s how it usually goes:
You install flutter_dotenv
, create a .env
file like this:
API_URL=https://myapi.com
API_KEY=super_secret_key
Then in your code:
import 'package:flutter_dotenv/flutter_dotenv.dart';
final apiUrl = dotenv.env['API_URL'];
final apiKey = dotenv.env['API_KEY'];
Looks clean, right?
🚨 But here’s the catch:
flutter:
assets:
- .env
This tells Flutter to bundle .env
as a plain asset. That means in your final .apk
(which is just a ZIP file), the .env
file is just sitting there under /assets/
, ready to be opened by anyone who downloads your app.
Don’t believe me? Try it:
unzip build/app/outputs/flutter-apk/app-release.apk
cat assets/.env
Yup. Your secrets are not secreting anymore.
✅ The Solution: Use --dart-define
to Pass Secrets Securely
Flutter gives you a better way: --dart-define
.
Instead of loading secrets from a file, you inject them at build time and retrieve them from Dart using String.fromEnvironment
.
🛠 How to Use It
flutter build apk \
--release \
--obfuscate \
--split-debug-info=./debug_info \
--dart-define=API_URL=https://myapi.com \
--dart-define=API_KEY=super_secret_key
And in your Dart code:
const apiUrl = String.fromEnvironment('API_URL');
const apiKey = String.fromEnvironment('API_KEY');
These values are compiled into the app binary — not bundled as assets — making them much harder to extract.
🤔 What If You Have Many Secrets? Use --dart-define-from-file
Now imagine this: you have 5, 10, maybe 15 different keys and toggles — API URLs, feature flags, auth tokens, etc.
Typing all of them into a CLI command gets ugly fast. That’s where --dart-define-from-file
comes in.
🔄 Instead of this:
flutter build apk \
--dart-define=API_URL=https://... \
--dart-define=API_KEY=... \
--dart-define=FEATURE_X=true \
--dart-define=CLIENT_ID=...
✅ Do this:
- Create a config file:
config.prod.json
{
"API_URL": "https://myapi.com",
"API_KEY": "super_secret_key",
"FEATURE_X": "true",
"CLIENT_ID": "xyz-123"
}
2. Build your app using:
flutter build apk \
--release \
--obfuscate \
--split-debug-info=./debug_info \
--dart-define-from-file=config.prod.json
3. Access values like before:
const apiUrl = String.fromEnvironment('API_URL');
const apiKey = String.fromEnvironment('API_KEY');
📦 Use Different Configs for Different Environments
Want to switch between environments?
Make multiple config files:
config.dev.json
config.staging.json
config.prod.json
Then:
flutter build apk --dart-define-from-file=config.dev.json
Easy to manage. Clean. Secure.
🚫 What Not to Do
Here are some real-world mistakes I’ve seen:
❌ Bundling .env
in assets
❌ Checking config.prod.json
with secrets into Git
❌ Hardcoding secrets in Dart
❌ Skipping code obfuscation
🔐 Bonus: Obfuscation + Code Shrinking = Stronger Defense
Reverse engineering is always a risk, but you can make it much harder:
✅ Enable R8
In android/gradle.properties
:
android.enableR8=true
✅ Obfuscate Your Code
flutter build apk \
--release \
--obfuscate \
--split-debug-info=./debug_info \
--dart-define-from-file=config.prod.json
✅ Add ProGuard Rules
Edit android/app/proguard-rules.pro
to hide sensitive class names or prevent reflection.
🧠 TL;DR — When to Use What
.env
+flutter_dotenv
—Use Case (Dev only) — Secure for Production? (❌ No) — Scalable? (🔸 Limited)--dart-define
—Use Case (Small set of secrets) — Secure for Production? (✅ Yes) — Scalable? (🔸 Medium)--dart-define-from-file
—Use Case (Many secrets or different envs) — Secure for Production? (✅ Yes) — Scalable? (✅ Excellent)
🧪 Real App Example
Let’s say your app needs:
API_URL
API_KEY
AUTH_DOMAIN
FIREBASE_ID
FEATURE_X
toggle
Create config.prod.json
:
{
"API_URL": "https://api.myapp.com",
"API_KEY": "prod-key",
"AUTH_DOMAIN": "auth.myapp.com",
"FIREBASE_ID": "abcd1234",
"FEATURE_X": "true"
}
Then build your app:
flutter build apk \
--release \
--obfuscate \
--split-debug-info=./debug_info \
--dart-define-from-file=config.prod.json
You now have a secure, production-ready build — without secrets exposed in assets.
🙌 Final Thoughts
If you’ve made it this far, here’s what I want you to remember:
.env
is not safe for production — don’t ship it.- Use
--dart-define
or--dart-define-from-file
instead. - Keep secrets out of your frontend when possible.
- Obfuscate. Shrink. Encrypt. Always.
👉 Don’t wait for a security audit or a breach to start caring about this.
Your users trust your app — protect that trust