Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working with multiple env files #16

Closed
Isuru-Nanayakkara opened this issue Nov 13, 2022 · 32 comments
Closed

Working with multiple env files #16

Isuru-Nanayakkara opened this issue Nov 13, 2022 · 32 comments
Labels
duplicate This issue or pull request already exists question Further information is requested

Comments

@Isuru-Nanayakkara
Copy link

Hi,

I usually have 3 flavors in my projects - dev, QA and prod. And I create env files for each flavor.

But using envied, I have to point it to a single env file.

@Envied(path: 'env/dev.env')
abstract class Env { }

Now if I want to switch between running different flavor, I have to manually edit the env path and run code generation every time.

Is there a better way to do this using envied?

@ThexXTURBOXx
Copy link
Contributor

You should be able to create 3 classes, e.g., EnvDev, EnvQA, and EnvProd and then use three different @Envied annotations with three different path and name values.

@ThexXTURBOXx
Copy link
Contributor

@Isuru-Nanayakkara Did it work or have there been any problems? :)

@Isuru-Nanayakkara
Copy link
Author

It technically works. However the problem is if I have 3 separate classes like that, wherever I have to access the env variables, I'll have to check which flavor I'm on at runtime and then call the relevant class. Which will result in a lot of switch case statements all over the codebase. Sooo not sure if that's the optimal way. I'm also thinking of a better way to handle this.

@YoungEii
Copy link

YoungEii commented Dec 22, 2022

@Isuru-Nanayakkara You were right. I also face the same situation. The better way is to use one entry to access the data. Maybe we can optimize the codegen.

@marcel-ploch
Copy link

marcel-ploch commented Jan 6, 2023

Would it be an idea to have the path to the env file in the builder config?
Than it would be possible to have different build configs with proper file handling.

Like buily.ci.yaml, build.test.yaml, build.yaml, build.dev.yaml

The Build runner could potentially be called with

flutter pub run build_runner build --delete-conflicting-outputs --config <your config name>

and inside the yaml we could have:

envied_generator|envied:
       enabled: False
       path: .env.<your config key>

Think that would be a nice option.

Just created this PR:

#20

Maybe if it is a good idea you can take it ^^

As example it would work like this:

targets:
  $default:
    builders:
      envied_generator|envied:
        options:
          path: .env.test
          override: true

@chillbrodev
Copy link

@Isuru-Nanayakkara I similarly have this multi flavor setup. A solution my team and I came up with is to utilize some bash scripts to load my env from 1password which overwrites the .env file. That way when we need to switch environments we run a flavor-setup script which pulls in the right environment. Obviously 1password doesn't work on remote CI/CD servers so you could utilize base64 encoding/decoding of the env file and load in the appropriate base64 encoded env file for said flavor. Still a bit manual but works within the current structures of envied.

If you are interested in this approach I can share my scripts with you here.

@Isuru-Nanayakkara
Copy link
Author

@chillbrodev That's great! I already have my env files as base64 encoded strings in the CI/CD so that approach would be easy to implement. Please do share the script. Thank you.

@chillbrodev
Copy link

@chillbrodev That's great! I already have my env files as base64 encoded strings in the CI/CD so that approach would be easy to implement. Please do share the script. Thank you.

You would run this inside your flutter project. We have a scripts folder that we put a bunch of scripts in and check it in. Enjoy!

Here is an example 1password secure note:

FLAVOR=dev
SOME_API_KEY=123456abcdefg
SOME_OTHER_KEY=98776ooiill

Here is the 1password script (Replace the values with your values):

# log in to 1Password
eval $(op signin --account <1passwordAccount>)
# Get exit code from `op whoami` since eval always returns 0
op whoami > /dev/null
if [ $? -ne 0 ]; then
  echo "Error: 1Password login failed"
  exit 1
fi

