Skip to content

Commit 3d9db32

Browse files
Dotenv setup with environment selector & interceptors (#108)
--------- Co-authored-by: aguskoll <aguskoll@gmail.com>
1 parent 06e0053 commit 3d9db32

28 files changed

+265
-56
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,6 @@ app.*.symbols
121121
!**/ios/**/default.perspectivev3
122122
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
123123
!/dev/ci/**/Gemfile.lock
124-
/app/env/
125-
126124

127125
/modules/common/.flutter-plugins
128126
/modules/common/.flutter-plugins-dependencies

app/.gitignore

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,8 @@ app.*.symbols
125125
/env*.json
126126

127127
# Environment and configuration files (contain sensitive data)
128-
env/.env
129-
env/.settings
130-
env/env_*.json
128+
../env/.env
129+
../env/env_*.json
131130
.env
132131
.env.*
133132
*.env
@@ -141,4 +140,4 @@ firebase-debug.log
141140

142141
# Local development files
143142
local.properties
144-
*.properties
143+
*.properties

app/env/.env.example

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# The .env file MUST be in .gitignore
2+
# Delete this file after creating .env
3+
#
4+
# Each flavor (dev/qa/prod) uses the same .env file but reads
5+
# different variables based on the suffix:
6+
# - Development (main_dev.dart) → reads *_DEV variables
7+
# - QA/Staging (main_qa.dart) → reads *_QA variables
8+
# - Production (main.dart) → reads *_PROD variables
9+
10+
API_URL_DEV=https://your-api-url-dev.com
11+
API_URL_QA=https://your-api-url-qa.com
12+
API_URL_PROD=https://your-api-url-prod.com
13+

app/lib/main/env/env_config.dart

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import 'package:domain/env/env_config.dart';
2+
13
enum Flavor { dev, qa, prod }
24

35
class FlavorValues {
4-
final String baseUrl;
56

6-
FlavorValues({required this.baseUrl});
7+
FlavorValues();
78
}
89

910
class FlavorConfig {
@@ -22,6 +23,17 @@ class FlavorConfig {
2223
flavor.toString(),
2324
values,
2425
);
26+
switch(flavor) {
27+
case Flavor.dev:
28+
EnvConfig.env = EnvConfig.kDevEnv;
29+
break;
30+
case Flavor.qa:
31+
EnvConfig.env = EnvConfig.kQaEnv;
32+
break;
33+
case Flavor.prod:
34+
EnvConfig.env = EnvConfig.kProdEnv;
35+
break;
36+
}
2537
return _instance!;
2638
}
2739

@@ -34,4 +46,26 @@ class FlavorConfig {
3446
static bool isDevelopment() => instance.flavor == Flavor.dev;
3547

3648
static bool isQA() => instance.flavor == Flavor.qa;
49+
50+
/// Returns the environment file path (single .env file for all flavors)
51+
///
52+
/// SETUP INSTRUCTIONS:
53+
/// 1. Navigate to app/env/ directory
54+
/// 2. Create a .env file (must be in .gitignore):
55+
/// cp .env.example .env
56+
/// 3. Add your environment variables with flavor suffixes:
57+
/// API_URL_DEV=https://dev-api.example.com
58+
/// API_URL_QA=https://qa-api.example.com
59+
/// API_URL_PROD=https://api.example.com
60+
/// 4. The active flavor determines which suffix is used (_DEV, _QA, _PROD)
61+
/// 5. Access variables in domain/lib/env/env_config.dart using:
62+
/// static String get apiUrl => dotenv.env['API_URL_$env']?.toString() ?? '';
63+
/// (The $env automatically appends DEV, QA, or PROD based on active flavor)
64+
/// 6. Go to pubspec.yaml and add the following:
65+
/// assets:
66+
/// - env/.env
67+
68+
static String getEnvFilePath() {
69+
return 'env/.env.example';
70+
}
3771
}

app/lib/main/env/main.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import 'package:flutter/material.dart';
22
import 'package:app/main/env/env_config.dart';
33
import 'package:app/main/init.dart';
4-
import 'package:domain/env.dart';
54

65
void main() async {
76
WidgetsFlutterBinding.ensureInitialized();
87
FlavorConfig(
98
flavor: Flavor.prod,
10-
values: FlavorValues(baseUrl: EnvConfig.apiUrl),
9+
values: FlavorValues(),
1110
);
1211
//Add your firebase configuration here
1312
/*await Firebase.initializeApp(

app/lib/main/env/main_dev.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ void main() async {
66
WidgetsFlutterBinding.ensureInitialized();
77
FlavorConfig(
88
flavor: Flavor.dev,
9-
values: FlavorValues(baseUrl: "https://demo_dev/web_api.json"),
9+
values: FlavorValues(),
1010
);
1111
//Add your firebase configuration here
1212
/*await Firebase.initializeApp(

app/lib/main/env/main_qa.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ void main() async {
66
WidgetsFlutterBinding.ensureInitialized();
77
FlavorConfig(
88
flavor: Flavor.qa,
9-
values: FlavorValues(baseUrl: "https://demo_qa/web_api.json"),
9+
values: FlavorValues(),
1010
);
1111
//Add your firebase configuration here
1212
/*await Firebase.initializeApp(

app/lib/main/init.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import 'package:domain/init.dart';
55
import 'package:example_domain/init.dart';
66
import 'package:example_data/init.dart';
77
import 'package:flutter/material.dart';
8+
import 'package:flutter_dotenv/flutter_dotenv.dart';
89
import 'package:get_it/get_it.dart';
910
import 'package:url_strategy/url_strategy.dart';
1011

12+
import 'env/env_config.dart';
13+
1114
void init() async {
1215
WidgetsFlutterBinding.ensureInitialized();
1316
await initialize();
@@ -18,11 +21,12 @@ void init() async {
1821
final getIt = GetIt.instance;
1922

2023
Future<void> initialize() async {
24+
await dotenv.load(fileName: FlavorConfig.getEnvFilePath());
2125
await CommonInit.initialize(getIt);
2226
await DataInit.initialize(getIt);
2327
await DomainInit.initialize(getIt);
2428

2529
// Example Module init
26-
await ExampleDomainInit.initialize(getIt);
2730
await ExampleDataInit.initialize(getIt);
31+
await ExampleDomainInit.initialize(getIt);
2832
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'package:app/presentation/resources/resources.dart';
2+
import 'package:app/presentation/themes/local_theme.dart';
3+
import 'package:domain/env/env_config.dart';
4+
import 'package:domain/services/environment_service.dart';
5+
import 'package:flutter/material.dart';
6+
7+
import '../../../main/init.dart';
8+
9+
class EnvironmentSelector extends StatelessWidget {
10+
EnvironmentSelector({
11+
super.key,
12+
});
13+
14+
final EnvironmentService environmentService = getIt<EnvironmentService>();
15+
16+
DropdownMenuItem<String> _item(
17+
String value, String label, TextStyle textStyle) =>
18+
DropdownMenuItem<String>(
19+
value: value,
20+
child: Padding(
21+
padding: EdgeInsets.symmetric(horizontal: spacing.xs),
22+
child: Text(label, style: textStyle),
23+
),
24+
);
25+
26+
@override
27+
Widget build(BuildContext context) {
28+
final textStyle =
29+
Theme.of(context).textTheme.bodyLarge?.copyWith(color: Theme.of(context).colorScheme.primary.v0);
30+
31+
final items = <DropdownMenuItem<String>>[
32+
_item(EnvConfig.kDevEnv, 'Development', textStyle!),
33+
_item(EnvConfig.kQaEnv, 'QA', textStyle),
34+
_item(EnvConfig.kProdEnv, 'Production', textStyle),
35+
];
36+
37+
return DropdownButtonFormField<String>(
38+
initialValue: EnvConfig.env,
39+
style: textStyle,
40+
decoration: InputDecoration(
41+
labelText: 'Environment',
42+
border: const OutlineInputBorder(),
43+
prefixIcon: const Icon(Icons.settings),
44+
labelStyle: textStyle,
45+
),
46+
items: items,
47+
onChanged: (value) => environmentService.setEnvironment(value!),
48+
);
49+
}
50+
}

app/lib/presentation/ui/pages/login/login_page.dart

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import 'package:app/main/init.dart';
2+
import 'package:app/presentation/resources/resources.dart';
23
import 'package:app/presentation/themes/app_themes.dart';
4+
import 'package:app/presentation/ui/custom/app_theme_switch.dart';
5+
import 'package:app/presentation/ui/custom/loading_screen.dart';
36
import 'package:common/core/resource.dart';
47
import 'package:domain/bloc/auth/auth_cubit.dart';
58
import 'package:domain/services/auth_service.dart';
9+
import 'package:flutter/foundation.dart';
610
import 'package:flutter/material.dart';
7-
import 'package:app/presentation/ui/custom/app_theme_switch.dart';
8-
import 'package:app/presentation/ui/custom/loading_screen.dart';
911
import 'package:flutter_bloc/flutter_bloc.dart';
1012

13+
import '../../custom/environment_selector.dart';
14+
1115
class LoginPage extends StatelessWidget {
1216
AuthService get _authService => getIt();
1317

@@ -21,12 +25,12 @@ class LoginPage extends StatelessWidget {
2125
appBar: AppBar(),
2226
backgroundColor: context.theme.colorScheme.surface,
2327
body: Padding(
24-
padding: const EdgeInsets.all(16),
28+
padding: EdgeInsets.all(spacing.m),
2529
child: Column(
2630
mainAxisAlignment: MainAxisAlignment.center,
2731
children: [
2832
const AppThemeSwitch(),
29-
const SizedBox(height: 16),
33+
SizedBox(height: spacing.m),
3034
SizedBox(
3135
width: double.maxFinite,
3236
child: ElevatedButton(
@@ -39,6 +43,10 @@ class LoginPage extends StatelessWidget {
3943
},
4044
),
4145
),
46+
SizedBox(height: spacing.xxxl),
47+
if (kDebugMode) ...[
48+
EnvironmentSelector(),
49+
],
4250
],
4351
),
4452
),

0 commit comments

Comments
 (0)