Skip to content

Commit 429b433

Browse files
committed
refactor(config): enhance AppConfig with Firebase options and robust validation
- Add Firebase config variables and update existing ones - Implement comprehensive validation for all environment variables - Refactor main() to use manual Firebase initialization with AppConfig options - Improve documentation and error handling for better developer experience
1 parent b28ec83 commit 429b433

File tree

2 files changed

+177
-51
lines changed

2 files changed

+177
-51
lines changed

lib/app/config/app_config.dart

Lines changed: 158 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,186 @@
11
import 'package:flutter_news_app_mobile_client_full_source_code/app/config/app_environment.dart';
22

3-
/// A class to hold all environment-specific configurations.
3+
/// {@template app_config}
4+
/// A centralized configuration class that provides all necessary environment-specific
5+
/// variables for the application.
46
///
5-
/// This class is instantiated in `main.dart` based on the compile-time
6-
/// environment variable. It provides a type-safe way to access
7-
/// environment-specific values like API base URLs.
7+
/// This class uses factory constructors (`.production()`, `.development()`, `.demo()`)
8+
/// to create an immutable configuration object based on the current build
9+
/// environment, which is determined at compile time. All required values are
10+
/// sourced from `--dart-define` variables, ensuring a clean separation of
11+
/// configuration from code and preventing accidental use of development keys
12+
/// in production builds.
13+
///
14+
/// It includes robust validation to fail fast if required variables are missing,
15+
/// providing clear error messages to the developer.
16+
/// {@endtemplate}
817
class AppConfig {
9-
/// Creates a new [AppConfig].
18+
/// {@macro app_config}
1019
AppConfig({
1120
required this.environment,
1221
required this.baseUrl,
1322
required this.oneSignalAppId,
23+
required this.firebaseApiKey,
24+
required this.firebaseAppId,
25+
required this.firebaseMessagingSenderId,
26+
required this.firebaseProjectId,
27+
required this.firebaseStorageBucket,
1428
// Add other environment-specific configs here (e.g., analytics keys)
1529
});
1630

17-
/// A factory constructor for the production environment.
31+
/// Creates an [AppConfig] for the **production** environment.
1832
///
19-
/// Reads the `BASE_URL` from a compile-time variable. Throws an exception
20-
/// if the URL is not provided, ensuring a production build cannot proceed
21-
/// with a missing configuration. This is a critical safety check.
33+
/// This factory reads all values directly from `String.fromEnvironment`.
34+
/// It does **not** provide default values. If any required variable is missing,
35+
/// the `_validateConfiguration` method will throw a [FormatException],
36+
/// causing the build to fail. This is a critical safety measure.
2237
factory AppConfig.production() {
23-
const baseUrl = String.fromEnvironment('BASE_URL');
24-
const oneSignalAppId = String.fromEnvironment('ONE_SIGNAL_APP_ID');
25-
if (baseUrl.isEmpty) {
26-
// This check is crucial for production builds.
27-
throw const FormatException(
28-
'FATAL: The BASE_URL compile-time variable was not provided for this '
29-
'production build. Ensure the build command includes '
30-
'--dart-define=BASE_URL=https://your.api.com',
31-
);
32-
}
33-
if (oneSignalAppId.isEmpty) {
34-
// This check is crucial for production builds.
35-
throw const FormatException(
36-
'FATAL: The ONE_SIGNAL_APP_ID compile-time variable was not provided '
37-
'for this production build. Ensure the build command includes '
38-
'--dart-define=ONE_SIGNAL_APP_ID=your-id',
39-
);
40-
}
41-
return AppConfig(
38+
final config = AppConfig(
4239
environment: AppEnvironment.production,
43-
baseUrl: baseUrl,
44-
oneSignalAppId: oneSignalAppId,
40+
baseUrl: const String.fromEnvironment('BASE_URL'),
41+
oneSignalAppId: const String.fromEnvironment('ONE_SIGNAL_APP_ID'),
42+
firebaseApiKey: const String.fromEnvironment('FIREBASE_API_KEY'),
43+
firebaseAppId: const String.fromEnvironment('FIREBASE_APP_ID'),
44+
firebaseMessagingSenderId: const String.fromEnvironment(
45+
'FIREBASE_MESSAGING_SENDER_ID',
46+
),
47+
firebaseProjectId: const String.fromEnvironment('FIREBASE_PROJECT_ID'),
48+
firebaseStorageBucket: const String.fromEnvironment(
49+
'FIREBASE_STORAGE_BUCKET',
50+
),
4551
);
52+
_validateConfiguration(config);
53+
return config;
4654
}
4755

48-
/// A factory constructor for the demo environment.
56+
/// Creates an [AppConfig] for the **demo** environment.
57+
///
58+
/// This factory uses hardcoded, non-functional placeholder values. It is
59+
/// designed for running the app in a completely offline, in-memory mode
60+
/// where no backend services are required. The Firebase values are validly
61+
// formatted dummies required to satisfy Firebase initialization.
4962
factory AppConfig.demo() => AppConfig(
5063
environment: AppEnvironment.demo,
5164
baseUrl: '', // No API access needed for in-memory demo
5265
oneSignalAppId: 'YOUR_DEMO_ONESIGNAL_APP_ID', // Placeholder for demo
66+
// Dummy Firebase values for demo mode.
67+
// These are required to initialize Firebase but won't be used for
68+
// actual backend communication in demo mode.
69+
firebaseApiKey: 'demo-key',
70+
firebaseAppId: '1:000000000000:android:0000000000000000000000',
71+
firebaseMessagingSenderId: '000000000000',
72+
firebaseProjectId: 'demo-project',
73+
firebaseStorageBucket: '',
5374
);
5475

55-
/// A factory constructor for the development environment.
56-
factory AppConfig.development() => AppConfig(
57-
environment: AppEnvironment.development,
58-
baseUrl: const String.fromEnvironment(
59-
'BASE_URL',
60-
defaultValue: 'http://localhost:8080',
61-
),
62-
oneSignalAppId: const String.fromEnvironment(
63-
'ONE_SIGNAL_APP_ID',
64-
defaultValue: 'YOUR_DEV_ONESIGNAL_APP_ID', // Placeholder for dev
65-
),
66-
);
76+
/// Creates an [AppConfig] for the **development** environment.
77+
///
78+
/// This factory reads values from `String.fromEnvironment` but provides
79+
/// `defaultValue`s for convenience during local development. If a developer
80+
/// runs the app without providing a specific `--dart-define` variable, the
81+
/// validation will catch the placeholder value (e.g., 'YOUR_DEV_...'),
82+
/// and throw a helpful error, guiding them to configure their environment.
83+
factory AppConfig.development() {
84+
final config = AppConfig(
85+
environment: AppEnvironment.development,
86+
baseUrl: const String.fromEnvironment(
87+
'BASE_URL',
88+
defaultValue: 'http://localhost:8080',
89+
),
90+
oneSignalAppId: const String.fromEnvironment(
91+
'ONE_SIGNAL_APP_ID',
92+
defaultValue: 'YOUR_DEV_ONESIGNAL_APP_ID',
93+
),
94+
firebaseApiKey: const String.fromEnvironment(
95+
'FIREBASE_API_KEY',
96+
defaultValue: 'YOUR_DEV_FIREBASE_API_KEY',
97+
),
98+
firebaseAppId: const String.fromEnvironment(
99+
'FIREBASE_APP_ID',
100+
defaultValue: 'YOUR_DEV_FIREBASE_APP_ID',
101+
),
102+
firebaseMessagingSenderId: const String.fromEnvironment(
103+
'FIREBASE_MESSAGING_SENDER_ID',
104+
defaultValue: 'YOUR_DEV_FIREBASE_MESSAGING_SENDER_ID',
105+
),
106+
firebaseProjectId: const String.fromEnvironment(
107+
'FIREBASE_PROJECT_ID',
108+
defaultValue: 'YOUR_DEV_FIREBASE_PROJECT_ID',
109+
),
110+
firebaseStorageBucket: const String.fromEnvironment(
111+
'FIREBASE_STORAGE_BUCKET',
112+
defaultValue: 'YOUR_DEV_FIREBASE_STORAGE_BUCKET',
113+
),
114+
);
115+
_validateConfiguration(config);
116+
return config;
117+
}
67118

119+
/// The current build environment (e.g., production, development, demo).
68120
final AppEnvironment environment;
121+
122+
/// The base URL for the backend API.
69123
final String baseUrl;
124+
125+
/// The OneSignal App ID for push notifications.
70126
final String oneSignalAppId;
127+
128+
/// The API key for the Firebase project.
129+
final String firebaseApiKey;
130+
131+
/// The App ID for the Firebase app.
132+
final String firebaseAppId;
133+
134+
/// The Sender ID for Firebase Cloud Messaging.
135+
final String firebaseMessagingSenderId;
136+
137+
/// The Project ID for the Firebase project.
138+
final String firebaseProjectId;
139+
140+
/// The storage bucket for Firebase Storage.
141+
final String firebaseStorageBucket;
142+
143+
/// A private static method to validate the loaded configuration.
144+
///
145+
/// Throws a [FormatException] if any required environment variables are
146+
/// missing or still set to placeholder values. This ensures that both
147+
/// production and development builds fail fast with clear instructions
148+
/// if not configured correctly.
149+
///
150+
/// #### Validation Checks:
151+
/// - Ensures `BASE_URL` is not empty or a localhost URL in production.
152+
/// - Checks for any placeholder values (containing 'YOUR_') in critical keys,
153+
/// which indicates a misconfigured development environment.
154+
static void _validateConfiguration(AppConfig config) {
155+
final errors = <String>[];
156+
157+
if (config.baseUrl.isEmpty || config.baseUrl.contains('localhost')) {
158+
if (config.environment == AppEnvironment.production) {
159+
errors.add('- BASE_URL is not set for production.');
160+
}
161+
}
162+
163+
final placeholderKeys = [
164+
if (config.oneSignalAppId.contains('YOUR_')) 'ONE_SIGNAL_APP_ID',
165+
if (config.firebaseApiKey.contains('YOUR_')) 'FIREBASE_API_KEY',
166+
if (config.firebaseAppId.contains('YOUR_')) 'FIREBASE_APP_ID',
167+
if (config.firebaseMessagingSenderId.contains('YOUR_'))
168+
'FIREBASE_MESSAGING_SENDER_ID',
169+
if (config.firebaseProjectId.contains('YOUR_')) 'FIREBASE_PROJECT_ID',
170+
];
171+
172+
if (placeholderKeys.isNotEmpty) {
173+
errors.add(
174+
'- The following keys have placeholder values: ${placeholderKeys.join(', ')}.',
175+
);
176+
}
177+
178+
if (errors.isNotEmpty) {
179+
throw FormatException(
180+
'FATAL: Invalid app configuration for ${config.environment.name} environment.\n'
181+
'Please provide the required --dart-define values.\n'
182+
'${errors.join('\n')}',
183+
);
184+
}
185+
}
71186
}

lib/main.dart

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,31 @@ const appEnvironment = String.fromEnvironment('APP_ENVIRONMENT') == 'production'
1515
? AppEnvironment.development
1616
: AppEnvironment.demo);
1717