echo "Getting flutter dev env vars from 1Password..."
# get pem from 1Password
DEV_ENVS=$(op item get "<Secured Note>" --vault <1password Vault> --format json | jq '.fields[0].value' | sed "s/\"\(.*\)\"/\1/" | sed 's/\\"/"/g')

ENV=".env"

echo $DEV_ENVS > $ENV

@Isuru-Nanayakkara
Copy link
Author

Thanks so much @chillbrodev 🙂 Appreciate it! I'll give it a try.

@chillbrodev
Copy link

Thanks so much @chillbrodev 🙂 Appreciate it! I'll give it a try.

Sounds great, lemme know if you run into any issues or need further help.

@ctsstc
Copy link

ctsstc commented Feb 8, 2023

Coming from JS and Ruby I'm used to the pattern that there's a base .env file and then you can have a .env.local that merges over the .env values; where the .env.local takes precedence over the values in .env for duplicate keys.

Here's a nice inheritance breakdown from the classic ruby dotenv:
https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use

@ThexXTURBOXx
Copy link
Contributor

@ctsstc I would think that this can already be done using envied:
As already said in my comment before, you just need to change the path parameter of the @Envied annotation and regenerate the values.
That would however not solve the issue at hand: Needing multiple switch statements in order to access the data.

That is also the reason why I think that #20 won't change too much about the issue at hand: You still need to change the yaml file in order to change the location of the env file

@lucasdidur
Copy link

Maybe can use someking like FirebaseOptions generation, use a superclass on a bootstrap param.

@rbdog
Copy link

rbdog commented May 18, 2023

I simply hope to stop using "static" values.
We need switch instance of Env class.

Like this

// now
Env.key1

// expected
Env().key1

Then, We can switch like this

if (prod) {
  return Env();
} else {
  return EnvDev();
}

@chillbrodev
Copy link

Why do you need to use a whole separate instance of Envied? Assuming your keys are the same across Dev/Production, you could change out the .env file before you deploy to production.

@rbdog
Copy link

rbdog commented May 18, 2023

The design I described above is incorrect.
Thank you very much.

I was thinking about an app that includes a developer mode, so initially, I wanted to deploy it with multiple environment configurations.
However, as I contemplated what an "environment" really means, my perspective has changed.

@affansk
Copy link

affansk commented Jul 7, 2023

Hello All, I am Pretty new to Flutter. just want to understand how will i run production enviornment using flutter run ?
I have 2 environment development and production, by default it runs development enviornment

I want to run my application in production mode, using flutter run. i tried few things but it didnt work

@skibble-app
Copy link

I had the same problem but was able to create a temporary solution using a singleton class and package_info_plus package.

Since using 3 different classes made things really hard, I had to reduce the stress and decided to add the variables(both dev and prod) to just one .env file. Just append "dev" or "prod" to the key names.

E.g

//.env
GOOGLE_API_PROD = ...
GOOGLE_API_DEV = ...

Then do the following:

Step 1:
Create a new file called app_environment.dart that will contain our singleton, then copy this code into the file:

import 'package:flutter/cupertino.dart';
import 'package:package_info_plus/package_info_plus.dart';

enum AppEnvironmentType { dev, prod }

class AppEnvironment {

  static PackageInfo? packageInfo;

  static AppEnvironmentType? currentEnv;
  static final AppEnvironment _instance = AppEnvironment._internal();


  factory AppEnvironment() {
    return _instance;
  }

  AppEnvironment._internal() {
    // initialization logic
  }

  static Future<void> init() async {
    PackageInfo.fromPlatform().then((PackageInfo packageInfo) {
      packageInfo = packageInfo;

      //testing to confirm that the right package name is printed
      debugPrint(packageInfo.packageName);
      switch (packageInfo.packageName) {
       //replace with your bundle name. I assume your dev flavor has .dev suffix
        case "<bundle name.dev>":
          currentEnv = AppEnvironmentType.dev;
          break;
        default:
          currentEnv = AppEnvironmentType.prod;
      }
    });
  }

}

Step 2:
Then in your main.dart, call await AppEnvironment.init() like this:

