Flutter Mobile Development · درس

نكهات البناء وإعدادات البيئة

عرّف نكهات dev وstaging وprod مع أصول خاصة بكل بيئة وإعدادات Dart-define

الدرس 1 من 413 خطوة

نكهات البناء وإعدادات البيئة درس مجاني في Flutter Mobile Development على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Flutter Mobile Development، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.

بعض أجزاء هذا الدرس لم تُترجم بعد وتظهر باللغة الإنجليزية.

Why Build Flavors Matter

A production Flutter app rarely talks to a single backend. You need a dev build pointing at a local or staging API, a staging build for QA, and a hardened prod build for the store.

  • Flavors are named build variants that can differ in app id, icon, name, and signing.
  • Environment config is the data each flavor injects: base URLs, feature flags, API keys.

The goal: install dev, staging, and prod side-by-side on one device, each fully isolated and impossible to confuse.

The Two Layers: Native Flavors + Dart-Define

A clean setup separates two concerns:

  • Native flavors (Android productFlavors, iOS schemes/xcconfig) control the application identity: bundle id, display name, icon.
  • Dart-define values control runtime configuration your Dart code reads: API URL, environment name, log level.

Native flavors let dev, staging, and prod coexist on the device with distinct ids like com.acme.app.dev. Dart-define keeps secrets and URLs out of source control and version-pinned at compile time.

Defining a Type-Safe Environment Enum

Start in Dart with a single source of truth for which environments exist. An enum prevents typos and enables exhaustive switch handling.

This snippet is plain Dart with no Flutter dependency, so it runs anywhere.

enum Environment { dev, staging, prod }

String labelFor(Environment env) {
  switch (env) {
    case Environment.dev:
      return 'Development';
    case Environment.staging:
      return 'Staging';
    case Environment.prod:
      return 'Production';
  }
}

void main() {
  for (final env in Environment.values) {
    print('${env.name} -> ${labelFor(env)}');
  }
}

Reading Dart-Define at Compile Time

Flutter exposes compile-time constants through String.fromEnvironment, bool.fromEnvironment, and int.fromEnvironment. You pass them with --dart-define at build time.

  • Values are baked into the binary; they are NOT read at runtime from the device.
  • Always provide a defaultValue so a missing define fails predictably.

Because these are const, they can be evaluated even in const contexts.

const String apiUrl = String.fromEnvironment(
  'API_URL',
  defaultValue: 'http://localhost:8080',
);

const String envName = String.fromEnvironment(
  'ENV',
  defaultValue: 'dev',
);

const bool analyticsEnabled = bool.fromEnvironment(
  'ANALYTICS',
  defaultValue: false,
);

void main() {
  print('env=$envName url=$apiUrl analytics=$analyticsEnabled');
}

Building an AppConfig Object

Scatter String.fromEnvironment calls across your codebase and you lose control. Centralize them in one immutable AppConfig that you resolve once and pass down.

This keeps every flavor difference in one auditable place and makes testing trivial: you just construct an AppConfig with the values you want.

class AppConfig {
  final String envName;
  final String apiUrl;
  final bool analyticsEnabled;

  const AppConfig({
    required this.envName,
    required this.apiUrl,
    required this.analyticsEnabled,
  });

  factory AppConfig.fromEnvironment() {
    return const AppConfig(
      envName: String.fromEnvironment('ENV', defaultValue: 'dev'),
      apiUrl: String.fromEnvironment('API_URL',
          defaultValue: 'http://localhost:8080'),
      analyticsEnabled:
          bool.fromEnvironment('ANALYTICS', defaultValue: false),
    );
  }

  bool get isProd => envName == 'prod';
}

void main() {
  final config = AppConfig.fromEnvironment();
  print('Running in ${config.envName} -> ${config.apiUrl}');
}

Passing Dart-Define on the Command Line

You inject configuration when you run or build. Each --dart-define sets one key. Pair it with the native flavor via --flavor.

  • --flavor staging selects the native variant (id, icon, name).
  • --dart-define feeds your AppConfig.

Typing these every time is error-prone, so teams capture them in scripts or files (next scene).

flutter run \
  --flavor staging \
  --target lib/main.dart \
  --dart-define=ENV=staging \
  --dart-define=API_URL=https://staging.api.acme.com \
  --dart-define=ANALYTICS=true

flutter build apk \
  --release \
  --flavor prod \
  --dart-define=ENV=prod \
  --dart-define=API_URL=https://api.acme.com \
  --dart-define=ANALYTICS=true

Dart-Define-From-File for Cleaner CI

Long --dart-define chains are brittle. Flutter supports --dart-define-from-file, which reads a JSON (or .env-style) file of key/value pairs.

  • Keep one file per environment: config/dev.json, config/staging.json, config/prod.json.
  • Commit non-secret files; inject secret ones in CI from a secure store.

Example config/prod.json and its invocation are shown. The keys map one-to-one to your fromEnvironment lookups.

// config/prod.json
{
  "ENV": "prod",
  "API_URL": "https://api.acme.com",
  "ANALYTICS": true
}

// Invocation:
// flutter build appbundle --release \
//   --flavor prod \
//   --dart-define-from-file=config/prod.json

Android: productFlavors

