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

Md. Al - AminMd. Al - Amin
4 min read

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"
}
  1. Build your app using:
flutter build apk \
  --release \
  --obfuscate \
  --split-debug-info=./debug_info \
  --dart-define-from-file=config.prod.json
  1. 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

MethodUse CaseSecure for Production?Scalable?
.env + flutter_dotenvDev only❌ No🔸 Limited
--dart-defineSmall set of secrets✅ Yes🔸 Medium
--dart-define-from-fileMany secrets or different env’s✅ Yes✅ 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.

0
Subscribe to my newsletter

Read articles from Md. Al - Amin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Md. Al - Amin
Md. Al - Amin

Experienced Android Developer with a demonstrated history of working for the IT industry. Skilled in JAVA, Dart, Flutter, and Teamwork. Strong Application Development professional with a Bachelor's degree focused in Computer Science & Engineering from Daffodil International University-DIU.