void main() async{
  await AppEnvironment.init();
}

After generating your env classes,
Step 3:
In order to avoid calling Env several times in different places of your project, just create a file called config.dart that contains functions that access each of the env variables:

class AppConfig {

    static String googleAPIKey() {
         //you can use a switch or if-else you have more than 2 environments.
          return AppEnvironment.currentEnv == AppEnvironmentType.dev ? Env.googleAPIKeyDev : Env.googleAPIKeyProd;
    }

}

This is our temporary solution but I hope it helps!

@ThexXTURBOXx
Copy link
Contributor

@skibble-app Why not just use kDebugMode instead?

@skibble-app
Copy link

@ThexXTURBOXx Yes, I tried that but there is a problem. You can have a dev, stg and prod flavors and they each would come in 3 variants (debug, profile & release).
dev - debug, profile, release
stg - debug, profile, release
prod - debug, profile, release

So if you ran the prod or stg flavor in debug mode, kDebugMode will always return true. The distinct feature in all the flavors is the bundle id.

@wer-mathurin
Copy link

@petercinibulk
Nice package BTW.

If we could have a CLI for envied this will be very generic and we would be able to use in any situations.

Use case #1 - Development mode (local machine)

you can use the usual suspect : dart run build_runner build --delete-conflicting-outputs

Use case #2 - Build with CI/CD pipeline

I think this will work whatever you are using flavors or not.

Can can have a task before calling the flutter build:

  1. Create an .env file from secrets
  2. Call the envied_generator (we only want the env.g.dart to be generated...not all the generated code)
  3. Delete the .env file
  4. Now you can start the usual build process for Flutter

@techouse techouse added duplicate This issue or pull request already exists question Further information is requested labels Aug 26, 2023
@techouse
Copy link
Collaborator

This is covered in an example.

Duplicate of #44

@Isuru-Nanayakkara
Copy link
Author

@techouse I'm not sure using kDebugMode actually solves this problem. Like @skibble-app has mentioned in the comment, the debug mode is simply a mode that the app is run, right? Technically you can run an app pointing to a prod environment in both debug and release modes. Therefore we cannot rely on that to determine the environment.

Also there can be more than 2 environments than dev and prod. The kDebugMode doesn't really fix that issue either.

@techouse
Copy link
Collaborator

techouse commented Aug 26, 2023

I'm not sure using kDebugMode actually solves this problem

@Isuru-Nanayakkara depends on your situation. I just gave an example of using an out-of-the-box deterministic way of differentiating configs. The example should NOT be verbatim copied into your app. It's up to the developer to find the best way of determining which config to load. I even pointed that out on line 6.

I suggest you use Flutter flavors to solve this. Using flavors you can pass the corresponding flavor to the flutter build command using the --flavor switch

flutter build ipa --release --flavor staging

and then parse that in your app into an enum value.

@Isuru-Nanayakkara
Copy link
Author

Yes, I'm already using flavors/schemes for environments. The problem I was facing was, as per the package's design, I'd have to generate multiple envied files. I was wondering if there was a way to extend the package to avoid that.

@techouse
Copy link
Collaborator

I'd have to generate multiple envied files.

Isn't separation of different env variables into multiple env files a good thing?

I'm quite opposed to the idea of having a huge switch statement in a single file.

@Isuru-Nanayakkara
Copy link
Author

Isuru-Nanayakkara commented Aug 26, 2023

Totally agreed on both counts. My intention is to avoid switch statements too. Please see my current way of handling environment values in this repo.

  1. I have defined 3 flavors in the Android gradle file. Each flavor has a different bundle identifier.
flavorDimensions "flavors"
    productFlavors {
        dev {
            dimension "flavors"
            resValue "string", "app_name", "(Dev) SuperCoolApp"
            applicationIdSuffix ".dev"
        }
        qa {
            dimension "flavors"
            resValue "string", "app_name", "(QA) SuperCoolApp"
            applicationIdSuffix ".qa"
        }
        prod {
            dimension "flavors"
            resValue "string", "app_name", "SuperCoolApp"
        }
    }
  1. I have created 3 separate env files to store environment values. Normally I don't commit these to git.