On Android, declare flavors in android/app/build.gradle. Each flavor overrides the application id suffix and name so builds install side-by-side.

  • applicationIdSuffix appends to the base id (e.g. com.acme.app.dev).
  • resValue overrides the launcher label per flavor.

A flavorDimensions entry is required before listing flavors.

android {
    flavorDimensions "env"
    productFlavors {
        dev {
            dimension "env"
            applicationIdSuffix ".dev"
            resValue "string", "app_name", "Acme Dev"
        }
        staging {
            dimension "env"
            applicationIdSuffix ".staging"
            resValue "string", "app_name", "Acme Staging"
        }
        prod {
            dimension "env"
            resValue "string", "app_name", "Acme"
        }
    }
}

iOS: Schemes and xcconfig

On iOS, flavors map to Xcode schemes backed by build configurations and .xcconfig files. Each scheme sets a distinct PRODUCT_BUNDLE_IDENTIFIER and display name.

  • Create configs like Debug-dev, Release-prod, etc.
  • An .xcconfig per environment overrides the bundle id and DISPLAY_NAME, read in Info.plist via $(DISPLAY_NAME).

Flutter matches --flavor prod to the Xcode scheme named prod.

// ios/Flutter/staging.xcconfig
#include "Generated.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = com.acme.app.staging
DISPLAY_NAME = Acme Staging

// In Info.plist:
// <key>CFBundleDisplayName</key>
// <string>$(DISPLAY_NAME)</string>

Per-Environment Assets

Flavors often need different assets: a colored DEV banner, a staging app icon, a different Firebase config.

  • Organize assets in folders like assets/dev/, assets/prod/ and select the path at runtime from your AppConfig.
  • For native icons, Android resolves src/dev/res automatically; iOS uses per-config asset catalogs.
  • Place flavor-specific google-services.json under android/app/src/<flavor>/.

The Dart side simply derives the asset path from the active environment.

class AssetPaths {
  final String envName;
  const AssetPaths(this.envName);

  String get logo => 'assets/$envName/logo.png';
  String get configBanner =>
      envName == 'prod' ? '' : 'assets/$envName/banner.png';
}

void main() {
  for (final env in ['dev', 'staging', 'prod']) {
    final paths = AssetPaths(env);
    print('$env logo: ${paths.logo}');
  }
}

Wiring AppConfig into main()

Resolve the config once at startup and make it available to the widget tree (via an InheritedWidget, provider, or a service locator). Avoid reading fromEnvironment deep in your widgets.

  • Build the config before runApp.
  • Show a visible environment banner for non-prod builds to prevent QA confusion.

The snippet below is Flutter framework code, so it is not standalone-runnable, but it shows the canonical entry point.

import 'package:flutter/material.dart';

void main() {
  final config = AppConfig.fromEnvironment();
  runApp(MyApp(config: config));
}

class MyApp extends StatelessWidget {
  final AppConfig config;
  const MyApp({super.key, required this.config});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Acme (${config.envName})',
      debugShowCheckedModeBanner: !config.isProd,
      home: const Scaffold(body: Center(child: Text('Home'))),
    );
  }
}

Quick Check: Where Config Lives

You configured dev, staging, and prod flavors. Test your understanding of how Dart-define values behave.

Recap

You built a complete flavor + environment configuration strategy:

  • Two layers: native flavors (Android productFlavors, iOS schemes/xcconfig) set app identity; dart-define sets runtime config.
  • Type safety: a single Environment enum and an immutable AppConfig resolved once via AppConfig.fromEnvironment().
  • Injection: pass --flavor with --dart-define, or scale cleanly with --dart-define-from-file=config/<env>.json.
  • Assets: per-environment folders and native resource overrides for icons, banners, and Firebase configs.
  • Key insight: dart-define is compile-time and baked into the binary, so changing config always means a rebuild.

The payoff: dev, staging, and prod install side-by-side, fully isolated, and impossible to confuse.

البدء مجانًا

تعلم Dart مع معلم ذكاء اصطناعي — مجانًا

اكتب وقم بتشغيل أكوادك الفعلية في المتصفح، واحصل على مساعدة فورية من معلم ذكاء اصطناعي متاح 24/7، واستمر من حيث توقفت على الويب أو في التطبيق.

الدورات
22
الدروس
88

الأسئلة الشائعة

هل درس «نكهات البناء وإعدادات البيئة» مجاني؟

نعم — نص درس «نكهات البناء وإعدادات البيئة» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Flutter Mobile Development، انتقل إلى CoddyKit PRO. تتضمن دورة Flutter Mobile Development 4 دروس في المجموع.

ماذا ستتعلم في «نكهات البناء وإعدادات البيئة»؟

عرّف نكهات dev وstaging وprod مع أصول خاصة بكل بيئة وإعدادات Dart-define تتمرن على Flutter Mobile Development مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Flutter Mobile Development؟

لا تُشترط خبرة سابقة. Flutter Mobile Development على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «نكهات البناء وإعدادات البيئة»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Flutter Mobile Development هذا؟

نعم. كل درس في Flutter Mobile Development يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. نكهات البناء وإعدادات البيئة
  2. مسارات مؤتمتة باستخدام Fastlane وGitHub Actions
  3. الإبلاغ عن الأعطال وآثار المكدس المرمّزة بالرموز
  4. الإعداد عن بُعد وأعلام الميزات والإطلاقات المرحلية
← العودة إلى Flutter Mobile Development