Weather1

/// ===============================================================
/// File: app_colors.dart
/// Path: lib/core/theme/app_colors.dart
///
/// مسؤولية الملف:
/// يحتوي جميع ألوان التطبيق.
/// لا يحتوي أي منطق برمجي.
/// لا يحتوي أي Widgets.
/// ===============================================================

import 'package:flutter/material.dart';

/// ---------------------------------------------------------------
/// AppColors
///
/// هذا الكلاس عبارة عن Container للألوان فقط.
/// لا ننشئ منه Object.
/// لذلك أنشأنا Private Constructor.
/// ---------------------------------------------------------------
class AppColors {

  /// يمنع إنشاء Object.
  AppColors._();

  /// اللون الأساسي للتطبيق.
  ///
  /// سيستخدم في:
  /// - الأزرار
  /// - AppBar
  /// - Icons
  /// - Links
  static const Color primary = Color(0xff2196F3);

  /// اللون الثانوي.
  static const Color secondary = Color(0xff64B5F6);

  /// لون خلفية الوضع الفاتح.
  static const Color backgroundLight = Color(0xffF5F5F5);

  /// لون خلفية الوضع الداكن.
  static const Color backgroundDark = Color(0xff121212);

  /// لون النص في الوضع الفاتح.
  static const Color textLight = Colors.black87;

  /// لون النص في الوضع الداكن.
  static const Color textDark = Colors.white;
}
Dart
/// ============================================================================
/// File : app.dart
/// Path : lib/app/app.dart
/// ============================================================================
///
/// هذا الملف يمثل الجذر الحقيقي للتطبيق.
///
/// بينما main.dart مسؤول فقط عن تشغيل التطبيق،
/// فإن هذا الملف مسؤول عن إعداد التطبيق بالكامل.
///
/// مع نمو المشروع سيحتوي على:
///
/// ✅ MaterialApp
/// ✅ Theme
/// ✅ Localization
/// ✅ Router
/// ✅ Dependency Injection
/// ✅ Bloc Providers
/// ✅ Navigator
///
/// أي أن جميع الإعدادات العامة للتطبيق ستكون هنا.
///
/// ============================================================================

import 'package:flutter/material.dart';

/// استيراد الثيم الخاص بالتطبيق.
///
/// يحتوي على:
///
/// - Light Theme
/// - Dark Theme
///
/// ولا يحتوي على أي Widgets.
///
import '../core/theme/app_theme.dart';

/// ============================================================================
///
/// WeatherApp
///
/// ============================================================================
///
/// هذا هو Root Widget الحقيقي.
///
/// عند استدعاء:
///
/// runApp(const WeatherApp());
///
/// يبدأ Flutter ببناء التطبيق من هنا.
///
/// ============================================================================

class WeatherApp extends StatelessWidget {

  /// constructor
  ///
  /// super.key
  ///
  /// يسمح لـ Flutter بالتعرف على الـ Widget
  /// داخل Widget Tree وتحسين إعادة البناء.
  ///
  const WeatherApp({super.key});

  /// ==========================================================================
  ///
  /// build()
  ///
  /// ==========================================================================
  ///
  /// مسؤولة عن إنشاء واجهة هذه الـ Widget.
  ///
  /// Flutter قد يستدعيها مرات كثيرة عند إعادة الرسم.
  ///
  /// لذلك:
  ///
  /// ❌ لا نضع API هنا.
  /// ❌ لا نضع عمليات قاعدة بيانات.
  /// ❌ لا نضع حسابات ثقيلة.
  ///
  /// يجب أن تبقى سريعة جداً.
  ///
  @override
  Widget build(BuildContext context) {

    /// MaterialApp
    ///
    /// يعتبر الأب الحقيقي لكل تطبيق Flutter
    /// المبني باستخدام Material Design.
    ///
    /// يقوم بإنشاء:
    ///
    /// ✅ Navigator
    /// ✅ Theme
    /// ✅ MediaQuery
    /// ✅ Localizations
    /// ✅ Directionality
    /// ✅ Hero Controller
    /// ✅ Default Text Style
    ///
    /// أي أن أغلب Widgets تعتمد عليه.
    ///
    return MaterialApp(

      /// اسم التطبيق.
      ///
      /// لا يظهر داخل الواجهة.
      ///
      /// يستخدمه النظام في بعض المنصات
      /// مثل Android و Web.
      ///
      title: 'Weather App',

      /// يخفي شريط DEBUG الموجود أعلى الشاشة
      /// أثناء التطوير.
      ///
      /// يفضل إيقافه دائماً.
      ///
      debugShowCheckedModeBanner: false,

      /// الثيم الفاتح.
      ///
      /// سيستخدم عندما يكون التطبيق
      /// في وضع Light Mode.
      ///
      theme: AppTheme.lightTheme,

      /// الثيم الداكن.
      ///
      /// سيستخدم عندما يكون التطبيق
      /// في وضع Dark Mode.
      ///
      darkTheme: AppTheme.darkTheme,

      /// يحدد طريقة اختيار الثيم.
      ///
      /// يوجد ثلاث قيم:
      ///
      /// ThemeMode.light
      ///     التطبيق دائماً فاتح.
      ///
      /// ThemeMode.dark
      ///     التطبيق دائماً داكن.
      ///
      /// ThemeMode.system
      ///     يعتمد على إعدادات الهاتف.
      ///
      /// لاحقاً سنستبدلها بقيمة قادمة من ThemeCubit
      /// ليتمكن المستخدم من تغيير الثيم من داخل التطبيق.
      ///
      themeMode: ThemeMode.system,

      /// أول شاشة تظهر للمستخدم.
      ///
      /// حالياً سنستخدم Scaffold بسيط.
      ///
      /// لاحقاً سيتم استبداله بـ:
      ///
      /// SplashPage
      ///
      /// أو باستخدام GoRouter.
      ///
      home: Scaffold(

        /// لون الخلفية سيأتي تلقائياً
        /// من ThemeData.
        ///
        /// لذلك لا نحدد backgroundColor هنا.
        ///
        body: Center(

          /// Widget يقوم بتوسيط العنصر
          /// أفقياً وعمودياً.
          ///
          child: Text(

            /// النص المعروض داخل الشاشة.
            ///
            'Weather App',

            /// لا نكتب TextStyle هنا.
            ///
            /// لأن جميع الخطوط ستأتي
            /// من ThemeData.
            ///
            /// لاحقاً سنكتب:
            ///
            /// Theme.of(context)
            ///     .textTheme
            ///     .headlineLarge
            ///
            style: Theme.of(context)
                .textTheme
                .headlineLarge,
          ),
        ),
      ),
    );
  }
}
Dart

File: app_colors.dart

Path

lib/core/theme/app_colors.dart
/// ============================================================================
/// File : app_colors.dart
/// Path : lib/core/theme/app_colors.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن جميع ألوان التطبيق.
///
/// أي لون سيتم استخدامه داخل المشروع
/// يجب أن يكون معرفاً هنا.
///
/// لماذا؟
///
/// لأن تغيير لون واحد لاحقاً سيكون من مكان واحد فقط،
/// بدلاً من البحث داخل عشرات الملفات.
///
/// ============================================================================

import 'package:flutter/material.dart';

/// ============================================================================
///
/// AppColors
///
/// ============================================================================
///
/// عبارة عن Class يحتوي ألواناً ثابتة فقط.
///
/// لا يحتوي:
///
/// ❌ Widgets
/// ❌ Functions
/// ❌ Business Logic
///
/// يحتوي فقط على Constants.
///
/// ============================================================================

class AppColors {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// الشرطة السفلية (_) تجعل الـ Constructor خاصاً.
  ///
  /// هذا يمنع إنشاء Object من هذا الكلاس.
  ///
  /// لذلك هذا السطر غير مسموح:
  ///
  /// AppColors colors = AppColors();
  ///
  /// لأننا لا نحتاج إنشاء نسخة.
  ///
  /// جميع القيم سيتم الوصول إليها مباشرة باستخدام:
  ///
  /// AppColors.primary
  ///
  AppColors._();

  //===========================================================================
  // Primary Colors
  //===========================================================================

  /// اللون الأساسي للتطبيق.
  ///
  /// يستخدم في:
  ///
  /// - AppBar
  /// - Buttons
  /// - Icons
  /// - Links
  /// - Progress Indicators
  ///
  /// يسمى Primary لأن وظيفته هي اللون الرئيسي،
  /// وليس لأنه أزرق.
  ///
  /// يمكن تغييره لأي لون مستقبلاً
  /// دون تعديل أي Widget.
  ///
  static const Color primary = Color(0xFF2196F3);

  /// اللون الثانوي.
  ///
  /// يستخدم مع اللون الأساسي
  /// لإعطاء تدرجات أو تمييز بعض العناصر.
  ///
  static const Color secondary = Color(0xFF64B5F6);

  //===========================================================================
  // Background Colors
  //===========================================================================

  /// لون خلفية التطبيق في الوضع الفاتح.
  ///
  static const Color backgroundLight = Color(0xFFF5F5F5);

  /// لون خلفية التطبيق في الوضع الداكن.
  ///
  static const Color backgroundDark = Color(0xFF121212);

  //===========================================================================
  // Card Colors
  //===========================================================================

  /// لون البطاقات في الوضع الفاتح.
  ///
  /// لا نستخدم Colors.white مباشرة داخل Widgets.
  ///
  static const Color cardLight = Colors.white;

  /// لون البطاقات في الوضع الداكن.
  ///
  static const Color cardDark = Color(0xFF1E1E1E);

  //===========================================================================
  // Text Colors
  //===========================================================================

  /// اللون الافتراضي للنصوص
  /// في الوضع الفاتح.
  ///
  static const Color textLight = Colors.black87;

  /// اللون الافتراضي للنصوص
  /// في الوضع الداكن.
  ///
  static const Color textDark = Colors.white;

  //===========================================================================
  // Status Colors
  //===========================================================================

  /// لون رسائل النجاح.
  ///
  static const Color success = Color(0xFF4CAF50);