dev.env

API_URL=https://dev.supercoolapp.com
// ...
  1. I have a Dart singleton class called EnvironmentValues where I extract the environment variable values from the env files and return them.
import 'package:flutter_dotenv/flutter_dotenv.dart';

enum Environment {
  dev,
  qa,
  prod,
}

class EnvironmentValues {
  static final EnvironmentValues _instance = EnvironmentValues._();
  static Environment? _environment;

  EnvironmentValues._();

  static EnvironmentValues get instance => _instance;

  static Future load({required env, required String fileName}) async {
    _environment = env;
    await dotenv.load(fileName: fileName);
  }

  static Environment get environment {
    return _environment!;
  }

  static String get apiBaseURL {
    return dotenv.get('API_URL');
  }
}
  1. I have separate main.dart files (main_dev.dart, main_qa.dart etc) for each flavor. This is where I feed the env file to the EnvironmentValues class.
void main() async {
  await EnvironmentValues.load(
    env: Environment.dev,
    fileName: 'env/dev.env',
  );

  runApp(const App());
}

This works great. I can switch between flavors and everything would just work. No need for conditional statements anywhere in the code to check for the current environment since it's handled at launch at the main entry point.

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(EnvironmentValues.apiBaseURL),
      ),
    );
  }

The only downside is the security aspect. Hence I looked to use envied. Essentially I was trying to replace my EnvironmentValues class with the envied's generated file. Problem was with envied, you need to generate the file each time I have to run the app in different flavors if you're using a single generated file.

@techouse
Copy link
Collaborator

The only downside is the security aspect.

You mean as in committing the env vars to Github?

Problem was with envied, you need to generate the file each time I have to run the app in different flavors if you're using a single generated file.

I use multiple files personally. I generate them all at once and based on the run/compilation I need I load them at compile time. I do not track them in git.

@Isuru-Nanayakkara
Copy link
Author

The only downside is the security aspect.

You mean as in committing the env vars to Github?

No no, I mean obfuscating the values. I have read somewhere that since env files need to be added as assets in the Flutter bundle, they can be extracted. I wanted to try envied since it supports obfuscation.

Problem was with envied, you need to generate the file each time I have to run the app in different flavors if you're using a single generated file.

I use multiple files personally. I generate them all at once and based on the run/compilation I need I load them at compile time. I do not track them in git.

How do you use it in your code? Since you're creating multiple files, I'm assuming you'll have separate files like EnvDev, EnvProd etc? So when you need to access a value like so Env.key1, won't you need to use conditional statements in that case?

@techouse
Copy link
Collaborator

techouse commented Aug 26, 2023

No no, I mean obfuscating the values.

Yeah, even though that's nice it can still be decrypted / deobfuscated. Treat any value you put into a client app as public, just as you would with JavaScript. It's public and should NOT be of a sensitive nature.

So when you need to access a value like so Env.key1, won't you need to use conditional statements in that case?

Pretty much the same way as I've done it in the example

abstract interface class AppEnv {
  factory AppEnv() => _instance;

  static final AppEnv _instance = kDebugMode
      ? DebugEnv()
      : kProfileMode
          ? ProfileEnv()
          : kBuildFlavor == Flavor.qa
              ? QaEnv()
              : ReleaseEnv();
}

@a1tem
Copy link

a1tem commented Aug 9, 2024

The solution for flavors is described here

@chillbrodev
Copy link

The solution for flavors is described here

That is a solution but I would not say it is THE solution. Ideally the app should use 1 .env file and it is up to the developer to properly update the .env file with the correct values for each flavor. I describe usage above #16 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
duplicate This issue or pull request already exists question Further information is requested
Projects
None yet
Development

No branches or pull requests