“Wait… You’re Using .env Files in Flutter for Secrets?” Let’s Talk Before It’s Too Late

 “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:

  1. 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

  1. .env + flutter_dotenv —Use Case (Dev only) — Secure for Production? (❌ No) — Scalable? (🔸 Limited)
  2. --dart-define—Use Case (Small set of secrets) — Secure for Production? (✅ Yes) — Scalable? (🔸 Medium)
  3. --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

Rakibul hasan

I am Rakibul hasan,Front and Back-End Developer.I am also good at database like mysql,Firebase,mssql. I'm a Workaholic Person and I love my job as a mobile application developer i never get tired for it. Its been always fasinating to me working for new creation. I am very passionate about my work.I do my job passionately and dedicatedly.I try to do my project with clean code and gather efficiency. The greatest quality of mine is patience.I can code all day long. I never feel bored during coding and i can't take rest until my code is completed.I can adapt anything very quickly. I am also very sincere,responsible,hard working and confident regarding my works.Each and Every Project is important to me and I like to take Challenge.

Post a Comment

Previous Post Next Post