  /// لون رسائل التحذير.
  ///
  static const Color warning = Color(0xFFFF9800);

  /// لون رسائل الخطأ.
  ///
  static const Color error = Color(0xFFF44336);

  /// لون المعلومات.
  ///
  static const Color info = Color(0xFF03A9F4);

  //===========================================================================
  // Divider
  //===========================================================================

  /// لون الفواصل بين العناصر.
  ///
  static const Color divider = Color(0xFFE0E0E0);

  //===========================================================================
  // Shadow
  //===========================================================================

  /// لون الظلال.
  ///
  static const Color shadow = Color(0x33000000);

}
Dart

File: app_text_theme.dart

Path

lib/core/theme/app_text_theme.dart




/// ============================================================================
/// File : app_text_theme.dart
/// Path : lib/core/theme/app_text_theme.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن جميع الخطوط (Typography) داخل التطبيق.
///
/// الهدف منه منع كتابة:
///
/// TextStyle(...)
///
/// داخل الصفحات.
///
/// أي Widget يحتاج إلى خط سيأخذه من ThemeData.
///
/// ============================================================================

import 'package:flutter/material.dart';

import 'app_colors.dart';

/// ============================================================================
///
/// AppTextTheme
///
/// ============================================================================
///
/// هذا الكلاس يحتوي جميع أنماط النصوص الخاصة بالتطبيق.
///
/// لا يحتوي:
///
/// ❌ Widgets
/// ❌ Functions
/// ❌ Business Logic
///
/// يحتوي فقط على TextTheme.
///
/// ============================================================================

class AppTextTheme {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object من هذا الكلاس.
  ///
  /// لذلك نستخدم:
  ///
  /// AppTextTheme.light
  ///
  /// وليس:
  ///
  /// AppTextTheme().light
  ///
  AppTextTheme._();

  //===========================================================================
  // Light Theme
  //===========================================================================

  /// --------------------------------------------------------------------------
  /// TextTheme
  /// --------------------------------------------------------------------------
  ///
  /// TextTheme ليس خطاً واحداً.
  ///
  /// بل هو مجموعة كبيرة من TextStyle.
  ///
  /// يمكن تخيله بهذا الشكل:
  ///
  /// TextTheme
  /// │
  /// ├── headlineLarge
  /// ├── headlineMedium
  /// ├── headlineSmall
  /// ├── titleLarge
  /// ├── titleMedium
  /// ├── bodyLarge
  /// ├── bodyMedium
  /// ├── bodySmall
  /// ├── labelLarge
  /// ├── labelMedium
  /// └── ...
  ///
  /// كل عنصر منها عبارة عن TextStyle مستقل.
  ///
  static const TextTheme light = TextTheme(

    //=========================================================================
    // Headline Large
    //=========================================================================

    /// يستخدم للعناوين الرئيسية.
    ///
    /// مثال:
    ///
    /// - Weather
    /// - اسم المدينة
    /// - درجة الحرارة الكبيرة
    ///
    headlineLarge: TextStyle(

      /// حجم الخط.
      ///
      fontSize: 32,

      /// سماكة الخط.
      ///
      fontWeight: FontWeight.bold,

      /// لون الخط.
      ///
      color: AppColors.textLight,
    ),

    //=========================================================================
    // Headline Medium
    //=========================================================================

    /// يستخدم للعناوين الثانوية.
    ///
    headlineMedium: TextStyle(

      fontSize: 24,

      fontWeight: FontWeight.w600,

      color: AppColors.textLight,
    ),

    //=========================================================================
    // Title Large
    //=========================================================================

    /// يستخدم لعناوين البطاقات.
    ///
    titleLarge: TextStyle(

      fontSize: 20,

      fontWeight: FontWeight.w600,

      color: AppColors.textLight,
    ),

    //=========================================================================
    // Body Large
    //=========================================================================

    /// يستخدم للنصوص المهمة.
    ///
    bodyLarge: TextStyle(

      fontSize: 18,

      fontWeight: FontWeight.w500,

      color: AppColors.textLight,
    ),

    //=========================================================================
    // Body Medium
    //=========================================================================

    /// أكثر TextStyle سيتم استخدامه داخل التطبيق.
    ///
    /// مثل:
    ///
    /// - وصف الطقس
    /// - التاريخ
    /// - أسماء الأيام
    ///
    bodyMedium: TextStyle(

      fontSize: 16,

      fontWeight: FontWeight.normal,

      color: AppColors.textLight,
    ),

    //=========================================================================
    // Body Small
    //=========================================================================

    /// يستخدم للنصوص الصغيرة.
    ///
    /// مثل:
    ///
    /// - الضغط
    /// - الرطوبة
    /// - سرعة الرياح
    ///
    bodySmall: TextStyle(

      fontSize: 14,

      color: Colors.grey,
    ),
  );

  //===========================================================================
  // Dark Theme
  //===========================================================================

  /// نفس فكرة Light Theme
  ///
  /// لكن يتم تغيير الألوان فقط.
  ///
  /// أما الأحجام والأوزان
  /// فتبقى نفسها حتى لا تتغير تجربة المستخدم.
  ///
  static const TextTheme dark = TextTheme(

    headlineLarge: TextStyle(

      fontSize: 32,

      fontWeight: FontWeight.bold,

      color: AppColors.textDark,
    ),

    headlineMedium: TextStyle(

      fontSize: 24,

      fontWeight: FontWeight.w600,

      color: AppColors.textDark,
    ),

    titleLarge: TextStyle(

      fontSize: 20,

      fontWeight: FontWeight.w600,

      color: AppColors.textDark,
    ),

    bodyLarge: TextStyle(

      fontSize: 18,

      fontWeight: FontWeight.w500,

      color: AppColors.textDark,
    ),

    bodyMedium: TextStyle(

      fontSize: 16,

      fontWeight: FontWeight.normal,

      color: AppColors.textDark,
    ),

    bodySmall: TextStyle(

      fontSize: 14,

      color: Colors.grey,
    ),
  );
}
Dart

File: app_theme.dart

Path

lib/core/theme/app_theme.dart
/// ============================================================================
/// File : app_theme.dart
/// Path : lib/core/theme/app_theme.dart
/// ============================================================================
///
/// هذا الملف هو المسؤول عن إنشاء ThemeData الخاص بالتطبيق.
///
/// لاحظ الفرق:
///
/// AppColors
///     ↓
/// يحتوي الألوان فقط.
///
/// AppTextTheme
///     ↓
/// يحتوي الخطوط فقط.
///
/// AppTheme
///     ↓
/// يجمع الألوان والخطوط داخل ThemeData.
///
/// أي أن MaterialApp لن يقرأ AppColors مباشرة.
///
/// وإنما سيقرأ ThemeData.
///
/// ============================================================================

import 'package:flutter/material.dart';

import 'app_colors.dart';
import 'app_text_theme.dart';

/// ============================================================================
///
/// AppTheme
///
/// ============================================================================
///
/// يحتوي ثيمين:
///
/// 1- Light Theme
/// 2- Dark Theme
///
/// لاحقاً سيتم استخدامهما داخل:
///
/// MaterialApp(
///   theme: AppTheme.lightTheme,
///   darkTheme: AppTheme.darkTheme,
/// )
///
/// ============================================================================

class AppTheme {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object.
  ///
  /// لأن هذا الكلاس عبارة عن Container فقط.
  ///
  AppTheme._();

  //===========================================================================
  // Light Theme
  //===========================================================================