18-
void main() async {
19-
// Ensure Flutter widgets are initialized before any Firebase operations.
20-
WidgetsFlutterBinding.ensureInitialized();
21-
// Initialize Firebase services only on non-web platforms, as it's used
22-
// for push notifications which are not supported in the web demo.
23-
if (!kIsWeb) {
24-
await Firebase.initializeApp();
25-
}
18+
Future<void> main() async {
2619
final appConfig = switch (appEnvironment) {
2720
AppEnvironment.production => AppConfig.production(),
2821
AppEnvironment.development => AppConfig.development(),
2922
AppEnvironment.demo => AppConfig.demo(),
3023
};
3124

25+
// Ensure Flutter widgets are initialized before any Firebase operations.
26+
WidgetsFlutterBinding.ensureInitialized();
27+
28+
// Initialize Firebase services only on non-web platforms.
29+
// Firebase is manually initialized using options from AppConfig,
30+
// removing the dependency on the auto-generated firebase_options.dart file.
31+
if (!kIsWeb) {
32+
await Firebase.initializeApp(
33+
options: FirebaseOptions(
34+
apiKey: appConfig.firebaseApiKey,
35+
appId: appConfig.firebaseAppId,
36+
messagingSenderId: appConfig.firebaseMessagingSenderId,
37+
projectId: appConfig.firebaseProjectId,
38+
storageBucket: appConfig.firebaseStorageBucket,
39+
),
40+
);
41+
}
42+
3243
final appWidget = await bootstrap(appConfig, appEnvironment);
3344

3445
// The AppHotRestartWrapper is used at the root to enable a full application

0 commit comments

Comments
 (0)