إدارة إعدادات البيئة باستخدام ملفات .env ومتغيرات Dart
إدارة إعدادات البيئة باستخدام متغيرات Dart
كل تطبيق Flutter في مرحلة الإنتاج يحتاج إلى التعامل مع بيئات متعددة: التطوير، والاختبار (Staging)، والإنتاج. لكل بيئة عناوين API مختلفة، وأعلام الميزات، ومفاتيح التحليلات، وبيانات اعتماد الجهات الخارجية. ترميز هذه القيم مباشرةً في الشفرة المصدرية يخلق مشكلتين خطيرتين: تنتهي الأسرار في نظام التحكم بالإصدار، وتتطلب التبديل بين البيئات إجراء تغييرات في الكود.
يحل Flutter هذه المشكلة باستخدام --dart-define و--dart-define-from-file، اللذين يُحقنان القيم في وقت البناء كثوابت وقت الترجمة. هذه القيم لا تعيش أبداً في ملفات Dart المصدرية؛ بل تُمرر في سطر الأوامر أو تُحمل من ملف محلي مُدرج في .gitignore.
تمرير القيم باستخدام --dart-define
الأسلوب الأبسط هو تمرير أزواج المفتاح-القيمة مباشرةً في سطر أوامر flutter build أو flutter run:
تمرير المتغيرات في سطر الأوامر
# تشغيل التطوير
flutter run \
--dart-define=API_BASE_URL=https://dev-api.example.com \
--dart-define=ENABLE_ANALYTICS=false \
--dart-define=MAPS_API_KEY=dev_key_abc123
# بناء الإنتاج
flutter build apk --release \
--dart-define=API_BASE_URL=https://api.example.com \
--dart-define=ENABLE_ANALYTICS=true \
--dart-define=MAPS_API_KEY=prod_key_xyz789
داخل كود Dart الخاص بك، اقرأ هذه الثوابت باستخدام String.fromEnvironment()، أو bool.fromEnvironment()، أو int.fromEnvironment(). هذه مُنشئات ثوابت وقت الترجمة، لذا يجب تعيينها لمتغيرات const:
قراءة قيم dart-define في كود Dart
// lib/config/app_config.dart
class AppConfig {
// قيمة نصية — تعود إلى سلسلة فارغة إذا لم تُوفَّر
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080',
);
// علم منطقي — يعود إلى false بشكل افتراضي
static const bool enableAnalytics = bool.fromEnvironment(
'ENABLE_ANALYTICS',
defaultValue: false,
);
// قيمة عددية — تعود إلى 30 بشكل افتراضي
static const int requestTimeoutSeconds = int.fromEnvironment(
'REQUEST_TIMEOUT_SECONDS',
defaultValue: 30,
);
// مفتاح API لجهة خارجية — فارغ بشكل افتراضي
static const String mapsApiKey = String.fromEnvironment(
'MAPS_API_KEY',
defaultValue: '',
);
}
// الاستخدام في أي مكان في التطبيق:
// final dio = Dio(BaseOptions(baseUrl: AppConfig.apiBaseUrl));
// if (AppConfig.enableAnalytics) FirebaseAnalytics.instance.logEvent(...);
استخدام --dart-define-from-file
تمرير قيم كثيرة في سطر الأوامر يصبح أمراً صعباً. يدعم Flutter 3.7+ خيار --dart-define-from-file، الذي يقرأ جميع أزواج المفتاح-القيمة من ملف JSON. تنشئ ملف JSON واحداً لكل بيئة، وتضيفها جميعاً إلى .gitignore، وتُودع فقط ملف .env.example أو وثائق تصف المفاتيح المطلوبة.
ملف .env.dev.json (لا يُودع في git)
{
"API_BASE_URL": "https://dev-api.example.com",
"ENABLE_ANALYTICS": "false",
"MAPS_API_KEY": "dev_key_abc123",
"REQUEST_TIMEOUT_SECONDS": "30",
"SENTRY_DSN": ""
}
التشغيل باستخدام ملف المتغيرات
# التطوير
flutter run --dart-define-from-file=.env.dev.json
# الاختبار (Staging)
flutter build apk --dart-define-from-file=.env.staging.json
# إصدار الإنتاج
flutter build appbundle --release \
--dart-define-from-file=.env.prod.json
.env.example.json إلى نظام التحكم بالإصدار يسرد جميع المفاتيح المطلوبة بقيم نائبة. هذا يوثق ما يحتاج كل مطور إنشاءه محلياً دون الكشف عن الأسرار الحقيقية. أدرج تعليقاً في ملف README يشرح أن كل مطور يجب أن ينسخ هذا الملف ويملأ القيم الحقيقية.هيكلة فئة الإعدادات
تجمع فئة الإعدادات المُهيكلة جيداً جميع صلاحيات الوصول إلى البيئة في مكان واحد، مما يسهل التدقيق في القيم التي يجب أن توفرها البيئة واستبدال التطبيقات أثناء الاختبار:
فئة AppConfig الكاملة مع التحقق
// lib/config/app_config.dart
class AppConfig {
static const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:8080',
);
static const String sentryDsn = String.fromEnvironment('SENTRY_DSN');
static const bool enableAnalytics = bool.fromEnvironment(
'ENABLE_ANALYTICS',
defaultValue: false,
);
static const bool enableCrashReporting = bool.fromEnvironment(
'ENABLE_CRASH_REPORTING',
defaultValue: false,
);
static const String mapsApiKey = String.fromEnvironment('MAPS_API_KEY');
/// استدع هذه الدالة عند بدء التطبيق للفشل بسرعة إذا كانت القيم المطلوبة مفقودة.
static void validate() {
if (apiBaseUrl.isEmpty) {
throw StateError('API_BASE_URL must be set via --dart-define');
}
if (mapsApiKey.isEmpty) {
throw StateError('MAPS_API_KEY must be set via --dart-define');
}
}
}
// في main():
// void main() {
// AppConfig.validate();
// runApp(const MyApp());
// }
أفضل ممارسات .gitignore
أضف جميع ملفات البيئة الحقيقية إلى .gitignore لمنع إيداع الأسرار:
إدخالات .gitignore لملفات البيئة
# ملفات إعدادات البيئة (تحتوي على أسرار حقيقية)
.env.dev.json
.env.staging.json
.env.prod.json
# احتفظ بالملف المثال مُودعاً
# .env.example.json <-- لا تتجاهل هذا الملف
--dart-define في نص CI/CD مُودع في نظام التحكم بالإصدار. بدلاً من ذلك، احتفظ بالأسرار كمتغيرات بيئة في CI/CD (أسرار GitHub Actions، متغيرات GitLab CI، أسرار Bitrise) ووسّعها في وقت البناء: flutter build apk --dart-define=MAPS_API_KEY=$MAPS_API_KEY.الوصول من المنصات الأصيلة
متغيرات Dart متاحة أيضاً داخل ملفات إعداد المنصات الأصيلة. بالنسبة لـ Android (ملف build.gradle) وiOS (xcconfig)، ينقل Flutter قيم dart-define حتى تتمكن من تهيئة إعدادات خاصة بالمنصة كمفتاح Google Maps SDK في AndroidManifest.xml:
قراءة dart-define في android/app/build.gradle
// في android/app/build.gradle
def dartDefines = [:]
if (project.hasProperty('dart-defines')) {
project.property('dart-defines').split(',').each { entry ->
def pair = new String(entry.decodeBase64(), 'UTF-8').split('=')
if (pair.length == 2) dartDefines[pair[0]] = pair[1]
}
}
android {
defaultConfig {
manifestPlaceholders += [
mapsApiKey: dartDefines.getOrDefault('MAPS_API_KEY', '')
]
}
}
خلاصة
يُبقي استخدام --dart-define و--dart-define-from-file القيم الخاصة بالبيئة — عناوين URL لـ API، وأعلام الميزات، والمفاتيح السرية — خارج الشفرة المصدرية. تُحقن القيم في وقت البناء كثوابت وقت الترجمة، وتُقرأ في Dart باستخدام String.fromEnvironment()، ويمكن أن تنتشر أيضاً إلى ملفات بناء المنصات الأصيلة. أضف دائماً ملفات JSON البيئية الحقيقية إلى .gitignore، وأودع فقط قالب مثال، واحتفظ بالأسرار الإنتاجية في مخزن الأسرار في منصة CI/CD الخاصة بك.