  /// ThemeData
  ///
  /// يعتبر القلب الحقيقي لشكل التطبيق.
  ///
  /// أي Widget لا تحدد له لوناً أو خطاً
  /// سيأخذ القيم من هنا.
  ///
  static final ThemeData lightTheme = ThemeData(

    //=========================================================================
    // Material Design Version
    //=========================================================================

    /// يجعل التطبيق يستخدم Material Design 3.
    ///
    /// إذا جعلتها false
    /// سيستخدم Material Design 2.
    ///
    useMaterial3: true,

    //=========================================================================
    // Brightness
    //=========================================================================

    /// يخبر Flutter أن هذا الثيم فاتح.
    ///
    brightness: Brightness.light,

    //=========================================================================
    // Primary Color
    //=========================================================================

    /// اللون الرئيسي للتطبيق.
    ///
    /// تستخدمه Widgets كثيرة بشكل افتراضي.
    ///
    primaryColor: AppColors.primary,

    //=========================================================================
    // Background
    //=========================================================================

    /// لون خلفية Scaffold.
    ///
    /// لذلك لن نكتب:
    ///
    /// backgroundColor
    ///
    /// داخل كل Scaffold.
    ///
    scaffoldBackgroundColor: AppColors.backgroundLight,

    //=========================================================================
    // Card
    //=========================================================================

    /// اللون الافتراضي لجميع Card Widgets.
    ///
    cardColor: AppColors.cardLight,

    //=========================================================================
    // Divider
    //=========================================================================

    /// اللون الافتراضي للفواصل.
    ///
    dividerColor: AppColors.divider,

    //=========================================================================
    // Text Theme
    //=========================================================================

    /// جميع النصوص داخل التطبيق
    /// ستستخدم هذا TextTheme.
    ///
    textTheme: AppTextTheme.light,

    //=========================================================================
    // Color Scheme
    //=========================================================================

    /// يعتبر أهم جزء في Material 3.
    ///
    /// Widgets الحديثة لا تعتمد على:
    ///
    /// primaryColor
    ///
    /// وإنما تعتمد على:
    ///
    /// colorScheme
    ///
    colorScheme: const ColorScheme.light(

      /// اللون الرئيسي.
      ///
      primary: AppColors.primary,

      /// اللون الثانوي.
      ///
      secondary: AppColors.secondary,

      /// لون الخطأ.
      ///
      error: AppColors.error,

      /// لون الخلفيات.
      ///
      surface: AppColors.backgroundLight,

      /// لون النص فوق اللون الرئيسي.
      ///
      onPrimary: Colors.white,

      /// لون النص فوق الخلفية.
      ///
      onSurface: AppColors.textLight,
    ),

    //=========================================================================
    // AppBar
    //=========================================================================

    /// جميع AppBar داخل التطبيق
    /// ستستخدم هذه الإعدادات.
    ///
    appBarTheme: const AppBarTheme(

      centerTitle: true,

      elevation: 0,

      backgroundColor: AppColors.primary,

      foregroundColor: Colors.white,
    ),

    //=========================================================================
    // Card Theme
    //=========================================================================

    /// الشكل الافتراضي لجميع البطاقات.
    ///
    cardTheme: const CardThemeData(

      elevation: 2,

      margin: EdgeInsets.all(8),

      shape: RoundedRectangleBorder(

        borderRadius: BorderRadius.all(
          Radius.circular(16),
        ),
      ),
    ),

    //=========================================================================
    // Elevated Button Theme
    //=========================================================================

    /// جميع ElevatedButton
    /// ستستخدم هذا الشكل.
    ///
    elevatedButtonTheme: ElevatedButtonThemeData(

      style: ElevatedButton.styleFrom(

        backgroundColor: AppColors.primary,

        foregroundColor: Colors.white,

        minimumSize: const Size(double.infinity, 50),

        shape: RoundedRectangleBorder(

          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
  );

  //===========================================================================
  // Dark Theme
  //===========================================================================

  /// نفس فكرة Light Theme.
  ///
  /// لكن يتم تغيير الألوان فقط.
  ///
  static final ThemeData darkTheme = ThemeData(

    useMaterial3: true,

    brightness: Brightness.dark,

    primaryColor: AppColors.primary,

    scaffoldBackgroundColor: AppColors.backgroundDark,

    cardColor: AppColors.cardDark,

    dividerColor: Colors.white24,

    textTheme: AppTextTheme.dark,

    colorScheme: const ColorScheme.dark(

      primary: AppColors.primary,

      secondary: AppColors.secondary,

      error: AppColors.error,

      surface: AppColors.backgroundDark,

      onPrimary: Colors.white,

      onSurface: AppColors.textDark,
    ),

    appBarTheme: const AppBarTheme(

      centerTitle: true,

      elevation: 0,

      backgroundColor: AppColors.backgroundDark,

      foregroundColor: Colors.white,
    ),

    cardTheme: const CardThemeData(

      elevation: 2,

      margin: EdgeInsets.all(8),

      shape: RoundedRectangleBorder(

        borderRadius: BorderRadius.all(
          Radius.circular(16),
        ),
      ),
    ),

    elevatedButtonTheme: ElevatedButtonThemeData(

      style: ElevatedButton.styleFrom(

        backgroundColor: AppColors.primary,

        foregroundColor: Colors.white,

        minimumSize: const Size(double.infinity, 50),

        shape: RoundedRectangleBorder(

          borderRadius: BorderRadius.circular(12),
        ),
      ),
    ),
  );
}
Dart

File: app_constants.dart

Path

lib/core/constants/app_constants.dart




/// ============================================================================
/// File : app_constants.dart
/// Path : lib/core/constants/app_constants.dart
/// ============================================================================
///
/// يحتوي هذا الملف على جميع الثوابت (Constants)
/// المستخدمة في التطبيق.
///
/// المقصود بالثوابت:
///
/// هي قيم لن تتغير أثناء تشغيل التطبيق.
///
/// مثال:
///
/// - اسم التطبيق
/// - مدة الأنيميشن
/// - عدد أيام التوقعات
/// - Timeout
/// - Padding
/// - Border Radius
///
/// وجودها في ملف واحد يجعل تعديلها لاحقاً سهلاً.
///
/// ============================================================================

import 'package:flutter/material.dart';

/// ============================================================================
///
/// AppConstants
///
/// ============================================================================
///
/// هذا الكلاس عبارة عن Container فقط.
///
/// لا يحتوي:
///
/// ❌ Widgets
/// ❌ Functions
/// ❌ Business Logic
///
/// يحتوي فقط على قيم ثابتة.
///
/// ============================================================================

class AppConstants {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object.
  ///
  /// لا نحتاج:
  ///
  /// AppConstants();
  ///
  /// وإنما:
  ///
  /// AppConstants.appName
  ///
  AppConstants._();

  //===========================================================================
  // Application
  //===========================================================================

  /// اسم التطبيق.
  ///
  /// يستخدم داخل:
  ///
  /// MaterialApp
  /// AppBar
  /// About Dialog
  ///
  static const String appName = 'Weather App';

  /// اللغة الافتراضية.
  ///
  static const String defaultLanguage = 'ar';

  /// الدولة الافتراضية.
  ///
  static const String defaultCountry = 'SY';

  //===========================================================================
  // Weather
  //===========================================================================

  /// عدد الأيام التي سيتم عرضها.
  ///
  static const int forecastDays = 7;

  /// درجة الحرارة الافتراضية.
  ///
  /// تستخدم قبل وصول البيانات من API.
  ///
  static const double defaultTemperature = 0.0;

  //===========================================================================
  // Animation
  //===========================================================================

  /// مدة الأنيميشن السريع.
  ///
  static const Duration fastAnimation = Duration(
    milliseconds: 200,
  );

  /// مدة الأنيميشن المتوسطة.
  ///
  static const Duration mediumAnimation = Duration(
    milliseconds: 300,
  );

  /// مدة الأنيميشن الطويلة.
  ///
  static const Duration slowAnimation = Duration(
    milliseconds: 500,
  );

  //===========================================================================
  // Network
  //===========================================================================

  /// أقصى مدة انتظار للاتصال بالسيرفر.
  ///
  static const Duration connectTimeout = Duration(
    seconds: 15,
  );

  /// أقصى مدة لانتظار استجابة السيرفر.
  ///
  static const Duration receiveTimeout = Duration(
    seconds: 15,
  );

  //===========================================================================
  // UI
  //===========================================================================

  /// المسافة الصغيرة.
  ///
  static const double smallPadding = 8;

  /// المسافة المتوسطة.
  ///
  static const double mediumPadding = 16;

  /// المسافة الكبيرة.
  ///
  static const double largePadding = 24;

  /// نصف قطر الحواف الصغيرة.
  ///
  static const double smallRadius = 8;

  /// نصف قطر الحواف المتوسطة.
  ///
  static const double mediumRadius = 12;

  /// نصف قطر الحواف الكبيرة.
  ///
  static const double largeRadius = 20;

  //===========================================================================
  // Icons
  //===========================================================================

  /// حجم الأيقونات الصغيرة.
  ///
  static const double smallIcon = 18;

  /// حجم الأيقونات المتوسطة.
  ///
  static const double mediumIcon = 24;

  /// حجم الأيقونات الكبيرة.
  ///
  static const double largeIcon = 40;

  //===========================================================================
  // Elevation
  //===========================================================================

  /// الارتفاع الافتراضي للبطاقات.
  ///
  static const double cardElevation = 2;

  /// الارتفاع الافتراضي للأزرار.
  ///
  static const double buttonElevation = 0;

  //===========================================================================
  // Storage Keys
  //===========================================================================

  /// مفتاح حفظ المدينة الأخيرة.
  ///
  static const String lastCityKey = 'last_city';

  /// مفتاح حفظ الثيم.
  ///
  static const String themeKey = 'theme_mode';

  /// مفتاح حفظ اللغة.
  ///
  static const String languageKey = 'language';

  //===========================================================================
  // Assets
  //===========================================================================

  /// مجلد الصور.
  ///
  static const String imagePath = 'assets/images';

  /// مجلد الأيقونات.
  ///
  static const String iconPath = 'assets/icons';

  /// مجلد الرسوم المتحركة.
  ///
  static const String animationPath = 'assets/animations';

  //===========================================================================
  // API
  //===========================================================================

  /// سيتم وضع Base URL الحقيقي لاحقاً.
  ///
  static const String baseUrl = '';

  /// سيتم وضع API Key الحقيقي لاحقاً.
  ///
  static const String apiKey = '';
}
Dart

File: api_constants.dart

Path

lib/core/constants/api_constants.dart




/// ============================================================================
/// File : api_constants.dart
/// Path : lib/core/constants/api_constants.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن جميع الثوابت المتعلقة بالـ API.
///
/// لماذا أنشأنا ملفاً مستقلاً؟
///
/// لأن AppConstants يحتوي ثوابت عامة للتطبيق.
///
/// أما هذا الملف فهو خاص بالشبكة فقط.
///
/// عند تغيير مزود الـ API مستقبلاً
/// لن نحتاج للبحث داخل المشروع.
///
/// سنعدل هنا فقط.
///
/// ============================================================================

class ApiConstants {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object.
  ///
  /// الاستخدام الصحيح:
  ///
  /// ApiConstants.baseUrl
  ///
  ApiConstants._();

  //===========================================================================
  // Base URL
  //===========================================================================

  /// الرابط الأساسي للـ API.
  ///
  /// جميع Endpoints ستبنى فوق هذا الرابط.
  ///
  /// مثال:
  ///
  /// https://api.weatherapi.com/v1
  ///
  /// لاحقاً عند استخدام Dio سيتم تمريره إلى:
  ///
  /// BaseOptions(
  ///   baseUrl: ApiConstants.baseUrl,
  /// )
  ///
  static const String baseUrl =
      'https://api.weatherapi.com/v1';

  //===========================================================================
  // API KEY
  //===========================================================================

  /// مفتاح الـ API.
  ///
  /// ملاحظة مهمة:
  ///
  /// في المشاريع الحقيقية لا يفضل وضعه هنا.
  ///
  /// وإنما:
  ///
  /// flutter_dotenv
  ///
  /// أو
  ///
  /// --dart-define
  ///
  /// حتى لا يظهر داخل الكود المنشور.
  ///
  /// ولكن في هذا المشروع التعليمي سنضعه هنا
  /// ثم ننقله لاحقاً إلى مكان أكثر أماناً.
  ///
  static const String apiKey = 'YOUR_API_KEY';

  //===========================================================================
  // Endpoints
  //===========================================================================

  /// Endpoint الخاص بالطقس الحالي.
  ///
  /// سيتم استخدامه داخل:
  ///
  /// WeatherRemoteDataSource
  ///
  static const String currentWeather = '/current.json';

  /// Endpoint الخاص بتوقعات الطقس.
  ///
  static const String forecastWeather = '/forecast.json';

  /// Endpoint البحث عن المدن.
  ///
  static const String searchCity = '/search.json';

  //===========================================================================
  // Query Parameters
  //===========================================================================

  /// اسم بارامتر مفتاح الـ API.
  ///
  static const String key = 'key';

  /// اسم بارامتر المدينة.
  ///
  static const String query = 'q';

  /// اسم بارامتر عدد الأيام.
  ///
  static const String days = 'days';

  /// اسم بارامتر جودة الهواء.
  ///
  static const String airQuality = 'aqi';

  /// اسم بارامتر التنبيهات الجوية.
  ///
  static const String alerts = 'alerts';

  //===========================================================================
  // Default Values
  //===========================================================================

  /// عدد الأيام الافتراضي.
  ///
  static const int defaultForecastDays = 7;

  /// إيقاف جودة الهواء افتراضياً.
  ///
  static const String defaultAirQuality = 'no';

  /// إيقاف التنبيهات الجوية افتراضياً.
  ///
  static const String defaultAlerts = 'no';

  //===========================================================================
  // Headers
  //===========================================================================

  /// نوع البيانات المرسلة.
  ///
  static const String contentType = 'Content-Type';

  /// نوع البيانات المستقبلة.
  ///
  static const String accept = 'Accept';

  /// القيمة الافتراضية.
  ///
  static const String applicationJson = 'application/json';

  //===========================================================================
  // Timeouts
  //===========================================================================

  /// مهلة الاتصال بالسيرفر.
  ///
  /// تستخدم داخل Dio.
  ///
  static const Duration connectTimeout =
      Duration(seconds: 15);

  /// مهلة استقبال البيانات.
  ///
  static const Duration receiveTimeout =
      Duration(seconds: 15);

  /// مهلة إرسال البيانات.
  ///
  static const Duration sendTimeout =
      Duration(seconds: 15);
}
Dart

File: api_constants.dart

Path

lib/core/constants/api_constants.dart




/// ============================================================================
/// File : api_constants.dart
/// Path : lib/core/constants/api_constants.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن جميع الثوابت المتعلقة بالـ API.
///
/// لماذا أنشأنا ملفاً مستقلاً؟
///
/// لأن AppConstants يحتوي ثوابت عامة للتطبيق.
///
/// أما هذا الملف فهو خاص بالشبكة فقط.
///
/// عند تغيير مزود الـ API مستقبلاً
/// لن نحتاج للبحث داخل المشروع.
///
/// سنعدل هنا فقط.
///
/// ============================================================================

class ApiConstants {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object.
  ///
  /// الاستخدام الصحيح:
  ///
  /// ApiConstants.baseUrl
  ///
  ApiConstants._();

  //===========================================================================
  // Base URL
  //===========================================================================

  /// الرابط الأساسي للـ API.
  ///
  /// جميع Endpoints ستبنى فوق هذا الرابط.
  ///
  /// مثال:
  ///
  /// https://api.weatherapi.com/v1
  ///
  /// لاحقاً عند استخدام Dio سيتم تمريره إلى:
  ///
  /// BaseOptions(
  ///   baseUrl: ApiConstants.baseUrl,
  /// )
  ///
  static const String baseUrl =
      'https://api.weatherapi.com/v1';

  //===========================================================================
  // API KEY
  //===========================================================================

  /// مفتاح الـ API.
  ///
  /// ملاحظة مهمة:
  ///
  /// في المشاريع الحقيقية لا يفضل وضعه هنا.
  ///
  /// وإنما:
  ///
  /// flutter_dotenv
  ///
  /// أو
  ///
  /// --dart-define
  ///
  /// حتى لا يظهر داخل الكود المنشور.
  ///
  /// ولكن في هذا المشروع التعليمي سنضعه هنا
  /// ثم ننقله لاحقاً إلى مكان أكثر أماناً.
  ///
  static const String apiKey = 'YOUR_API_KEY';

  //===========================================================================
  // Endpoints
  //===========================================================================

  /// Endpoint الخاص بالطقس الحالي.
  ///
  /// سيتم استخدامه داخل:
  ///
  /// WeatherRemoteDataSource
  ///
  static const String currentWeather = '/current.json';

  /// Endpoint الخاص بتوقعات الطقس.
  ///
  static const String forecastWeather = '/forecast.json';

  /// Endpoint البحث عن المدن.
  ///
  static const String searchCity = '/search.json';

  //===========================================================================
  // Query Parameters
  //===========================================================================

  /// اسم بارامتر مفتاح الـ API.
  ///
  static const String key = 'key';

  /// اسم بارامتر المدينة.
  ///
  static const String query = 'q';

  /// اسم بارامتر عدد الأيام.
  ///
  static const String days = 'days';

  /// اسم بارامتر جودة الهواء.
  ///
  static const String airQuality = 'aqi';

  /// اسم بارامتر التنبيهات الجوية.
  ///
  static const String alerts = 'alerts';

  //===========================================================================
  // Default Values
  //===========================================================================

  /// عدد الأيام الافتراضي.
  ///
  static const int defaultForecastDays = 7;

  /// إيقاف جودة الهواء افتراضياً.
  ///
  static const String defaultAirQuality = 'no';

  /// إيقاف التنبيهات الجوية افتراضياً.
  ///
  static const String defaultAlerts = 'no';

  //===========================================================================
  // Headers
  //===========================================================================

  /// نوع البيانات المرسلة.
  ///
  static const String contentType = 'Content-Type';

  /// نوع البيانات المستقبلة.
  ///
  static const String accept = 'Accept';

  /// القيمة الافتراضية.
  ///
  static const String applicationJson = 'application/json';

  //===========================================================================
  // Timeouts
  //===========================================================================

  /// مهلة الاتصال بالسيرفر.
  ///
  /// تستخدم داخل Dio.
  ///
  static const Duration connectTimeout =
      Duration(seconds: 15);

  /// مهلة استقبال البيانات.
  ///
  static const Duration receiveTimeout =
      Duration(seconds: 15);

  /// مهلة إرسال البيانات.
  ///
  static const Duration sendTimeout =
      Duration(seconds: 15);
}
Dart

File: api_client.dart

Path

lib/core/network/api_client.dart
/// ============================================================================
/// File : api_client.dart
/// Path : lib/core/network/api_client.dart
/// ============================================================================
///
/// هذا الملف يعتبر البوابة الوحيدة للتعامل مع الإنترنت.
///
/// أي Request أو Response يجب أن يمر من هنا.
///
/// ممنوع في المشروع كتابة:
///
/// Dio().get(...)
///
/// داخل:
///
/// ❌ Cubit
/// ❌ Repository
/// ❌ DataSource
/// ❌ Widgets
///
/// جميع الاتصالات تمر من هذا الكلاس.
///
/// لاحقاً سيتم إضافة:
///
/// ✅ Interceptors
/// ✅ Logging
/// ✅ Retry
/// ✅ Authentication
/// ✅ Refresh Token
/// ✅ Certificate Pinning
///
/// دون تعديل أي جزء آخر من المشروع.
///
/// ============================================================================

import 'package:dio/dio.dart';

import '../constants/api_constants.dart';

/// ============================================================================
///
/// ApiClient
///
/// ============================================================================
///
/// Wrapper فوق Dio.
///
/// الهدف منه:
///
/// توحيد جميع إعدادات الشبكة في مكان واحد.
///
/// ============================================================================

class ApiClient {

  /// --------------------------------------------------------------------------
  /// Dio Instance
  /// --------------------------------------------------------------------------
  ///
  /// هذا هو الكائن الحقيقي المسؤول عن إرسال الطلبات.
  ///
  /// لاحظ أننا جعلناه private.
  ///
  /// أي لا يستطيع أي كلاس الوصول إليه مباشرة.
  ///
  final Dio _dio;

  /// --------------------------------------------------------------------------
  /// Constructor
  /// --------------------------------------------------------------------------
  ///
  /// عند إنشاء ApiClient
  /// نقوم مباشرة بتهيئة Dio.
  ///
  ApiClient()
      : _dio = Dio(

          /// BaseOptions
          ///
          /// يحتوي الإعدادات الافتراضية.
          ///
          BaseOptions(

            /// الرابط الأساسي.
            ///
            /// لاحقاً عندما نكتب:
            ///
            /// /forecast.json
            ///
            /// سيصبح:
            ///
            /// https://api.weatherapi.com/v1/forecast.json
            ///
            baseUrl: ApiConstants.baseUrl,

            /// مدة انتظار الاتصال.
            ///
            connectTimeout:
                ApiConstants.connectTimeout,

            /// مدة استقبال البيانات.
            ///
            receiveTimeout:
                ApiConstants.receiveTimeout,

            /// مدة إرسال البيانات.
            ///
            sendTimeout:
                ApiConstants.sendTimeout,

            /// نوع البيانات المستقبلة.
            ///
            responseType: ResponseType.json,

            /// نوع البيانات المرسلة.
            ///
            contentType:
                ApiConstants.applicationJson,

            /// Headers الافتراضية.
            ///
            headers: {

              ApiConstants.accept:
                  ApiConstants.applicationJson,

              ApiConstants.contentType:
                  ApiConstants.applicationJson,
            },
          ),
        ) {

    /// ------------------------------------------------------------------------
    /// Interceptors
    /// ------------------------------------------------------------------------
    ///
    /// الـ Interceptor يشبه نقطة التفتيش.
    ///
    /// كل Request يمر من هنا.
    ///
    /// وكل Response يمر من هنا.
    ///
    /// وكل Error يمر من هنا.
    ///
    /// لاحقاً سنستخدمه في:
    ///
    /// Logging
    /// Authentication
    /// Refresh Token
    /// Retry
    ///
    _dio.interceptors.add(

      InterceptorsWrapper(

        //======================================================================
        // Request
        //======================================================================

        onRequest: (

          options,

          handler,

        ) {

          /// هنا نستطيع تعديل الطلب
          /// قبل إرساله للسيرفر.
          ///
          /// مثال:
          ///
          /// إضافة Token
          /// إضافة Headers
          /// طباعة الطلب
          ///
          handler.next(options);
        },

        //======================================================================
        // Response
        //======================================================================

        onResponse: (

          response,

          handler,

        ) {

          /// هنا نستقبل البيانات
          /// قبل وصولها لبقية المشروع.
          ///
          handler.next(response);
        },

        //======================================================================
        // Error
        //======================================================================

        onError: (

          error,

          handler,

        ) {

          /// جميع أخطاء الشبكة
          /// تمر من هنا.
          ///
          /// مثل:
          ///
          /// Timeout
          /// Socket Exception
          /// 404
          /// 500
          /// وغيرها.
          ///
          handler.next(error);
        },
      ),
    );
  }

  //===========================================================================
  // GET
  //===========================================================================

  /// إرسال طلب GET.
  ///
  /// مثال:
  ///
  /// get(
  ///     "/forecast.json",
  ///     queryParameters: {}
  /// );
  ///
  Future<Response<dynamic>> get(

    String path, {

    Map<String, dynamic>? queryParameters,

  }) async {

    return await _dio.get(

      path,

      queryParameters: queryParameters,
    );
  }

  //===========================================================================
  // POST
  //===========================================================================

  /// إرسال بيانات إلى السيرفر.
  ///
  Future<Response<dynamic>> post(

    String path, {

    dynamic data,

    Map<String, dynamic>? queryParameters,

  }) async {

    return await _dio.post(

      path,

      data: data,

      queryParameters: queryParameters,
    );
  }

  //===========================================================================
  // PUT
  //===========================================================================

  /// تحديث بيانات موجودة.
  ///
  Future<Response<dynamic>> put(

    String path, {

    dynamic data,

    Map<String, dynamic>? queryParameters,

  }) async {

    return await _dio.put(

      path,

      data: data,

      queryParameters: queryParameters,
    );
  }

  //===========================================================================
  // DELETE
  //===========================================================================

  /// حذف بيانات من السيرفر.
  ///
  Future<Response<dynamic>> delete(

    String path, {

    dynamic data,

    Map<String, dynamic>? queryParameters,

  }) async {

    return await _dio.delete(

      path,

      data: data,

      queryParameters: queryParameters,
    );
  }
}
Dart

File: network_info.dart

Path

lib/core/network/network_info.dart
/// ============================================================================
/// File : network_info.dart
/// Path : lib/core/network/network_info.dart
/// ============================================================================
///
/// مسؤولية هذا الملف هي معرفة:
///
/// هل يوجد اتصال بالإنترنت أم لا؟
///
/// لاحظ:
///
/// هذا الملف لا يرسل أي Request.
///
/// ولا يعرف أي شيء عن API.
///
/// ولا يعرف Dio.
///
/// هو فقط يجيب على سؤال واحد:
///
/// "هل الجهاز متصل بالإنترنت؟"
///
/// وهذا يحقق مبدأ:
///
/// Single Responsibility Principle.
///
/// ============================================================================

import 'package:connectivity_plus/connectivity_plus.dart';

/// ============================================================================
///
/// NetworkInfo
///
/// ============================================================================
///
/// عبارة عن Interface.
///
/// لماذا Interface ؟
///
/// لأن Repository لا يجب أن يعرف
/// كيف يتم فحص الاتصال.
///
/// هو فقط يريد معرفة:
///
/// true
///
/// أو
///
/// false
///
/// ============================================================================

abstract class NetworkInfo {

  /// --------------------------------------------------------------------------
  /// isConnected
  /// --------------------------------------------------------------------------
  ///
  /// Future
  ///
  /// لأن عملية فحص الشبكة غير متزامنة.
  ///
  /// bool
  ///
  /// true  -> يوجد اتصال.
  ///
  /// false -> لا يوجد اتصال.
  ///
  Future<bool> get isConnected;
}

/// ============================================================================
///
/// NetworkInfoImpl
///
/// ============================================================================
///
/// التطبيق الحقيقي للـ Interface.
///
/// هذا الكلاس هو الوحيد الذي يعرف
/// مكتبة connectivity_plus.
///
/// أما بقية المشروع
/// فلا يعرف شيئاً عنها.
///
/// ============================================================================

class NetworkInfoImpl implements NetworkInfo {

  /// --------------------------------------------------------------------------
  /// Connectivity
  /// --------------------------------------------------------------------------
  ///
  /// الكلاس المسؤول عن التواصل
  /// مع نظام التشغيل لمعرفة حالة الشبكة.
  ///
  final Connectivity connectivity;

  /// Constructor
  ///
  /// نستقبل Connectivity من الخارج.
  ///
  /// لاحقاً سيتم حقنه باستخدام:
  ///
  /// GetIt
  ///
  NetworkInfoImpl(this.connectivity);

  /// --------------------------------------------------------------------------
  /// isConnected
  /// --------------------------------------------------------------------------
  ///
  /// تقوم بفحص حالة الاتصال الحالية.
  ///
  @override
  Future<bool> get isConnected async {

    /// ------------------------------------------------------------------------
    /// checkConnectivity()
    /// ------------------------------------------------------------------------
    ///
    /// تعيد نوع الاتصال الحالي.
    ///
    /// القيم الممكنة:
    ///
    /// wifi
    /// mobile
    /// ethernet
    /// vpn
    /// bluetooth
    /// none
    ///
    final List<ConnectivityResult> result =
        await connectivity.checkConnectivity();

    /// ------------------------------------------------------------------------
    /// إذا احتوت النتيجة على:
    ///
    /// ConnectivityResult.none
    ///
    /// فهذا يعني عدم وجود اتصال.
    ///
    if (result.contains(ConnectivityResult.none)) {
      return false;
    }

    /// ------------------------------------------------------------------------
    /// أي قيمة أخرى تعني
    /// وجود اتصال بالشبكة.
    ///
    /// ملاحظة مهمة:
    ///
    /// هذا لا يعني أن الإنترنت يعمل فعلاً.
    ///
    /// بل يعني فقط أن الجهاز متصل
    /// بشبكة WiFi أو بيانات أو Ethernet.
    ///
    /// لذلك لاحقاً يمكن تطوير هذا الكلاس
    /// ليتحقق من الوصول إلى الإنترنت
    /// الحقيقي بواسطة Ping أو Request صغير.
    ///
    return true;
  }
}
Dart

File: exceptions.dart

Path

lib/core/errors/exceptions.dart
/// ============================================================================
/// File : exceptions.dart
/// Path : lib/core/errors/exceptions.dart
/// ============================================================================
///
/// هذا الملف يحتوي جميع Exceptions الخاصة بالمشروع.
///
/// انتبه جيداً...
///
/// Exception ≠ Failure
///
/// Exception
/// ----------
/// تمثل الخطأ الذي حدث داخل مصدر البيانات (Data Source).
///
/// مثال:
///
/// - Dio رمى Exception.
/// - SQLite رمت Exception.
/// - SharedPreferences فشل.
/// - API أعاد خطأ.
///
/// أما Failure فهو ينتقل إلى Domain Layer.
///
/// لذلك:
///
/// Data Layer
///      ↓
/// Exception
///      ↓
/// Repository
///      ↓
/// Failure
///      ↓
/// UseCase
///      ↓
/// Cubit
///
/// ============================================================================

/// ============================================================================
///
/// AppException
///
/// ============================================================================
///
/// الأب لجميع Exceptions داخل المشروع.
///
/// جميع Exceptions سترث منه.
///
/// ============================================================================

abstract class AppException implements Exception {

  /// رسالة الخطأ.
  ///
  final String message;

  /// Constructor
  ///
  const AppException(this.message);

  @override
  String toString() => message;
}

/// ============================================================================
///
/// ServerException
///
/// ============================================================================
///
/// يحدث عندما يعيد السيرفر استجابة غير ناجحة.
///
/// أمثلة:
///
/// 500
/// 404
/// 401
/// 403
///
/// ============================================================================

class ServerException extends AppException {

  const ServerException(super.message);

}

/// ============================================================================
///
/// CacheException
///
/// ============================================================================
///
/// يحدث عند فشل القراءة أو الكتابة
/// داخل التخزين المحلي.
///
/// مثال:
///
/// SharedPreferences
/// Hive
/// SQLite
///
/// ============================================================================

class CacheException extends AppException {

  const CacheException(super.message);

}

/// ============================================================================
///
/// NetworkException
///
/// ============================================================================
///
/// يحدث عند عدم وجود اتصال بالشبكة.
///
/// ============================================================================

class NetworkException extends AppException {

  const NetworkException(super.message);

}

/// ============================================================================
///
/// TimeoutException
///
/// ============================================================================
///
/// يحدث عند انتهاء مهلة الاتصال.
///
/// ============================================================================

class TimeoutException extends AppException {

  const TimeoutException(super.message);

}

/// ============================================================================
///
/// UnauthorizedException
///
/// ============================================================================
///
/// يحدث عندما يرفض السيرفر الطلب.
///
/// مثال:
///
/// 401 Unauthorized
///
/// ============================================================================

class UnauthorizedException extends AppException {

  const UnauthorizedException(super.message);

}

/// ============================================================================
///
/// ForbiddenException
///
/// ============================================================================
///
/// يحدث عند عدم امتلاك المستخدم
/// صلاحية للوصول.
///
/// مثال:
///
/// 403 Forbidden
///
/// ============================================================================

class ForbiddenException extends AppException {

  const ForbiddenException(super.message);

}

/// ============================================================================
///
/// NotFoundException
///
/// ============================================================================
///
/// يحدث عندما لا يجد السيرفر
/// المورد المطلوب.
///
/// مثال:
///
/// مدينة غير موجودة.
///
/// 404
///
/// ============================================================================

class NotFoundException extends AppException {

  const NotFoundException(super.message);

}

/// ============================================================================
///
/// BadRequestException
///
/// ============================================================================
///
/// يحدث عندما يكون الطلب غير صحيح.
///
/// مثال:
///
/// 400 Bad Request
///
/// ============================================================================

class BadRequestException extends AppException {

  const BadRequestException(super.message);

}

/// ============================================================================
///
/// UnknownException
///
/// ============================================================================
///
/// آخر Exception يتم استخدامه
/// إذا لم نستطع معرفة نوع الخطأ.
///
/// ============================================================================

class UnknownException extends AppException {

  const UnknownException(super.message);

}
Dart

File: failures.dart

Path

lib/core/errors/failures.dart
/// ============================================================================
/// File : failures.dart
/// Path : lib/core/errors/failures.dart
/// ============================================================================
///
/// هذا الملف يحتوي جميع Failures الخاصة بالمشروع.
///
/// انتبه جيداً...
///
/// Failure ليست Exception.
///
/// ============================================================================
///
/// الفرق بينهما:
///
/// Exception
/// ---------
/// تمثل خطأً تقنياً (Technical Error).
///
/// مثال:
///
/// DioException
/// SocketException
/// FormatException
///
/// مكانها:
///
/// Data Layer.
///
/// ============================================================================
///
/// Failure
/// -------
/// تمثل خطأً يفهمه التطبيق (Business Error).
///
/// هي التي تنتقل إلى:
///
/// UseCase
/// Cubit
/// UI
///
/// لذلك لا يجب أن يعرف Cubit أي شيء عن Dio.
///
/// Cubit يتعامل مع Failure فقط.
///
/// ============================================================================
///
/// رحلة الخطأ داخل التطبيق:
///
/// API
///      │
///      ▼
/// DioException
///      │
///      ▼
/// ServerException
///      │
///      ▼
/// Repository
///      │
///      ▼
/// ServerFailure
///      │
///      ▼
/// Cubit
///      │
///      ▼
/// UI
///
/// ============================================================================

/// ============================================================================
///
/// Failure
///
/// ============================================================================
///
/// الأب لجميع Failures داخل المشروع.
///
/// جميع أنواع Failure سترث منه.
///
/// ============================================================================

abstract class Failure {

  /// --------------------------------------------------------------------------
  /// message
  /// --------------------------------------------------------------------------
  ///
  /// الرسالة التي سيتم عرضها للمستخدم
  /// أو استخدامها داخل Cubit.
  ///
  final String message;

  /// Constructor
  ///
  const Failure(this.message);

  @override
  String toString() => message;
}

/// ============================================================================
///
/// ServerFailure
///
/// ============================================================================
///
/// يستخدم عندما يحدث خطأ من السيرفر.
///
/// أمثلة:
///
/// 500
/// 404
/// 401
///
/// ============================================================================

class ServerFailure extends Failure {

  const ServerFailure(super.message);

}

/// ============================================================================
///
/// CacheFailure
///
/// ============================================================================
///
/// يحدث عند فشل التخزين المحلي.
///
/// مثال:
///
/// SharedPreferences
/// Hive
/// SQLite
///
/// ============================================================================

class CacheFailure extends Failure {

  const CacheFailure(super.message);

}

/// ============================================================================
///
/// NetworkFailure
///
/// ============================================================================
///
/// يحدث عند عدم وجود اتصال بالإنترنت.
///
/// ============================================================================

class NetworkFailure extends Failure {

  const NetworkFailure(super.message);

}

/// ============================================================================
///
/// TimeoutFailure
///
/// ============================================================================
///
/// يحدث عند انتهاء مهلة الاتصال.
///
/// ============================================================================

class TimeoutFailure extends Failure {

  const TimeoutFailure(super.message);

}

/// ============================================================================
///
/// UnauthorizedFailure
///
/// ============================================================================
///
/// يمثل خطأ 401.
///
/// ============================================================================

class UnauthorizedFailure extends Failure {

  const UnauthorizedFailure(super.message);

}

/// ============================================================================
///
/// ForbiddenFailure
///
/// ============================================================================
///
/// يمثل خطأ 403.
///
/// ============================================================================

class ForbiddenFailure extends Failure {

  const ForbiddenFailure(super.message);

}

/// ============================================================================
///
/// NotFoundFailure
///
/// ============================================================================
///
/// يمثل خطأ 404.
///
/// مثل:
///
/// المدينة غير موجودة.
///
/// ============================================================================

class NotFoundFailure extends Failure {

  const NotFoundFailure(super.message);

}

/// ============================================================================
///
/// ValidationFailure
///
/// ============================================================================
///
/// يستخدم عندما تكون البيانات المدخلة
/// من المستخدم غير صحيحة.
///
/// مثال:
///
/// اسم المدينة فارغ.
///
/// ============================================================================

class ValidationFailure extends Failure {

  const ValidationFailure(super.message);

}

/// ============================================================================
///
/// UnknownFailure
///
/// ============================================================================
///
/// يستخدم عندما لا نستطيع
/// تحديد نوع الخطأ.
///
/// يعتبر آخر Failure يمكن إرجاعه.
///
/// ============================================================================

class UnknownFailure extends Failure {

  const UnknownFailure(super.message);

}
Dart

File: error_messages.dart

Path

lib/core/errors/error_messages.dart




/// ============================================================================
/// File : error_messages.dart
/// Path : lib/core/errors/error_messages.dart
/// ============================================================================
///
/// هذا الملف يحتوي جميع رسائل الأخطاء الخاصة بالتطبيق.
///
/// لماذا أنشأنا هذا الملف؟
///
/// لأننا لا نريد كتابة رسائل الأخطاء داخل:
///
/// ❌ Cubit
/// ❌ Repository
/// ❌ DataSource
/// ❌ UI
///
/// لو احتجنا تغيير صياغة رسالة واحدة
/// سنعدلها هنا فقط.
///
/// ============================================================================
///
/// ملاحظة مهمة:
///
/// حالياً الرسائل باللغة العربية.
///
/// لاحقاً عند إضافة Localization
/// لن نستخدم هذه الرسائل مباشرة.
///
/// وإنما سنربطها بملفات ARB.
///
/// ============================================================================

class ErrorMessages {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// يمنع إنشاء Object.
  ///
  /// الاستخدام الصحيح:
  ///
  /// ErrorMessages.serverError
  ///
  ErrorMessages._();

  //===========================================================================
  // General Errors
  //===========================================================================

  /// رسالة عامة.
  ///
  static const String unknownError =
      'حدث خطأ غير متوقع، يرجى المحاولة مرة أخرى.';

  /// خطأ غير معروف.
  ///
  static const String unexpectedError =
      'حدث خطأ غير معروف.';

  //===========================================================================
  // Network
  //===========================================================================

  /// لا يوجد اتصال بالإنترنت.
  ///
  static const String noInternetConnection =
      'لا يوجد اتصال بالإنترنت.';

  /// انتهت مهلة الاتصال.
  ///
  static const String timeout =
      'انتهت مهلة الاتصال بالخادم.';

  //===========================================================================
  // Server
  //===========================================================================

  /// خطأ داخلي في السيرفر.
  ///
  static const String serverError =
      'حدث خطأ في الخادم، حاول لاحقاً.';

  /// الخدمة غير متوفرة.
  ///
  static const String serviceUnavailable =
      'الخدمة غير متوفرة حالياً.';

  /// الطلب غير صحيح.
  ///
  static const String badRequest =
      'الطلب المرسل غير صحيح.';

  /// غير مصرح.
  ///
  static const String unauthorized =
      'غير مصرح لك بالوصول.';

  /// ممنوع.
  ///
  static const String forbidden =
      'ليس لديك صلاحية للوصول إلى هذا المورد.';

  /// غير موجود.
  ///
  static const String notFound =
      'العنصر المطلوب غير موجود.';

  //===========================================================================
  // Cache
  //===========================================================================

  /// خطأ في التخزين المحلي.
  ///
  static const String cacheError =
      'تعذر الوصول إلى البيانات المحلية.';

  /// لا توجد بيانات محفوظة.
  ///
  static const String emptyCache =
      'لا توجد بيانات محفوظة.';

  //===========================================================================
  // Weather
  //===========================================================================

  /// المدينة غير موجودة.
  ///
  static const String cityNotFound =
      'المدينة غير موجودة.';

  /// لا توجد بيانات طقس.
  ///
  static const String noWeatherData =
      'لا توجد بيانات طقس متاحة.';

  /// تعذر تحميل بيانات الطقس.
  ///
  static const String weatherLoadingFailed =
      'تعذر تحميل بيانات الطقس.';

  //===========================================================================
  // Location
  //===========================================================================

  /// خدمات الموقع متوقفة.
  ///
  static const String locationServiceDisabled =
      'خدمة الموقع غير مفعلة.';

  /// صلاحية الموقع مرفوضة.
  ///
  static const String locationPermissionDenied =
      'تم رفض صلاحية الموقع.';

  /// رفض دائم.
  ///
  static const String locationPermissionForeverDenied =
      'تم رفض صلاحية الموقع بشكل دائم.';

  /// تعذر تحديد الموقع.
  ///
  static const String locationUnavailable =
      'تعذر تحديد موقعك الحالي.';

  //===========================================================================
  // Validation
  //===========================================================================

  /// اسم المدينة فارغ.
  ///
  static const String emptyCity =
      'يرجى إدخال اسم المدينة.';

  /// اسم مدينة قصير جداً.
  ///
  static const String invalidCity =
      'اسم المدينة غير صالح.';

  //===========================================================================
  // Storage
  //===========================================================================

  /// تعذر حفظ البيانات.
  ///
  static const String saveFailed =
      'تعذر حفظ البيانات.';

  /// تعذر قراءة البيانات.
  ///
  static const String readFailed =
      'تعذر قراءة البيانات.';

  //===========================================================================
  // API
  //===========================================================================

  /// مفتاح API غير صحيح.
  ///
  static const String invalidApiKey =
      'مفتاح API غير صالح.';

  /// تم تجاوز الحد المسموح.
  ///
  static const String apiLimitExceeded =
      'تم تجاوز الحد المسموح من الطلبات.';
}
Dart

ملاحظة معمارية: في هذه المرحلة أصبح لدينا أساس طبقة Core → Errors بالكامل. والخطوة المنطقية التالية ستكون Core → Services، وسنبدأ بملف:

lib/core/services/location_service.dart

وليس StorageService، لأن تطبيق الطقس يعتمد أولًا على الحصول على موقع المستخدم قبل التخزين المحلي.

File: location_service.dart

Path

lib/core/services/location_service.dart




/// ============================================================================
/// File : location_service.dart
/// Path : lib/core/services/location_service.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن جميع العمليات المتعلقة بالموقع الجغرافي.
///
/// انتبه...
///
/// هذا الملف لا يعرف أي شيء عن:
///
/// ❌ API
/// ❌ Weather
/// ❌ Repository
/// ❌ Cubit
///
/// مسؤوليته الوحيدة هي التعامل مع GPS وصلاحيات الموقع.
///
/// ============================================================================

import 'package:geolocator/geolocator.dart';

import '../errors/error_messages.dart';
import '../errors/exceptions.dart';

/// ============================================================================
///
/// LocationService
///
/// ============================================================================
///
/// Interface
///
/// لماذا أنشأنا Interface؟
///
/// حتى لا تعتمد بقية طبقات المشروع
/// على geolocator مباشرة.
///
/// مستقبلاً لو غيرنا المكتبة
/// لن نعدل أي كود خارج هذا الملف.
///
/// ============================================================================

abstract class LocationService {

  /// --------------------------------------------------------------------------
  /// getCurrentLocation()
  /// --------------------------------------------------------------------------
  ///
  /// تعيد موقع المستخدم الحالي.
  ///
  /// Position يحتوي:
  ///
  /// latitude
  /// longitude
  /// altitude
  /// speed
  /// accuracy
  ///
  Future<Position> getCurrentLocation();

  /// --------------------------------------------------------------------------
  /// isLocationEnabled()
  /// --------------------------------------------------------------------------
  ///
  /// هل خدمة الموقع مفعلة؟
  ///
  Future<bool> isLocationEnabled();

  /// --------------------------------------------------------------------------
  /// hasPermission()
  /// --------------------------------------------------------------------------
  ///
  /// هل التطبيق يمتلك صلاحية الوصول للموقع؟
  ///
  Future<bool> hasPermission();
}

/// ============================================================================
///
/// LocationServiceImpl
///
/// ============================================================================
///
/// التطبيق الحقيقي للـ Interface.
///
/// ============================================================================

class LocationServiceImpl implements LocationService {

  /// --------------------------------------------------------------------------
  /// Constructor
  /// --------------------------------------------------------------------------
  ///
  /// حالياً فارغ.
  ///
  /// مستقبلاً يمكن حقن Dependencies هنا.
  ///
  LocationServiceImpl();

  //===========================================================================
  // isLocationEnabled()
  //===========================================================================

  @override
  Future<bool> isLocationEnabled() async {

    /// يسأل نظام التشغيل:
    ///
    /// هل خدمة GPS مفعلة؟
    ///
    return await Geolocator.isLocationServiceEnabled();
  }

  //===========================================================================
  // hasPermission()
  //===========================================================================

  @override
  Future<bool> hasPermission() async {

    /// ------------------------------------------------------------------------
    /// checkPermission()
    /// ------------------------------------------------------------------------
    ///
    /// تعيد حالة الصلاحية الحالية.
    ///
    final permission =
        await Geolocator.checkPermission();

    /// إذا كانت الصلاحية موجودة.
    ///
    if (permission == LocationPermission.always ||
        permission == LocationPermission.whileInUse) {

      return true;
    }

    /// إذا لم تكن موجودة
    /// نطلبها من المستخدم.
    ///
    final requestedPermission =
        await Geolocator.requestPermission();

    /// نتحقق مرة أخرى.
    ///
    return requestedPermission == LocationPermission.always ||
        requestedPermission == LocationPermission.whileInUse;
  }

  //===========================================================================
  // getCurrentLocation()
  //===========================================================================

  @override
  Future<Position> getCurrentLocation() async {

    /// ------------------------------------------------------------------------
    /// أولاً
    ///
    /// هل خدمة الموقع مفعلة؟
    ///
    final enabled = await isLocationEnabled();

    if (!enabled) {

      /// نرمي Exception.
      ///
      /// Repository سيقوم بتحويله لاحقاً
      /// إلى Failure.
      ///
      throw NetworkException(
        ErrorMessages.locationServiceDisabled,
      );
    }

    /// ------------------------------------------------------------------------
    /// هل لدينا صلاحية؟
    ///
    final permissionGranted =
        await hasPermission();

    if (!permissionGranted) {

      throw NetworkException(
        ErrorMessages.locationPermissionDenied,
      );
    }

    /// ------------------------------------------------------------------------
    /// الحصول على الموقع الحالي.
    ///
    /// desiredAccuracy
    ///
    /// تحدد دقة الموقع المطلوبة.
    ///
    /// كلما زادت الدقة
    /// زاد استهلاك البطارية.
    ///
    return await Geolocator.getCurrentPosition(

      desiredAccuracy: LocationAccuracy.high,

      /// يحدد أقصى مدة انتظار.
      ///
      timeLimit: const Duration(

        seconds: 15,
      ),
    );
  }
}
Dart

ملاحظة معمارية: هذا الملف جيد كبداية، لكن في مشروع Production سأقوم لاحقًا بإعادة هيكلته قليلًا:

  • سأفصل إدارة Permissions عن الحصول على Position.
  • سأضيف getLastKnownPosition().
  • سأضيف openLocationSettings().
  • سأضيف openAppSettings().
  • وسأستبدل رمي NetworkException بـ LocationException مخصص لأن خطأ الموقع ليس خطأ شبكة. وهذا سيكون تصميمًا أنظف وأكثر احترافية.

File: storage_service.dart

Path

lib/core/services/storage_service.dart
/// ============================================================================
/// File : storage_service.dart
/// Path : lib/core/services/storage_service.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن التخزين المحلي فقط.
///
/// انتبه جيداً...
///
/// هذا الملف لا يعرف:
///
/// ❌ الطقس
/// ❌ المدينة
/// ❌ المستخدم
/// ❌ API
///
/// هو مجرد طبقة تغلف SharedPreferences.
///
/// لماذا؟
///
/// لأننا لو استبدلنا SharedPreferences مستقبلاً بـ:
///
/// Hive
/// SQLite
/// Isar
///
/// فلن نغير أي كود خارج هذا الملف.
///
/// ============================================================================

import 'package:shared_preferences/shared_preferences.dart';

/// ============================================================================
///
/// StorageService
///
/// ============================================================================
///
/// Interface.
///
/// بقية المشروع ستتعامل مع هذا الـ Interface فقط.
///
/// ============================================================================

abstract class StorageService {

  /// حفظ نص.
  Future<void> saveString(
    String key,
    String value,
  );

  /// قراءة نص.
  Future<String?> getString(
    String key,
  );

  /// حفظ رقم صحيح.
  Future<void> saveInt(
    String key,
    int value,
  );

  /// قراءة رقم صحيح.
  Future<int?> getInt(
    String key,
  );

  /// حفظ رقم عشري.
  Future<void> saveDouble(
    String key,
    double value,
  );

  /// قراءة رقم عشري.
  Future<double?> getDouble(
    String key,
  );

  /// حفظ Boolean.
  Future<void> saveBool(
    String key,
    bool value,
  );

  /// قراءة Boolean.
  Future<bool?> getBool(
    String key,
  );

  /// حذف قيمة.
  Future<void> remove(
    String key,
  );

  /// حذف جميع البيانات.
  Future<void> clear();
}

/// ============================================================================
///
/// StorageServiceImpl
///
/// ============================================================================
///
/// التطبيق الحقيقي.
///
/// ============================================================================

class StorageServiceImpl implements StorageService {

  /// --------------------------------------------------------------------------
  /// SharedPreferences
  /// --------------------------------------------------------------------------
  ///
  /// يمثل قاعدة البيانات الصغيرة
  /// الخاصة بالتطبيق.
  ///
  final SharedPreferences preferences;

  /// Constructor
  ///
  /// سيتم حقنه لاحقاً باستخدام GetIt.
  ///
  StorageServiceImpl(
    this.preferences,
  );

  //===========================================================================
  // String
  //===========================================================================

  @override
  Future<void> saveString(
    String key,
    String value,
  ) async {

    /// setString()
    ///
    /// تحفظ النص داخل SharedPreferences.
    ///
    await preferences.setString(
      key,
      value,
    );
  }

  @override
  Future<String?> getString(
    String key,
  ) async {

    /// getString()
    ///
    /// تعيد النص المحفوظ.
    ///
    /// إذا لم يكن موجوداً
    /// ستعيد null.
    ///
    return preferences.getString(key);
  }

  //===========================================================================
  // Integer
  //===========================================================================

  @override
  Future<void> saveInt(
    String key,
    int value,
  ) async {

    await preferences.setInt(
      key,
      value,
    );
  }

  @override
  Future<int?> getInt(
    String key,
  ) async {

    return preferences.getInt(key);
  }

  //===========================================================================
  // Double
  //===========================================================================

  @override
  Future<void> saveDouble(
    String key,
    double value,
  ) async {

    await preferences.setDouble(
      key,
      value,
    );
  }

  @override
  Future<double?> getDouble(
    String key,
  ) async {

    return preferences.getDouble(key);
  }

  //===========================================================================
  // Boolean
  //===========================================================================

  @override
  Future<void> saveBool(
    String key,
    bool value,
  ) async {

    await preferences.setBool(
      key,
      value,
    );
  }

  @override
  Future<bool?> getBool(
    String key,
  ) async {

    return preferences.getBool(key);
  }

  //===========================================================================
  // Remove
  //===========================================================================

  @override
  Future<void> remove(
    String key,
  ) async {

    /// حذف قيمة واحدة فقط.
    ///
    await preferences.remove(key);
  }

  //===========================================================================
  // Clear
  //===========================================================================

  @override
  Future<void> clear() async {

    /// حذف جميع البيانات المخزنة.
    ///
    /// يستخدم غالباً عند:
    ///
    /// - تسجيل الخروج.
    /// - إعادة ضبط التطبيق.
    ///
    await preferences.clear();
  }
}
Dart

ملاحظة احترافية: هذا الملف جيد كبداية، لكن في مشروع Production لن أجعل بقية المشروع تتعامل مع String key مباشرة، بل سأضيف في الخطوة القادمة Storage Keys و Storage Manager بحيث يصبح الحفظ هكذا:

await storage.saveLastCity("Damascus");
await storage.saveThemeMode(ThemeMode.dark);

بدلاً من:

await storage.saveString(AppConstants.lastCityKey, "Damascus");

وهذا يمنع أخطاء كتابة المفاتيح ويجعل الكود أكثر نظافة.

File: service_locator.dart

Path

lib/core/services/service_locator.dart
/// ============================================================================
/// File : service_locator.dart
/// Path : lib/core/services/service_locator.dart
/// ============================================================================
///
/// هذا الملف يعتبر قلب Dependency Injection في المشروع.
///
/// جميع الكلاسات التي تحتاجها بقية طبقات التطبيق
/// يتم تسجيلها هنا مرة واحدة.
///
/// بعد ذلك نستطيع الحصول عليها من أي مكان.
///
/// بدلاً من كتابة:
///
/// final api = ApiClient();
///
/// في كل مرة.
///
/// سنكتب:
///
/// final api = sl<ApiClient>();
///
/// ============================================================================

import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get_it/get_it.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../network/api_client.dart';
import '../network/network_info.dart';
import 'location_service.dart';
import 'storage_service.dart';

/// ============================================================================
///
/// GetIt Instance
///
/// ============================================================================
///
/// sl اختصار لـ:
///
/// Service Locator.
///
/// يمكن الوصول إليه من أي مكان داخل المشروع.
///
/// مثال:
///
/// sl<ApiClient>()
///
/// أو
///
/// sl<StorageService>()
///
/// ============================================================================

final GetIt sl = GetIt.instance;

/// ============================================================================
///
/// init()
///
/// ============================================================================
///
/// يتم استدعاء هذه الدالة مرة واحدة فقط
/// داخل main().
///
/// جميع Dependencies يتم تسجيلها هنا.
///
/// ============================================================================

Future<void> init() async {

  //===========================================================================
  // External Packages
  //===========================================================================

  /// --------------------------------------------------------------------------
  /// SharedPreferences
  /// --------------------------------------------------------------------------
  ///
  /// أولاً ننشئ SharedPreferences.
  ///
  /// لأنه يحتاج await.
  ///
  final sharedPreferences =
      await SharedPreferences.getInstance();

  /// تسجيل SharedPreferences.
  ///
  /// Singleton
  ///
  /// أي يتم إنشاء نسخة واحدة فقط
  /// طوال عمر التطبيق.
  ///
  sl.registerLazySingleton<SharedPreferences>(

    () => sharedPreferences,

  );

  /// --------------------------------------------------------------------------
  /// Connectivity
  /// --------------------------------------------------------------------------
  ///
  /// مسؤول عن معرفة حالة الاتصال.
  ///
  sl.registerLazySingleton<Connectivity>(

    Connectivity.new,

  );

  //===========================================================================
  // Core
  //===========================================================================

  /// --------------------------------------------------------------------------
  /// ApiClient
  /// --------------------------------------------------------------------------
  ///
  /// جميع Requests ستمر من خلاله.
  ///
  sl.registerLazySingleton<ApiClient>(

    ApiClient.new,

  );

  /// --------------------------------------------------------------------------
  /// NetworkInfo
  /// --------------------------------------------------------------------------
  ///
  /// يعتمد على Connectivity.
  ///
  /// sl()
  ///
  /// تعني:
  ///
  /// أعطني النسخة المسجلة مسبقاً.
  ///
  sl.registerLazySingleton<NetworkInfo>(

    () => NetworkInfoImpl(

      sl<Connectivity>(),

    ),

  );

  /// --------------------------------------------------------------------------
  /// StorageService
  /// --------------------------------------------------------------------------
  ///
  /// يعتمد على SharedPreferences.
  ///
  sl.registerLazySingleton<StorageService>(

    () => StorageServiceImpl(

      sl<SharedPreferences>(),

    ),

  );

  /// --------------------------------------------------------------------------
  /// LocationService
  /// --------------------------------------------------------------------------
  ///
  /// لا يحتاج أي Dependency حالياً.
  ///
  sl.registerLazySingleton<LocationService>(

    LocationServiceImpl.new,

  );

  //===========================================================================
  // لاحقاً سيتم تسجيل:
  //===========================================================================
  //
  // Remote Data Sources
  //
  // Local Data Sources
  //
  // Repositories
  //
  // UseCases
  //
  // Cubits
  //
  //===========================================================================
}
Dart

File: app_router.dart

Path

lib/app/router/app_router.dart




/// ============================================================================
/// File : app_router.dart
/// Path : lib/app/router/app_router.dart
/// ============================================================================
///
/// هذا الملف مسؤول عن التنقل (Navigation) داخل التطبيق.
///
/// لاحظ...
///
/// هذا الملف لا يحتوي أي Widget.
///
/// ولا يعرف شيئاً عن API.
///
/// ولا يعرف Cubit.
///
/// مسؤوليته الوحيدة هي:
///
/// "كيف ينتقل المستخدم بين الشاشات؟"
///
/// ============================================================================

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import '../../features/weather/presentation/pages/home_page.dart';

/// ============================================================================
///
/// AppRouter
///
/// ============================================================================
///
/// يحتوي جميع Routes الخاصة بالتطبيق.
///
/// لاحقاً عندما يزداد حجم المشروع
/// سنضيف:
///
/// Search Page
///
/// Settings Page
///
/// Splash Page
///
/// وغيرها.
///
/// ============================================================================

class AppRouter {

  /// --------------------------------------------------------------------------
  /// Private Constructor
  /// --------------------------------------------------------------------------
  ///
  /// لا نحتاج إنشاء Object.
  ///
  AppRouter._();

  //===========================================================================
  // Route Names
  //===========================================================================

  /// أسماء الـ Routes.
  ///
  /// نستخدمها بدلاً من كتابة Strings
  /// داخل المشروع.
  ///
  /// بدلاً من:
  ///
  /// "/home"
  ///
  /// نكتب:
  ///
  /// AppRouter.home
  ///
  static const String home = '/';

  static const String search = '/search';

  static const String settings = '/settings';

  //===========================================================================
  // GoRouter
  //===========================================================================

  /// الراوتر الرئيسي للتطبيق.
  ///
  /// سيتم تمريره لاحقاً إلى:
  ///
  /// MaterialApp.router()
  ///
  static final GoRouter router = GoRouter(

    //=======================================================================
    // Initial Location
    //=======================================================================

    /// أول Route يتم فتحه.
    ///
    initialLocation: home,

    //=======================================================================
    // Debug
    //=======================================================================

    /// يطبع جميع عمليات التنقل داخل Console.
    ///
    debugLogDiagnostics: true,

    //=======================================================================
    // Routes
    //=======================================================================

    routes: [

      /// --------------------------------------------------------------------
      /// Home
      /// --------------------------------------------------------------------
      ///
      /// الصفحة الرئيسية.
      ///
      GoRoute(

        /// اسم الـ Route.
        ///
        name: 'home',

        /// الرابط.
        ///
        path: home,

        /// الصفحة التي سيتم فتحها.
        ///
        builder: (

          BuildContext context,

          GoRouterState state,

        ) {

          return const HomePage();

        },
      ),

      //======================================================================
      // لاحقاً
      //======================================================================
      //
      // GoRoute(
      //   name: 'search',
      //   path: search,
      //   builder: ...
      // )
      //
      // GoRoute(
      //   name: 'settings',
      //   path: settings,
      //   builder: ...
      // )
      //
      // GoRoute(
      //   name: 'forecast',
      //   path: '/forecast',
      //   builder: ...
      // )
      //
      //======================================================================
    ],

    //=======================================================================
    // Error Page
    //=======================================================================

    /// إذا حاول المستخدم فتح Route غير موجود
    /// سيتم عرض هذه الصفحة.
    ///
    errorBuilder: (

      context,

      state,

    ) {

      return Scaffold(

        appBar: AppBar(

          title: const Text(

            '404',

          ),
        ),

        body: const Center(

          child: Text(

            'Page Not Found',

          ),
        ),
      );
    },
  );

  //===========================================================================
  // Helper Methods
  //===========================================================================

  /// الانتقال إلى صفحة.
  ///
  /// تستبدل الصفحة الحالية.
  ///
  static void go(

    BuildContext context,

    String route,

  ) {

    context.go(route);

  }

  /// فتح صفحة جديدة.
  ///
  /// تبقى الصفحة السابقة داخل Stack.
  ///
  static Future<T?> push<T>(

    BuildContext context,

    String route,

  ) {

    return context.push<T>(route);

  }

  /// الرجوع للصفحة السابقة.
  ///
  static void pop(

    BuildContext context,

  ) {

    context.pop();

  }
}
Dart

ملاحظة معمارية مهمة: لو كنا نبني مشروعًا داخل شركة فلن أنتقل الآن إلى HomePage.

الترتيب الصحيح بعد إنهاء Core هو بدء Feature Weather من Domain Layer أولًا:

  1. weather.dart (Entity)
  2. forecast.dart (Entity)
  3. weather_repository.dart (Interface)
  4. get_current_weather.dart
  5. get_forecast.dart

ثم ننتقل إلى Data Layer، وبعدها فقط إلى Presentation. هذا هو التسلسل الحقيقي في Clean Architecture.