/// ===============================================================
/// 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,
),
),
),
);
}
}DartFile: 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);
}DartFile: 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,
),
);
}DartFile: 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),
),
),
),
);
}DartFile: 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 = '';
}DartFile: 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);
}DartFile: 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);
}DartFile: 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,
);
}
}DartFile: 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;
}
}DartFile: 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);
}DartFile: 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);
}DartFile: 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
//
//===========================================================================
}DartFile: 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 أولًا:
weather.dart(Entity)forecast.dart(Entity)weather_repository.dart(Interface)get_current_weather.dartget_forecast.dart
ثم ننتقل إلى Data Layer، وبعدها فقط إلى Presentation. هذا هو التسلسل الحقيقي في Clean Architecture.