المُنشئات بالتفصيل
لماذا نتعلم المزيد عن المُنشئات؟
في الدرس السابق تعلمت كيفية إنشاء مُنشئات أساسية بمعاملات موضعية ومسماة. لكن Dart تقدم أنواع مُنشئات أقوى بكثير ستراها في كل مكان في Flutter. فهمها ضروري لأن ودجات Flutter تستخدم مُنشئات const للأداء ومُنشئات factory لأنماط مثل Singleton والمُنشئات المسماة لإنشاء الكائنات بطرق مختلفة. هذا الدرس يغطيها جميعاً.
مراجعة سريعة: المُنشئ الافتراضي
المُنشئ الافتراضي له نفس اسم الفئة ويستخدم اختصار Dart this.:
مراجعة المُنشئ الافتراضي
class Point {
double x;
double y;
// المُنشئ الافتراضي بمعاملات موضعية
Point(this.x, this.y);
}
void main() {
Point p = Point(3.0, 4.0);
print('(${p.x}, ${p.y})'); // (3.0, 4.0)
}
قوائم التهيئة
قائمة التهيئة تعمل قبل جسم المُنشئ. تُستخدم لتعيين الحقول النهائية أو إجراء التحقق بـ assert أو حساب القيم من المعاملات. استخدم نقطتين : بعد قائمة المعاملات.
أساسيات قائمة التهيئة
class Rectangle {
final double width;
final double height;
final double area;
// قائمة التهيئة تحسب المساحة من العرض والارتفاع
Rectangle(this.width, this.height)
: area = width * height;
// مع التحقق باستخدام assert (يعمل فقط في وضع التصحيح)
Rectangle.validated(this.width, this.height)
: assert(width > 0, 'العرض يجب أن يكون موجباً'),
assert(height > 0, 'الارتفاع يجب أن يكون موجباً'),
area = width * height;
@override
String toString() => 'مستطيل(${width}x$height، المساحة: $area)';
}
void main() {
var r1 = Rectangle(10, 5);
print(r1); // مستطيل(10.0x5.0، المساحة: 50.0)
var r2 = Rectangle.validated(8, 3);
print(r2); // مستطيل(8.0x3.0، المساحة: 24.0)
}
this في قائمة التهيئة (إلا في اختصار المعاملات this.field). قوائم التهيئة هي الطريقة الوحيدة لتهيئة حقول final المحسوبة من المعاملات.المُنشئات المسماة
Dart لا تدعم تحميل المُنشئات الزائد (مُنشئات متعددة بمعاملات مختلفة). بدلاً من ذلك تستخدم المُنشئات المسماة لتوفير طرق بديلة لإنشاء كائن. الصيغة هي ClassName.constructorName().
المُنشئات المسماة
class Point {
double x;
double y;
// المُنشئ الافتراضي
Point(this.x, this.y);
// مُنشئ مسمى: إنشاء من نقطة الأصل
Point.origin()
: x = 0,
y = 0;
// مُنشئ مسمى: إنشاء على محور X
Point.onXAxis(double x)
: this.x = x,
y = 0;
// مُنشئ مسمى: إنشاء على محور Y
Point.onYAxis(double y)
: x = 0,
this.y = y;
// مُنشئ مسمى: إنشاء من Map
Point.fromMap(Map<String, double> map)
: x = map['x'] ?? 0,
y = map['y'] ?? 0;
@override
String toString() => 'نقطة($x, $y)';
}
void main() {
var p1 = Point(3, 4);
var p2 = Point.origin();
var p3 = Point.onXAxis(5);
var p4 = Point.onYAxis(7);
var p5 = Point.fromMap({'x': 1, 'y': 2});
print(p1); // نقطة(3.0, 4.0)
print(p2); // نقطة(0.0, 0.0)
print(p3); // نقطة(5.0, 0.0)
print(p4); // نقطة(0.0, 7.0)
print(p5); // نقطة(1.0, 2.0)
}
مثال واقعي: نموذج المستخدم
class User {
final String name;
final String email;
final String role;
final DateTime createdAt;
User({
required this.name,
required this.email,
this.role = 'user',
}) : createdAt = DateTime.now();
// مُنشئ مسمى: إنشاء مدير
User.admin({required String name, required String email})
: this.name = name,
this.email = email,
role = 'admin',
createdAt = DateTime.now();
// مُنشئ مسمى: إنشاء زائر
User.guest()
: name = 'زائر',
email = 'guest@example.com',
role = 'guest',
createdAt = DateTime.now();
// مُنشئ مسمى: إنشاء من خريطة JSON
User.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
email = json['email'] as String,
role = json['role'] as String? ?? 'user',
createdAt = DateTime.parse(json['created_at'] as String);
// التحويل إلى خريطة JSON
Map<String, dynamic> toJson() => {
'name': name,
'email': email,
'role': role,
'created_at': createdAt.toIso8601String(),
};
@override
String toString() => 'مستخدم($name, $email, $role)';
}
void main() {
var user = User(name: 'أحمد', email: 'ahmed@test.com');
var admin = User.admin(name: 'سارة', email: 'sara@test.com');
var guest = User.guest();
print(user); // مستخدم(أحمد, ahmed@test.com, user)
print(admin); // مستخدم(سارة, sara@test.com, admin)
print(guest); // مستخدم(زائر, guest@example.com, guest)
// محاكاة JSON من API
var json = {'name': 'خالد', 'email': 'k@test.com', 'role': 'editor', 'created_at': '2024-01-15T10:30:00.000'};
var fromApi = User.fromJson(json);
print(fromApi); // مستخدم(خالد, k@test.com, editor)
}
.fromJson() والطرق مثل .toJson() هي النمط القياسي في Flutter للتحويل بين كائنات Dart وبيانات JSON من الـ APIs. ستستخدم هذا النمط في تقريباً كل تطبيق Flutter.المُنشئات المُعاد توجيهها
المُنشئ المُعاد توجيهه يفوّض لمُنشئ آخر في نفس الفئة. لا يمكن أن يحتوي على جسم -- ببساطة يستدعي المُنشئ المستهدف باستخدام : this(...).
المُنشئات المُعاد توجيهها
class Color {
final int red;
final int green;
final int blue;
final double opacity;
// المُنشئ الرئيسي
Color(this.red, this.green, this.blue, {this.opacity = 1.0});
// مُنشئات مُعاد توجيهها -- تفوّض للرئيسي
Color.red() : this(255, 0, 0);
Color.green() : this(0, 255, 0);
Color.blue() : this(0, 0, 255);
Color.white() : this(255, 255, 255);
Color.black() : this(0, 0, 0);
// إعادة توجيه مع شفافية مخصصة
Color.semiTransparent(int r, int g, int b)
: this(r, g, b, opacity: 0.5);
@override
String toString() =>
'لون($red, $green, $blue, الشفافية: $opacity)';
}
void main() {
print(Color.red()); // لون(255, 0, 0, الشفافية: 1.0)
print(Color.blue()); // لون(0, 0, 255, الشفافية: 1.0)
print(Color.semiTransparent(100, 200, 50));
// لون(100, 200, 50, الشفافية: 0.5)
}
مُنشئات Const
مُنشئ const ينشئ كائنات ثابتة وقت الترجمة. عندما تنشئ كائنين const بنفس القيم يعيد Dart استخدام نفس النسخة في الذاكرة. هذا تحسين أداء ضخم في Flutter حيث تُعاد بناء الودجات بشكل متكرر.
أساسيات مُنشئ Const
class ImmutablePoint {
final double x;
final double y;
// مُنشئ const -- جميع الحقول يجب أن تكون final
const ImmutablePoint(this.x, this.y);
// مُنشئ const مسمى
const ImmutablePoint.origin() : x = 0, y = 0;
@override
String toString() => 'نقطة_ثابتة($x, $y)';
}
void main() {
// باستخدام const -- نفس القيم تنتج نفس الكائن
const p1 = ImmutablePoint(1, 2);
const p2 = ImmutablePoint(1, 2);
print(identical(p1, p2)); // true! هما نفس الكائن
// بدون const -- كائنات مختلفة حتى بنفس القيم
var p3 = ImmutablePoint(1, 2);
var p4 = ImmutablePoint(1, 2);
print(identical(p3, p4)); // false! كائنات مختلفة
// مُنشئ const مسمى
const origin = ImmutablePoint.origin();
print(origin); // نقطة_ثابتة(0.0, 0.0)
}
• جميع حقول النسخة يجب أن تكون
final.• جسم المُنشئ يجب أن يكون فارغاً (لا كود داخل
{}).• يمكنك استخدام قوائم التهيئة لكن فقط مع تعبيرات ثابتة.
• الفئة لا يمكن أن تحتوي على حقول قابلة للتغيير (غير final).
Const في سياق Flutter
// هكذا تبدو ودجات Flutter
class AppConfig {
final String appName;
final String version;
final bool debugMode;
const AppConfig({
required this.appName,
required this.version,
this.debugMode = false,
});
}
void main() {
// كائنات const تُنشأ وقت الترجمة -- تكلفة صفر وقت التشغيل
const config = AppConfig(
appName: 'تطبيق Flutter',
version: '1.0.0',
);
print(config.appName); // تطبيق Flutter
// في Flutter ودجات const تتخطى إعادة البناء:
// const Text('مرحبا') -- لا تُعاد بناؤها أبداً!
// Text('مرحبا') -- تُعاد بناؤها كل مرة يُعاد بناء الأب
}
const عندما يكون ذلك ممكناً في Flutter. ودجة const Text('مرحبا') تُنشأ مرة واحدة وقت الترجمة ولا تُعاد بناؤها أبداً بينما Text('مرحبا') غير const تُعاد إنشاؤها كل مرة يُعاد بناء الودجة الأب. هذا الفرق الصغير يمكن أن يكون له تأثير كبير على أداء التطبيق.مُنشئات Factory
مُنشئ factory هو مُنشئ خاص لا ينشئ دائماً نسخة جديدة. على عكس المُنشئات العادية يمكنه:
- إرجاع نسخة موجودة (نمط التخزين المؤقت/Singleton)
- إرجاع نسخة من فئة فرعية
- تشغيل منطق قبل تحديد ما يُرجع
- إرجاع
null(إذا كان نوع الإرجاع nullable)
مُنشئ Factory: نمط Singleton
class Database {
static Database? _instance;
final String connectionString;
// مُنشئ خاص -- لا يمكن استدعاؤه من الخارج
Database._internal(this.connectionString);
// Factory يُرجع دائماً نفس النسخة
factory Database(String connectionString) {
_instance ??= Database._internal(connectionString);
return _instance!;
}
void query(String sql) {
print('تنفيذ: $sql على $connectionString');
}
}
void main() {
var db1 = Database('localhost:3306/mydb');
var db2 = Database('other:5432/other'); // تُتجاهل! تُرجع نفس النسخة
print(identical(db1, db2)); // true -- نفس الكائن
db1.query('SELECT * FROM users');
// تنفيذ: SELECT * FROM users على localhost:3306/mydb
}
مُنشئ Factory: التخزين المؤقت
class Logger {
static final Map<String, Logger> _cache = {};
final String name;
// مُنشئ خاص
Logger._internal(this.name);
// Factory يُرجع نسخة مخزنة مؤقتاً أو ينشئ جديدة
factory Logger(String name) {
return _cache.putIfAbsent(name, () => Logger._internal(name));
}
void log(String message) {
print('[$name] $message');
}
}
void main() {
var appLogger = Logger('App');
var dbLogger = Logger('Database');
var appLogger2 = Logger('App'); // تُرجع النسخة المخزنة
print(identical(appLogger, appLogger2)); // true
print(identical(appLogger, dbLogger)); // false
appLogger.log('بدأ'); // [App] بدأ
dbLogger.log('متصل'); // [Database] متصل
}
مُنشئ Factory: إرجاع نوع فرعي
abstract class Shape {
double get area;
// Factory يُرجع أنواع فرعية مختلفة
factory Shape.circle(double radius) = Circle;
factory Shape.square(double side) = Square;
}
class Circle implements Shape {
final double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
@override
String toString() => 'دائرة(نق=$radius، المساحة=${area.toStringAsFixed(2)})';
}
class Square implements Shape {
final double side;
Square(this.side);
@override
double get area => side * side;
@override
String toString() => 'مربع(ض=$side، المساحة=${area.toStringAsFixed(2)})';
}
void main() {
Shape s1 = Shape.circle(5);
Shape s2 = Shape.square(4);
print(s1); // دائرة(نق=5.0، المساحة=78.54)
print(s2); // مربع(ض=4.0، المساحة=16.00)
}
Factory مقابل Named مقابل Const: متى تستخدم ماذا
• المُنشئ الافتراضي -- إنشاء كائن قياسي.
• المُنشئ المسمى -- طرق بديلة لإنشاء كائن (مثل
.fromJson() و .empty()).• مُنشئ Const -- كائنات غير قابلة للتغيير للأداء (جميع الحقول يجب أن تكون
final).• مُنشئ Factory -- عندما تحتاج تحكماً: التخزين المؤقت و Singleton وإرجاع أنواع فرعية أو منطق شرطي.
• المُنشئ المُعاد توجيهه -- اختصار يفوّض لمُنشئ آخر في نفس الفئة.
مثال عملي: نظام التكوين
مثال كامل باستخدام أنواع مُنشئات متعددة
class AppTheme {
final String name;
final int primaryColor;
final int backgroundColor;
final double fontSize;
// مُنشئ const افتراضي
const AppTheme({
required this.name,
required this.primaryColor,
required this.backgroundColor,
this.fontSize = 16.0,
});
// مُنشئات const مسماة للإعدادات المسبقة
const AppTheme.light()
: name = 'فاتح',
primaryColor = 0xFF2196F3,
backgroundColor = 0xFFFFFFFF,
fontSize = 16.0;
const AppTheme.dark()
: name = 'داكن',
primaryColor = 0xFF64B5F6,
backgroundColor = 0xFF121212,
fontSize = 16.0;
// مُنشئ مسمى من JSON (غير const لأنه يحسب قيماً)
AppTheme.fromJson(Map<String, dynamic> json)
: name = json['name'] as String,
primaryColor = json['primary_color'] as int,
backgroundColor = json['background_color'] as int,
fontSize = (json['font_size'] as num?)?.toDouble() ?? 16.0;
// Factory للتخزين المؤقت للسمات بالاسم
static final Map<String, AppTheme> _cache = {};
factory AppTheme.cached(String name) {
return _cache.putIfAbsent(name, () {
return switch (name) {
'light' => const AppTheme.light(),
'dark' => const AppTheme.dark(),
_ => const AppTheme.light(),
};
});
}
@override
String toString() => 'سمة_التطبيق($name، حجم_الخط: $fontSize)';
}
void main() {
// مُنشئات const
const light = AppTheme.light();
const dark = AppTheme.dark();
print(light); // سمة_التطبيق(فاتح، حجم_الخط: 16.0)
print(dark); // سمة_التطبيق(داكن، حجم_الخط: 16.0)
// كائنات const بنفس القيم متطابقة
const light2 = AppTheme.light();
print(identical(light, light2)); // true
// سمة مخصصة
const custom = AppTheme(
name: 'محيط',
primaryColor: 0xFF006064,
backgroundColor: 0xFFE0F7FA,
fontSize: 18.0,
);
print(custom); // سمة_التطبيق(محيط، حجم_الخط: 18.0)
// من JSON (مثل استجابة API)
var fromApi = AppTheme.fromJson({
'name': 'العلامة',
'primary_color': 0xFFFF5722,
'background_color': 0xFFFFF3E0,
});
print(fromApi); // سمة_التطبيق(العلامة، حجم_الخط: 16.0)
// Factory مع تخزين مؤقت
var t1 = AppTheme.cached('dark');
var t2 = AppTheme.cached('dark');
print(identical(t1, t2)); // true
}
تمرين عملي
افتح DartPad وابنِ التالي: (1) أنشئ فئة Temperature بحقل خاص _celsius. (2) أضف مُنشئاً افتراضياً يأخذ celsius. (3) أضف مُنشئاً مسمى Temperature.fromFahrenheit(double f) يحول إلى مئوي باستخدام قائمة التهيئة. (4) أضف مُنشئاً مسمى Temperature.fromJson(Map). (5) أضف مُنشئ const Temperature.absoluteZero() للقيمة -273.15. (6) أضف مُنشئ factory Temperature.boiling() يُرجع نسخة مخزنة مؤقتاً عند 100 درجة مئوية. (7) أضف getters لـ celsius و fahrenheit و kelvin. (8) اختبر جميع المُنشئات واطبع القيم بالمقاييس الثلاثة.