File: weather.dart
Path
lib/features/weather/domain/entities/weather.dart
/// ============================================================================
/// File : weather.dart
/// Path : lib/features/weather/domain/entities/weather.dart
/// ============================================================================
///
/// هذا الملف يمثل أهم كلاس داخل Feature الطقس.
///
/// يسمى:
///
/// Entity
///
/// ============================================================================
///
/// ما هو الـ Entity ؟
///
/// الـ Entity يمثل البيانات التي يفهمها التطبيق.
///
/// ولا يهتم:
///
/// ❌ من أين جاءت البيانات.
///
/// ❌ هل جاءت من API.
///
/// ❌ هل جاءت من قاعدة بيانات.
///
/// ❌ هل جاءت من Cache.
///
/// هو يعرف فقط شكل البيانات.
///
/// ============================================================================
///
/// مثال
///
/// API قد يعيد:
///
/// {
/// "temp_c":25,
/// "humidity":70
/// }
///
/// بينما قاعدة البيانات قد تخزن:
///
/// temperature
///
/// وفي النهاية الـ UI يريد:
///
/// weather.temperature
///
/// لذلك جميع الطبقات تتعامل مع Entity فقط.
///
/// ============================================================================
import 'package:equatable/equatable.dart';
import 'forecast.dart';
/// ============================================================================
///
/// Weather
///
/// ============================================================================
///
/// يمثل حالة الطقس الكاملة.
///
/// لاحظ:
///
/// هذا الكلاس لا يحتوي:
///
/// ❌ fromJson()
///
/// ❌ toJson()
///
/// ❌ API
///
/// لأن هذه مسؤولية Model وليس Entity.
///
/// ============================================================================
class Weather extends Equatable {
//===========================================================================
// City
//===========================================================================
/// اسم المدينة.
///
final String city;
/// اسم الدولة.
///
final String country;
//===========================================================================
// Temperature
//===========================================================================
/// درجة الحرارة الحالية.
///
final double temperature;
/// أعلى درجة حرارة.
///
final double maxTemperature;
/// أقل درجة حرارة.
///
final double minTemperature;
//===========================================================================
// Description
//===========================================================================
/// وصف حالة الطقس.
///
/// مثال:
///
/// Sunny
/// Rain
/// Cloudy
///
final String description;
/// رابط أو اسم أيقونة الطقس.
///
final String icon;
//===========================================================================
// Details
//===========================================================================
/// الرطوبة.
///
final int humidity;
/// الضغط الجوي.
///
final double pressure;
/// سرعة الرياح.
///
final double windSpeed;
/// مدى الرؤية.
///
final double visibility;
//===========================================================================
// Astronomy
//===========================================================================
/// وقت الشروق.
///
final DateTime sunrise;
/// وقت الغروب.
///
final DateTime sunset;
//===========================================================================
// Date
//===========================================================================
/// تاريخ القراءة.
///
final DateTime date;
//===========================================================================
// Forecast
//===========================================================================
/// توقعات الأيام القادمة.
///
final List<Forecast> forecast;
//===========================================================================
// Constructor
//===========================================================================
/// جميع البيانات مطلوبة.
///
/// required
///
/// حتى لا يوجد Weather ناقص.
///
const Weather({
required this.city,
required this.country,
required this.temperature,
required this.maxTemperature,
required this.minTemperature,
required this.description,
required this.icon,
required this.humidity,
required this.pressure,
required this.windSpeed,
required this.visibility,
required this.sunrise,
required this.sunset,
required this.date,
required this.forecast,
});
//===========================================================================
// Equatable
//===========================================================================
/// props
///
/// Equatable يستخدم هذه القائمة
/// لمقارنة الكائنات.
///
/// بدون Equatable
/// فإن Dart يقارن العناوين داخل الذاكرة.
///
/// أما مع Equatable
/// فإنه يقارن القيم نفسها.
///
/// وهذا مهم جداً مع Cubit و Bloc.
///
@override
List<Object> get props => [
city,
country,
temperature,
maxTemperature,
minTemperature,
description,
icon,
humidity,
pressure,
windSpeed,
visibility,
sunrise,
sunset,
date,
forecast,
];
}Dartملاحظة احترافية: في مشروع Production لا أحب وضع List<Forecast> داخل Weather لأن ذلك يخلط بين Current Weather و Forecast في كيان واحد. الأفضل هو إنشاء كيان ثالث مثل WeatherResponse أو WeatherData يحتوي:
Weather currentWeatherList<Forecast> forecast
لكن في مشروعنا التعليمي سنبقيهما معًا لتبسيط البنية في البداية، ثم سنعيد هيكلتها لاحقًا عندما نصل إلى مرحلة الـ Models والـ Repository.
File: forecast.dart
Path
lib/features/weather/domain/entities/forecast.dart
/// ============================================================================
/// File : forecast.dart
/// Path : lib/features/weather/domain/entities/forecast.dart
/// ============================================================================
///
/// هذا الملف يمثل توقعات الطقس ليوم واحد.
///
/// انتبه...
///
/// Forecast ليس Weather.
///
/// Weather
/// -------
/// يمثل حالة الطقس الحالية.
///
/// Forecast
/// --------
/// يمثل توقعات يوم واحد فقط.
///
/// لذلك سيكون لدينا List<Forecast>
/// تحتوي توقعات الأسبوع.
///
/// ============================================================================
import 'package:equatable/equatable.dart';
/// ============================================================================
///
/// Forecast
///
/// ============================================================================
///
/// Entity يمثل يوم واحد من توقعات الطقس.
///
/// لا يحتوي:
///
/// ❌ fromJson()
///
/// ❌ toJson()
///
/// ❌ API
///
/// لأنه Entity.
///
/// ============================================================================
class Forecast extends Equatable {
//===========================================================================
// Date
//===========================================================================
/// تاريخ هذا التوقع.
///
/// مثال:
///
/// 2026-07-08
///
final DateTime date;
//===========================================================================
// Day Name
//===========================================================================
/// اسم اليوم.
///
/// مثال:
///
/// السبت
/// الأحد
/// Monday
///
/// لاحقاً يمكن توليده من التاريخ
/// باستخدام intl.
///
/// وضعناه هنا لتسهيل الاستخدام.
///
final String day;
//===========================================================================
// Temperature
//===========================================================================
/// أعلى درجة حرارة.
///
final double maxTemperature;
/// أقل درجة حرارة.
///
final double minTemperature;
//===========================================================================
// Weather
//===========================================================================
/// وصف الحالة.
///
/// مثال:
///
/// Sunny
/// Rain
/// Cloudy
///
final String description;
/// أيقونة الحالة.
///
/// قد تكون:
///
/// URL
///
/// أو
///
/// Icon Code
///
/// حسب الـ API المستخدم.
///
final String icon;
//===========================================================================
// Chance Of Rain
//===========================================================================
/// احتمال هطول المطر.
///
/// قيمة من:
///
/// 0
///
/// إلى
///
/// 100
///
final int chanceOfRain;
//===========================================================================
// Constructor
//===========================================================================
/// جميع البيانات مطلوبة.
///
const Forecast({
required this.date,
required this.day,
required this.maxTemperature,
required this.minTemperature,
required this.description,
required this.icon,
required this.chanceOfRain,
});
//===========================================================================
// copyWith()
//===========================================================================
/// يستخدم لإنشاء نسخة جديدة
/// مع تعديل بعض القيم فقط.
///
/// مثال:
///
/// final newForecast =
/// forecast.copyWith(
/// chanceOfRain:80,
/// );
///
Forecast copyWith({
DateTime? date,
String? day,
double? maxTemperature,
double? minTemperature,
String? description,
String? icon,
int? chanceOfRain,
}) {
return Forecast(
date: date ?? this.date,
day: day ?? this.day,
maxTemperature:
maxTemperature ?? this.maxTemperature,
minTemperature:
minTemperature ?? this.minTemperature,
description:
description ?? this.description,
icon: icon ?? this.icon,
chanceOfRain:
chanceOfRain ?? this.chanceOfRain,
);
}
//===========================================================================
// Equatable
//===========================================================================
/// يسمح بمقارنة القيم
/// بدلاً من مقارنة عناوين الذاكرة.
///
@override
List<Object> get props => [
date,
day,
maxTemperature,
minTemperature,
description,
icon,
chanceOfRain,
];
}DartFile: weather_repository.dart
Path
lib/features/weather/domain/repositories/weather_repository.dart
/// ============================================================================
/// File : weather_repository.dart
/// Path :
/// lib/features/weather/domain/repositories/weather_repository.dart
/// ============================================================================
///
/// هذا الملف يعتبر من أهم ملفات Clean Architecture.
///
/// لأنه يمثل العقد (Contract)
/// بين Domain Layer و Data Layer.
///
/// انتبه جيداً...
///
/// هذا الملف لا يحتوي أي كود حقيقي.
///
/// لا يوجد:
///
/// ❌ Dio
/// ❌ API
/// ❌ SharedPreferences
/// ❌ JSON
///
/// يوجد فقط تعريف للدوال.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
/// ============================================================================
///
/// WeatherRepository
///
/// ============================================================================
///
/// Repository عبارة عن Interface.
///
/// لماذا؟
///
/// لأن UseCase يجب ألا يعرف
/// كيف يتم جلب البيانات.
///
/// هل جاءت من:
///
/// API ؟
///
/// Cache ؟
///
/// Database ؟
///
/// لا يهم.
///
/// هو فقط يطلب البيانات.
///
/// ============================================================================
abstract class WeatherRepository {
//===========================================================================
// Current Weather
//===========================================================================
/// --------------------------------------------------------------------------
/// getCurrentWeather()
/// --------------------------------------------------------------------------
///
/// تجلب حالة الطقس الحالية.
///
/// تستقبل:
///
/// city
///
/// مثال:
///
/// Damascus
///
/// أو
///
/// Aleppo
///
/// --------------------------------------------------------------------------
///
/// لماذا Future؟
///
/// لأن العملية تحتاج وقتاً.
///
/// سواء كانت:
///
/// API
///
/// أو
///
/// Database.
///
/// --------------------------------------------------------------------------
///
/// لماذا Either؟
///
/// حتى لا نستخدم:
///
/// try
/// catch
///
/// في جميع أنحاء المشروع.
///
/// Either تحتوي احتمالين فقط.
///
/// Left
///
/// Failure
///
/// Right
///
/// Weather
///
/// --------------------------------------------------------------------------
///
/// Left
///
/// تعني:
///
/// حدث خطأ.
///
/// Right
///
/// تعني:
///
/// نجحت العملية.
///
Future<Either<Failure, Weather>>
getCurrentWeather(
String city,
);
//===========================================================================
// Forecast
//===========================================================================
/// --------------------------------------------------------------------------
/// getForecast()
/// --------------------------------------------------------------------------
///
/// تجلب توقعات الطقس.
///
/// الأيام ستكون حسب القيمة
/// المرسلة إلى API.
///
Future<Either<Failure, Weather>>
getForecast(
String city,
);
//===========================================================================
// Current Location
//===========================================================================
/// --------------------------------------------------------------------------
/// getWeatherByLocation()
/// --------------------------------------------------------------------------
///
/// تجلب الطقس حسب
/// خط العرض والطول.
///
/// تستخدم عند
/// فتح التطبيق لأول مرة.
///
Future<Either<Failure, Weather>>
getWeatherByLocation(
double latitude,
double longitude,
);
//===========================================================================
// Cache
//===========================================================================
/// --------------------------------------------------------------------------
/// saveLastCity()
/// --------------------------------------------------------------------------
///
/// تحفظ آخر مدينة
/// اختارها المستخدم.
///
Future<Either<Failure, Unit>>
saveLastCity(
String city,
);
/// --------------------------------------------------------------------------
/// getLastCity()
/// --------------------------------------------------------------------------
///
/// تعيد آخر مدينة
/// محفوظة داخل الجهاز.
///
Future<Either<Failure, String>>
getLastCity();
//===========================================================================
// Cache Weather
//===========================================================================
/// --------------------------------------------------------------------------
/// saveLastWeather()
/// --------------------------------------------------------------------------
///
/// تحفظ آخر بيانات طقس.
///
/// تستخدم عند:
///
/// عدم وجود إنترنت.
///
Future<Either<Failure, Unit>>
saveLastWeather(
Weather weather,
);
/// --------------------------------------------------------------------------
/// getLastWeather()
/// --------------------------------------------------------------------------
///
/// تعيد آخر بيانات محفوظة.
///
Future<Either<Failure, Weather>>
getLastWeather();
}Dartملاحظة احترافية مهمة: الملف السابق الذي أنشأناه (Weather) يحتاج إلى تعديل معماري. في التطبيقات الاحترافية لا يُفضَّل أن يحتوي Weather على قائمة forecast. الأفضل أن يكون لدينا:
CurrentWeather(Entity)Forecast(Entity)WeatherRepositoryيعيدCurrentWeatherأوList<Forecast>كلٌ في دالته الخاصة.
لذلك عندما نصل إلى مرحلة الـ Models سأعيد هيكلة الـ Entities لتصبح مطابقة لمخرجات الـ API ومعايير Clean Architecture المستخدمة في المشاريع الكبيرة، وهذا سيجعل الـ Repository أنظف وأكثر مرونة.
File: get_current_weather.dart
Path
lib/features/weather/domain/usecases/get_current_weather.dart
/// ============================================================================
/// File : get_current_weather.dart
/// Path :
/// lib/features/weather/domain/usecases/get_current_weather.dart
/// ============================================================================
///
/// هذا الملف يمثل أول UseCase داخل المشروع.
///
/// انتبه...
///
/// الـ UseCase لا يعرف:
///
/// ❌ Dio
/// ❌ API
/// ❌ JSON
/// ❌ Flutter
/// ❌ Cubit
///
/// هو يعرف شيئاً واحداً فقط.
///
/// "أريد تنفيذ عملية واحدة."
///
/// وهذه العملية هنا هي:
///
/// جلب حالة الطقس الحالية.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// GetCurrentWeather
///
/// ============================================================================
///
/// كل UseCase يمثل عملية واحدة فقط.
///
/// لذلك اسمه يبدأ بفعل.
///
/// Get
///
/// وليس:
///
/// WeatherUseCase
///
/// لأن اسمه يجب أن يصف العملية.
///
/// ============================================================================
class GetCurrentWeather {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لاحظ...
///
/// لا نعتمد على:
///
/// WeatherRepositoryImpl
///
/// وإنما على Interface فقط.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// يتم حقن Repository.
///
/// Dependency Injection.
///
GetCurrentWeather(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// --------------------------------------------------------------------------
/// لماذا اسم الدالة call ؟
/// --------------------------------------------------------------------------
///
/// Dart يسمح لأي Class يحتوي:
///
/// call()
///
/// أن يتم استدعاؤه
/// وكأنه Function.
///
/// مثال:
///
/// final useCase =
/// GetCurrentWeather(repository);
///
/// ثم:
///
/// await useCase("Damascus");
///
/// بدلاً من:
///
/// await useCase.call("Damascus");
///
/// لذلك أغلب مشاريع Flutter
/// تستخدم call().
///
/// --------------------------------------------------------------------------
///
/// Future
///
/// لأن العملية غير متزامنة.
///
/// Either
///
/// تعيد:
///
/// Failure
///
/// أو
///
/// Weather.
///
Future<Either<Failure, Weather>> call(
String city,
) async {
/// لا يوجد أي منطق هنا.
///
/// لماذا؟
///
/// لأن Repository هو المسؤول
/// عن معرفة مصدر البيانات.
///
/// UseCase يطلب فقط تنفيذ العملية.
///
return await repository.getCurrentWeather(
city,
);
}
}DartFile: get_forecast.dart
Path
lib/features/weather/domain/usecases/get_forecast.dart
/// ============================================================================
/// File : get_forecast.dart
/// Path :
/// lib/features/weather/domain/usecases/get_forecast.dart
/// ============================================================================
///
/// هذا الملف يمثل UseCase مسؤول عن جلب توقعات الطقس.
///
/// لاحظ...
///
/// كل UseCase داخل المشروع يجب أن ينفذ عملية واحدة فقط.
///
/// لذلك:
///
/// GetCurrentWeather
///
/// مسؤول عن:
///
/// الطقس الحالي.
///
/// بينما:
///
/// GetForecast
///
/// مسؤول عن:
///
/// توقعات الأيام القادمة.
///
/// لا يجوز دمج العمليتين داخل UseCase واحد.
///
/// لأن ذلك يخالف:
///
/// Single Responsibility Principle.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// GetForecast
///
/// ============================================================================
///
/// يمثل عملية واحدة فقط.
///
/// وهى:
///
/// جلب توقعات الطقس.
///
/// ============================================================================
class GetForecast {
//===========================================================================
// Repository
//===========================================================================
/// يعتمد على Interface فقط.
///
/// لا يعرف:
///
/// WeatherRepositoryImpl
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// Dependency Injection.
///
GetForecast(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// يتم استدعاؤه بهذه الطريقة:
///
/// final result =
/// await getForecast("Damascus");
///
/// وليس:
///
/// getForecast.call(...)
///
Future<Either<Failure, Weather>> call(
String city,
) async {
/// Repository هو المسؤول عن:
///
/// - API
/// - Cache
/// - اختيار مصدر البيانات
///
/// أما UseCase
/// فلا يعرف شيئاً عن ذلك.
///
return await repository.getForecast(
city,
);
}
}Dartملاحظة معمارية مهمة: عند هذه النقطة سأغيّر التصميم قليلًا في مشروعنا الاحترافي. بدلًا من إنشاء UseCase لكل دالة في WeatherRepository (مثل SaveLastCity وGetLastCity وSaveLastWeather…)، سننشئ فقط الـ UseCases التي تمثل عمليات يستخدمها الـ UI فعليًا. هذا يجعل طبقة Domain أنظف وأسهل في الصيانة، وهو الأسلوب المتبع في كثير من المشاريع الكبيرة.
File: get_weather_by_location.dart
Path
lib/features/weather/domain/usecases/get_weather_by_location.dart
/// ============================================================================
/// File : get_weather_by_location.dart
/// Path :
/// lib/features/weather/domain/usecases/get_weather_by_location.dart
/// ============================================================================
///
/// هذا الـ UseCase مسؤول عن جلب حالة الطقس
/// اعتماداً على الموقع الجغرافي للمستخدم.
///
/// لماذا أنشأنا UseCase منفصل؟
///
/// لأن البحث بواسطة:
///
/// اسم المدينة
///
/// يختلف عن البحث بواسطة:
///
/// Latitude + Longitude.
///
/// وكل عملية داخل Clean Architecture
/// يجب أن تكون مستقلة.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// GetWeatherByLocation
///
/// ============================================================================
///
/// يمثل عملية واحدة فقط.
///
/// وهى:
///
/// جلب الطقس باستخدام
/// الإحداثيات الجغرافية.
///
/// ============================================================================
class GetWeatherByLocation {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لا نعتمد على Implementation.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// Dependency Injection.
///
const GetWeatherByLocation(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// يتم استدعاؤه بهذه الطريقة:
///
/// final result =
/// await getWeatherByLocation(
/// 36.2021,
/// 37.1343,
/// );
///
/// وليس:
///
/// getWeatherByLocation.call(...)
///
Future<Either<Failure, Weather>> call({
required double latitude,
required double longitude,
}) async {
/// ------------------------------------------------------------------------
/// لا يحتوي هذا UseCase
/// على أي Business Logic حالياً.
///
/// هو فقط يطلب تنفيذ العملية.
///
/// مستقبلاً يمكن إضافة:
///
/// - Validation للإحداثيات.
/// - تسجيل Analytics.
/// - Logging.
/// - التحقق من Cache.
///
/// دون تعديل Cubit.
/// ------------------------------------------------------------------------
return await repository.getWeatherByLocation(
latitude,
longitude,
);
}
}Dartملاحظة احترافية: من هذه النقطة سأبدأ بإعادة تحسين التصميم قليلًا عن الشرح السابق. سأستخدم Parameter Objects بدلاً من تمرير عدة متغيرات للدوال (مثل latitude وlongitude وcity) لأن هذا هو الأسلوب المستخدم في المشاريع الكبيرة، ويجعل الـ UseCases أسهل للتوسع والاختبار. سنقوم بهذا التحسين عندما ننتهي من بناء النسخة الأولى من الـ Domain Layer.
File: save_last_city.dart
Path
lib/features/weather/domain/usecases/save_last_city.dart
/// ============================================================================
/// File : save_last_city.dart
/// Path :
/// lib/features/weather/domain/usecases/save_last_city.dart
/// ============================================================================
///
/// هذا الـ UseCase مسؤول عن حفظ آخر مدينة
/// اختارها المستخدم.
///
/// لماذا يوجد UseCase للحفظ؟
///
/// لأن Clean Architecture تعتبر:
///
/// "الحفظ"
///
/// عملية (Business Action)
///
/// وليست مجرد استدعاء لـ SharedPreferences.
///
/// لذلك لا يجوز أن يستدعي Cubit
/// StorageService مباشرة.
///
/// دائماً:
///
/// Cubit
/// ↓
/// UseCase
/// ↓
/// Repository
/// ↓
/// LocalDataSource
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// SaveLastCity
///
/// ============================================================================
///
/// مسؤول عن تنفيذ عملية واحدة فقط.
///
/// وهى:
///
/// حفظ آخر مدينة.
///
/// ============================================================================
class SaveLastCity {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لا يعتمد على Implementation.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// Dependency Injection.
///
const SaveLastCity(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// يستقبل اسم المدينة.
///
/// مثال:
///
/// Damascus
///
/// ثم يطلب من Repository حفظها.
///
/// --------------------------------------------------------------------------
///
/// لماذا Unit ؟
///
/// لأن عملية الحفظ
/// لا تعيد بيانات.
///
/// وإنما تعيد:
///
/// نجاح
///
/// أو
///
/// Failure.
///
/// لذلك نستخدم:
///
/// Either<Failure, Unit>
///
Future<Either<Failure, Unit>> call(
String city,
) async {
/// ------------------------------------------------------------------------
/// يمكن هنا مستقبلاً
/// إضافة Validation.
///
/// مثال:
///
/// - منع حفظ اسم فارغ.
/// - إزالة الفراغات.
/// - توحيد صيغة الاسم.
///
/// دون تعديل Repository.
/// ------------------------------------------------------------------------
return await repository.saveLastCity(
city,
);
}
}DartFile: get_last_city.dart
Path
lib/features/weather/domain/usecases/get_last_city.dart
/// ============================================================================
/// File : get_last_city.dart
/// Path :
/// lib/features/weather/domain/usecases/get_last_city.dart
/// ============================================================================
///
/// هذا الـ UseCase مسؤول عن استرجاع آخر مدينة
/// قام المستخدم بالبحث عنها أو اختيارها.
///
/// لماذا لا يستدعي Cubit Repository مباشرة؟
///
/// لأن Cubit يجب أن يعرف:
///
/// "أريد آخر مدينة."
///
/// فقط.
///
/// أما كيفية الحصول عليها
/// فهي مسؤولية Repository.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// GetLastCity
///
/// ============================================================================
///
/// يمثل عملية واحدة فقط.
///
/// وهى:
///
/// استرجاع آخر مدينة محفوظة محلياً.
///
/// ============================================================================
class GetLastCity {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لا نعتمد على:
///
/// WeatherRepositoryImpl
///
/// وإنما على الـ Interface فقط.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// Dependency Injection.
///
const GetLastCity(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// لا يستقبل أي Parameters.
///
/// لأن آخر مدينة
/// موجودة مسبقاً داخل التخزين المحلي.
///
/// --------------------------------------------------------------------------
///
/// يعيد:
///
/// Either<Failure, String>
///
/// Left
///
/// Failure
///
/// عند حدوث خطأ.
///
/// Right
///
/// اسم المدينة.
///
/// --------------------------------------------------------------------------
///
/// مثال:
///
/// Damascus
///
/// Aleppo
///
/// Istanbul
///
Future<Either<Failure, String>> call() async {
/// ------------------------------------------------------------------------
/// لا يوجد Business Logic حالياً.
///
/// لكن لاحقاً يمكن إضافة:
///
/// - التحقق من وجود قيمة.
/// - تنظيف النص.
/// - إرجاع مدينة افتراضية.
/// - Validation.
///
/// دون تعديل Repository.
/// ------------------------------------------------------------------------
return await repository.getLastCity();
}
}Dartملاحظة معمارية: بعد إنهاء جميع الـ UseCases سنتوقف قليلًا لإعادة تحسين التصميم. سأقوم بدمج بعض الـ UseCases غير الضرورية (مثل عمليات الحفظ البسيطة) إذا لم تكن تضيف قيمة حقيقية، لأن الهدف ليس الإكثار من الملفات، بل تطبيق Clean Architecture بطريقة عملية ومتوازنة كما تُستخدم في المشاريع الاحترافية.
File: save_last_weather.dart
Path
lib/features/weather/domain/usecases/save_last_weather.dart
/// ============================================================================
/// File : save_last_weather.dart
/// Path :
/// lib/features/weather/domain/usecases/save_last_weather.dart
/// ============================================================================
///
/// هذا الـ UseCase مسؤول عن حفظ آخر بيانات طقس
/// داخل التخزين المحلي.
///
/// لماذا نحفظ بيانات الطقس؟
///
/// لأن التطبيق يجب أن يعمل حتى عند
/// انقطاع الإنترنت.
///
/// السيناريو:
///
/// يوجد إنترنت
/// │
/// ▼
/// تحميل بيانات جديدة
/// │
/// ▼
/// حفظها محلياً
/// │
/// ▼
/// إذا انقطع الإنترنت
/// │
/// ▼
/// عرض آخر بيانات محفوظة.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// SaveLastWeather
///
/// ============================================================================
///
/// يمثل عملية واحدة فقط.
///
/// وهى:
///
/// حفظ بيانات الطقس.
///
/// ============================================================================
class SaveLastWeather {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لا يعتمد على Implementation.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// Dependency Injection.
///
const SaveLastWeather(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// يستقبل Entity من نوع Weather.
///
/// لاحظ...
///
/// لا يستقبل:
///
/// WeatherModel
///
/// لأن Domain Layer
/// لا تعرف Models.
///
Future<Either<Failure, Unit>> call(
Weather weather,
) async {
/// ------------------------------------------------------------------------
/// مستقبلاً يمكن إضافة Business Logic هنا.
///
/// مثال:
///
/// - عدم حفظ بيانات قديمة.
/// - حفظ البيانات إذا تغيرت فقط.
/// - ضغط البيانات قبل التخزين.
/// - تشفير البيانات.
///
/// دون تعديل Repository.
/// ------------------------------------------------------------------------
return await repository.saveLastWeather(
weather,
);
}
}DartFile: get_last_weather.dart
Path
lib/features/weather/domain/usecases/get_last_weather.dart
/// ============================================================================
/// File : get_last_weather.dart
/// Path :
/// lib/features/weather/domain/usecases/get_last_weather.dart
/// ============================================================================
///
/// هذا الـ UseCase مسؤول عن استرجاع آخر بيانات طقس
/// محفوظة داخل الجهاز.
///
/// متى يستخدم؟
///
/// عندما:
///
/// - لا يوجد اتصال بالإنترنت.
/// - فتح التطبيق بدون شبكة.
/// - فشل الاتصال بالسيرفر.
///
/// عندها لا نعرض شاشة فارغة.
///
/// وإنما نعرض آخر بيانات تم حفظها.
///
/// ============================================================================
import 'package:dartz/dartz.dart';
import '../../../../core/errors/failures.dart';
import '../entities/weather.dart';
import '../repositories/weather_repository.dart';
/// ============================================================================
///
/// GetLastWeather
///
/// ============================================================================
///
/// يمثل عملية واحدة فقط.
///
/// وهى:
///
/// استرجاع آخر بيانات طقس محفوظة.
///
/// ============================================================================
class GetLastWeather {
//===========================================================================
// Repository
//===========================================================================
/// Repository Interface.
///
/// لا يعتمد على:
///
/// WeatherRepositoryImpl
///
/// وإنما يعتمد على Interface فقط.
///
final WeatherRepository repository;
//===========================================================================
// Constructor
//===========================================================================
/// يتم حقن Repository.
///
/// Dependency Injection.
///
const GetLastWeather(
this.repository,
);
//===========================================================================
// call()
//===========================================================================
/// لا يحتاج أي Parameters.
///
/// لأن البيانات موجودة مسبقاً
/// داخل Local Storage.
///
/// --------------------------------------------------------------------------
///
/// يعيد:
///
/// Either<Failure, Weather>
///
/// Left
///
/// Failure
///
/// إذا لم توجد بيانات
/// أو حدث خطأ.
///
/// Right
///
/// Weather
///
/// إذا نجحت العملية.
///
Future<Either<Failure, Weather>> call() async {
/// ------------------------------------------------------------------------
/// حالياً لا يوجد Business Logic.
///
/// لكن لاحقاً يمكن إضافة:
///
/// - التحقق من عمر البيانات.
/// - تجاهل البيانات القديمة.
/// - إعادة تحميل البيانات إذا تجاوزت مدة معينة.
/// - التحقق من سلامة البيانات.
///
/// دون تعديل Repository أو Cubit.
/// ------------------------------------------------------------------------
return await repository.getLastWeather();
}
}Dartالملف التالي (بداية Data Layer):
lib/features/weather/data/models/weather_model.dart
وهنا سنبدأ ببناء أول Model وشرح التحويل بين:
- JSON
- Model
- Entity
وهو من أهم أجزاء Clean Architecture.
File: weather_model.dart
Path
lib/features/weather/data/models/weather_model.dart
/// ============================================================================
/// File : weather_model.dart
/// Path :
/// lib/features/weather/data/models/weather_model.dart
/// ============================================================================
///
/// هذا الملف يعتبر أهم ملف داخل Data Layer.
///
/// لماذا؟
///
/// لأنه يمثل البيانات القادمة من الـ API.
///
/// انتبه جيداً...
///
/// Model ≠ Entity
///
/// كثير من المبتدئين يخلط بينهما.
///
/// ============================================================================
///
/// Entity
///
/// يمثل البيانات التي يفهمها التطبيق.
///
/// ولا يعرف شيئاً عن:
///
/// API
/// JSON
///
/// ============================================================================
///
/// Model
///
/// يمثل البيانات كما يعيدها السيرفر.
///
/// لذلك يحتوي:
///
/// fromJson()
///
/// toJson()
///
/// ============================================================================
///
/// رحلة البيانات تكون:
///
/// API
/// │
/// ▼
/// JSON
/// │
/// ▼
/// WeatherModel
/// │
/// ▼
/// Weather Entity
///
/// ============================================================================
import '../../domain/entities/weather.dart';
import '../../domain/entities/forecast.dart';
/// ============================================================================
///
/// WeatherModel
///
/// ============================================================================
///
/// يرث من Weather.
///
/// لماذا؟
///
/// لأن Model هو نسخة قابلة للتحويل
/// من وإلى JSON.
///
/// ============================================================================
class WeatherModel extends Weather {
/// --------------------------------------------------------------------------
/// Constructor
/// --------------------------------------------------------------------------
///
/// جميع القيم يتم تمريرها
/// إلى Entity.
///
const WeatherModel({
required super.city,
required super.country,
required super.temperature,
required super.maxTemperature,
required super.minTemperature,
required super.description,
required super.icon,
required super.humidity,
required super.pressure,
required super.windSpeed,
required super.visibility,
required super.sunrise,
required super.sunset,
required super.date,
required super.forecast,
});
//===========================================================================
// fromJson()
//===========================================================================
/// --------------------------------------------------------------------------
/// تحول JSON القادم من API
/// إلى WeatherModel.
///
/// مثال:
///
/// Response.data
///
/// --------------------------------------------------------------------------
///
factory WeatherModel.fromJson(
Map<String, dynamic> json,
) {
return WeatherModel(
//=======================================================================
// Location
//=======================================================================
city: json["location"]["name"],
country: json["location"]["country"],
//=======================================================================
// Current Weather
//=======================================================================
temperature:
(json["current"]["temp_c"] as num).toDouble(),
maxTemperature:
(json["forecast"]["forecastday"][0]["day"]["maxtemp_c"]
as num)
.toDouble(),
minTemperature:
(json["forecast"]["forecastday"][0]["day"]["mintemp_c"]
as num)
.toDouble(),
description:
json["current"]["condition"]["text"],
icon:
json["current"]["condition"]["icon"],
humidity:
json["current"]["humidity"],
pressure:
(json["current"]["pressure_mb"] as num)
.toDouble(),
windSpeed:
(json["current"]["wind_kph"] as num)
.toDouble(),
visibility:
(json["current"]["vis_km"] as num)
.toDouble(),
//=======================================================================
// Astronomy
//=======================================================================
/// لاحقاً سنحول الوقت
/// إلى DateTime بطريقة احترافية.
///
sunrise: DateTime.now(),
sunset: DateTime.now(),
//=======================================================================
// Date
//=======================================================================
date: DateTime.parse(
json["location"]["localtime"],
),
//=======================================================================
// Forecast
//=======================================================================
/// يتم تحويل List القادمة من API
/// إلى List<Forecast>.
///
/// حالياً سنضعها فارغة.
///
/// في الملف التالي
/// ForecastModel
/// سنقوم ببنائها بالكامل.
///
forecast: const [],
);
}
//===========================================================================
// toJson()
//===========================================================================
/// --------------------------------------------------------------------------
/// تحول Model
/// إلى JSON.
///
/// تستخدم غالباً مع:
///
/// POST
/// PUT
/// Cache
///
Map<String, dynamic> toJson() {
return {
"city": city,
"country": country,
"temperature": temperature,
"maxTemperature": maxTemperature,
"minTemperature": minTemperature,
"description": description,
"icon": icon,
"humidity": humidity,
"pressure": pressure,
"windSpeed": windSpeed,
"visibility": visibility,
"sunrise": sunrise.toIso8601String(),
"sunset": sunset.toIso8601String(),
"date": date.toIso8601String(),
};
}
//===========================================================================
// toEntity()
//===========================================================================
/// --------------------------------------------------------------------------
/// يحول Model
/// إلى Entity.
///
/// Repository سيستخدم هذه الدالة.
///
Weather toEntity() {
return Weather(
city: city,
country: country,
temperature: temperature,
maxTemperature: maxTemperature,
minTemperature: minTemperature,
description: description,
icon: icon,
humidity: humidity,
pressure: pressure,
windSpeed: windSpeed,
visibility: visibility,
sunrise: sunrise,
sunset: sunset,
date: date,
forecast: forecast,
);
}
//===========================================================================
// copyWith()
//===========================================================================
/// إنشاء نسخة جديدة
/// مع تعديل بعض القيم.
///
WeatherModel copyWith({
String? city,
String? country,
double? temperature,
}) {
return WeatherModel(
city: city ?? this.city,
country: country ?? this.country,
temperature:
temperature ?? this.temperature,
maxTemperature: maxTemperature,
minTemperature: minTemperature,
description: description,
icon: icon,
humidity: humidity,
pressure: pressure,
windSpeed: windSpeed,
visibility: visibility,
sunrise: sunrise,
sunset: sunset,
date: date,
forecast: forecast,
);
}
}Dartملاحظة احترافية مهمة: هذا الملف لن أعتمده في النسخة النهائية للمشروع لأنه يرث WeatherModel من Weather (extends Weather). في المشاريع الكبيرة أفضل استخدام Composition بدل Inheritance، كما أنني لن أضع forecast داخل WeatherModel، بل سأفصل إلى:
CurrentWeatherModelForecastModelWeatherResponseModel
لأن هذا يطابق استجابة الـ API ويجعل الصيانة أسهل. سنعيد هيكلة هذه الطبقة قبل بدء RepositoryImpl لتصبح بمعايير Production الحقيقية.
File: forecast_model.dart
Path
lib/features/weather/data/models/forecast_model.dart
/// ============================================================================
/// File : forecast_model.dart
/// Path :
/// lib/features/weather/data/models/forecast_model.dart
/// ============================================================================
///
/// يمثل هذا الملف الـ Model الخاص بتوقعات الطقس.
///
/// انتبه...
///
/// ForecastModel ليس Forecast.
///
/// Forecast
/// --------
/// Entity داخل Domain Layer.
///
/// ForecastModel
/// -------------
/// يمثل البيانات القادمة من الـ API.
///
/// لذلك يحتوي:
///
/// ✅ fromJson()
/// ✅ toJson()
/// ✅ toEntity()
///
/// ============================================================================
import '../../domain/entities/forecast.dart';
/// ============================================================================
///
/// ForecastModel
///
/// ============================================================================
///
/// يرث من Forecast.
///
/// ويضيف إمكانية التحويل
/// من وإلى JSON.
///
/// ============================================================================
class ForecastModel extends Forecast {
/// --------------------------------------------------------------------------
/// Constructor
/// --------------------------------------------------------------------------
///
/// جميع القيم تمرر إلى Entity.
///
const ForecastModel({
required super.date,
required super.day,
required super.maxTemperature,
required super.minTemperature,
required super.description,
required super.icon,
required super.chanceOfRain,
});
//===========================================================================
// fromJson()
//===========================================================================
/// --------------------------------------------------------------------------
/// تحويل بيانات يوم واحد
/// من JSON إلى ForecastModel.
///
/// عنصر json يمثل:
///
/// forecastday[index]
///
factory ForecastModel.fromJson(
Map<String, dynamic> json,
) {
return ForecastModel(
//=======================================================================
// Date
//=======================================================================
date: DateTime.parse(
json["date"],
),
//=======================================================================
// Day Name
//=======================================================================
/// سيتم توليده لاحقاً باستخدام intl.
///
day: "",
//=======================================================================
// Temperature
//=======================================================================
maxTemperature:
(json["day"]["maxtemp_c"] as num)
.toDouble(),
minTemperature:
(json["day"]["mintemp_c"] as num)
.toDouble(),
//=======================================================================
// Weather
//=======================================================================
description:
json["day"]["condition"]["text"],
icon:
json["day"]["condition"]["icon"],
//=======================================================================
// Rain
//=======================================================================
chanceOfRain:
json["day"]["daily_chance_of_rain"] ?? 0,
);
}
//===========================================================================
// fromJsonList()
//===========================================================================
/// --------------------------------------------------------------------------
/// تحويل List كاملة من JSON
/// إلى List<ForecastModel>.
///
/// API يعيد:
///
/// forecastday:[
/// {},
/// {},
/// {}
/// ]
///
static List<ForecastModel> fromJsonList(
List<dynamic> json,
) {
return json
.map(
(item) => ForecastModel.fromJson(item),
)
.toList();
}
//===========================================================================
// toJson()
//===========================================================================
/// تحويل Model إلى JSON.
///
Map<String, dynamic> toJson() {
return {
"date": date.toIso8601String(),
"day": day,
"maxTemperature": maxTemperature,
"minTemperature": minTemperature,
"description": description,
"icon": icon,
"chanceOfRain": chanceOfRain,
};
}
//===========================================================================
// toEntity()
//===========================================================================
/// تحويل Model إلى Entity.
///
Forecast toEntity() {
return Forecast(
date: date,
day: day,
maxTemperature: maxTemperature,
minTemperature: minTemperature,
description: description,
icon: icon,
chanceOfRain: chanceOfRain,
);
}
//===========================================================================
// copyWith()
//===========================================================================
/// إنشاء نسخة جديدة
/// مع تعديل بعض القيم.
///
ForecastModel copyWith({
DateTime? date,
String? day,
double? maxTemperature,
double? minTemperature,
String? description,
String? icon,
int? chanceOfRain,
}) {
return ForecastModel(
date: date ?? this.date,
day: day ?? this.day,
maxTemperature:
maxTemperature ?? this.maxTemperature,
minTemperature:
minTemperature ?? this.minTemperature,
description:
description ?? this.description,
icon: icon ?? this.icon,
chanceOfRain:
chanceOfRain ?? this.chanceOfRain,
);
}
}Dart