البرمجة غير المتزامنة: Futures
ما هي البرمجة غير المتزامنة؟
في البرمجة اليومية، يعمل معظم الكود بشكل متزامن -- كل سطر ينتظر انتهاء السطر السابق قبل التنفيذ. لكن ماذا يحدث عندما يحتاج برنامجك لجلب بيانات من خادم، أو قراءة ملف كبير، أو الانتظار لمؤقت؟ إذا كان كل شيء يعمل بشكل متزامن، سيتجمد تطبيقك بالكامل أثناء الانتظار.
تتيح البرمجة غير المتزامنة لبرنامجك بدء عملية طويلة الأمد والاستمرار في تنفيذ كود آخر أثناء انتظار النتيجة. عند اكتمال العملية، يتم إخطار برنامجك ويمكنه معالجة النتيجة. هذا ضروري لبناء تطبيقات سريعة الاستجابة، خاصة في Flutter حيث يجب أن تبقى واجهة المستخدم سلسة في جميع الأوقات.
حلقة أحداث Dart
في قلب نظام Dart غير المتزامن توجد حلقة الأحداث. فهمها هو المفتاح لكتابة كود غير متزامن صحيح.
تعمل حلقة الأحداث كالتالي:
- ينفذ Dart جميع الكود المتزامن في دالة
main()أولاً - ثم يتحقق من قائمة المهام الدقيقة (مهام عالية الأولوية مثل استدعاءات Future)
- إذا كانت قائمة المهام الدقيقة فارغة، يتحقق من قائمة الأحداث (أحداث الإدخال/الإخراج، المؤقتات، إدخال المستخدم)
- يعالج حدثًا واحدًا في كل مرة، ثم يتحقق من قائمة المهام الدقيقة مرة أخرى
- تتكرر هذه الدورة حتى تفرغ كلتا القائمتين
حلقة الأحداث في العمل
void main() {
print('1. Start of main');
Future(() => print('4. Event queue task'));
Future.microtask(() => print('3. Microtask queue'));
print('2. End of main');
}
// الإخراج:
// 1. Start of main
// 2. End of main
// 3. Microtask queue
// 4. Event queue task
فئة Future
Future<T> يمثل قيمة ستكون متاحة في وقت ما في المستقبل. هي فئة Dart الأساسية للتعامل مع العمليات غير المتزامنة المفردة. يمكن أن يكون Future في واحدة من ثلاث حالات:
- غير مكتمل -- العملية لا تزال قيد التشغيل
- مكتمل بقيمة -- نجحت العملية وأعادت نتيجة من نوع
T - مكتمل بخطأ -- فشلت العملية
إنشاء Future أساسي
void main() {
// Future يكتمل بعد دورة حلقة الأحداث الحالية
Future(() {
print('This runs asynchronously');
return 42;
});
print('This runs first (synchronous)');
}
// الإخراج:
// This runs first (synchronous)
// This runs asynchronously
Future.delayed
منشئ Future.delayed ينشئ Future يكتمل بعد مدة محددة. هذا مفيد للغاية لمحاكاة طلبات الشبكة أثناء التطوير، وتنفيذ المهلات الزمنية، وإنشاء عمليات مؤقتة.
استخدام Future.delayed
void main() {
print('Fetching data...');
Future.delayed(Duration(seconds: 2), () {
return 'Data loaded successfully!';
}).then((value) {
print(value);
});
print('Doing other work while waiting...');
}
// الإخراج:
// Fetching data...
// Doing other work while waiting...
// (بعد ثانيتين)
// Data loaded successfully!
محاكاة طلب شبكة
Future<Map<String, dynamic>> fetchUser(int id) {
// يحاكي استدعاء API مدته 1.5 ثانية
return Future.delayed(Duration(milliseconds: 1500), () {
return {
'id': id,
'name': 'Ahmed Al-Farsi',
'email': 'ahmed@example.com',
'role': 'developer',
};
});
}
void main() {
print('Requesting user data...');
fetchUser(101).then((user) {
print('User: ${user["name"]} (${user["email"]})');
});
print('Request sent, continuing...');
}
Future.value و Future.error
Future.value ينشئ Future يكتمل فورًا بقيمة محددة. Future.error ينشئ Future يكتمل فورًا بخطأ. هذه مفيدة لإعادة نتائج مخبأة أو أخطاء معروفة من دوال يجب أن تعيد Future.
Future.value -- اكتمال فوري
Future<String> getCachedData(String key) {
final cache = {
'user': 'Ahmed',
'theme': 'dark',
'lang': 'en',
};
if (cache.containsKey(key)) {
// إعادة فورية من الذاكرة المخبأة
return Future.value(cache[key]!);
}
// محاكاة جلب من الشبكة للمفاتيح غير المخبأة
return Future.delayed(Duration(seconds: 1), () {
return 'fetched_$key';
});
}
void main() {
getCachedData('user').then((v) => print('Cached: $v'));
getCachedData('avatar').then((v) => print('Fetched: $v'));
}
Future.error -- خطأ فوري
Future<int> divide(int a, int b) {
if (b == 0) {
return Future.error(ArgumentError('Cannot divide by zero'));
}
return Future.value(a ~/ b);
}
void main() {
divide(10, 2).then((result) {
print('Result: $result'); // Result: 5
});
divide(10, 0).catchError((error) {
print('Error: $error'); // Error: Invalid argument(s): Cannot divide by zero
});
}
Future.value "فورًا"، فإن استدعاء .then() الخاص به لا يزال يعمل بشكل غير متزامن (في المهمة الدقيقة التالية). لا يتم تنفيذه بشكل متزامن في السطر. هذا مصدر شائع للارتباك عند المبتدئين.تسلسل Futures: .then() و .catchError() و .whenComplete()
تدعم Futures واجهة تسلسل قوية تتيح لك بناء خطوط أنابيب من العمليات غير المتزامنة:
.then()-- يعمل عند اكتمال Future بنجاح؛ يمكنه إعادة قيمة جديدة أو Future.catchError()-- يعمل عند اكتمال Future بخطأ.whenComplete()-- يعمل بغض النظر عن النجاح أو الفشل (مثلfinallyفي try-catch)
تسلسل استدعاءات .then()
Future<String> fetchUserId() {
return Future.delayed(Duration(seconds: 1), () => 'user_42');
}
Future<Map<String, String>> fetchProfile(String userId) {
return Future.delayed(Duration(milliseconds: 800), () => {
'id': userId,
'name': 'Sara Khan',
'city': 'Dubai',
});
}
Future<String> fetchAvatar(String userId) {
return Future.delayed(Duration(milliseconds: 500), () {
return 'https://avatars.example.com/$userId.png';
});
}
void main() {
fetchUserId()
.then((userId) {
print('Got user ID: $userId');
return fetchProfile(userId);
})
.then((profile) {
print('Got profile: ${profile["name"]} from ${profile["city"]}');
return fetchAvatar(profile['id']!);
})
.then((avatarUrl) {
print('Avatar URL: $avatarUrl');
})
.catchError((error) {
print('Something went wrong: $error');
})
.whenComplete(() {
print('All operations finished.');
});
}
.then() يمكنه إعادة قيمة عادية أو Future آخر. إذا أعاد Future، فإن .then() التالي في السلسلة ينتظر اكتمال ذلك Future قبل التشغيل. هذه هي الطريقة لتسلسل العمليات غير المتزامنة المعتمدة على بعضها.معالجة الأخطاء مع .catchError()
طريقة .catchError() تلتقط الأخطاء من أي نقطة سابقة في السلسلة. يمكنك أيضًا تصفية الأخطاء حسب النوع باستخدام معامل test الاختياري.
معالجة أخطاء انتقائية
Future<String> riskyOperation() {
return Future.delayed(Duration(seconds: 1), () {
throw FormatException('Invalid data format');
});
}
void main() {
riskyOperation()
.then((value) => print('Success: $value'))
.catchError(
(error) => print('Format error: $error'),
test: (error) => error is FormatException,
)
.catchError(
(error) => print('Unknown error: $error'),
);
}
Future.wait -- التنفيذ المتوازي
Future.wait يأخذ قائمة من Futures ويعيد Future واحد يكتمل عندما تكتمل جميعها. النتيجة هي قائمة بجميع قيمها. إذا فشل أي Future في القائمة، يفشل Future المجمّع.
تشغيل عدة Futures بالتوازي
Future<String> fetchUser() =>
Future.delayed(Duration(seconds: 2), () => 'Ahmed');
Future<List<String>> fetchPosts() =>
Future.delayed(Duration(seconds: 3), () => ['Post 1', 'Post 2']);
Future<int> fetchNotifications() =>
Future.delayed(Duration(seconds: 1), () => 5);
void main() {
final stopwatch = Stopwatch()..start();
Future.wait([
fetchUser(),
fetchPosts(),
fetchNotifications(),
]).then((results) {
print('User: ${results[0]}');
print('Posts: ${results[1]}');
print('Notifications: ${results[2]}');
print('Total time: ${stopwatch.elapsedMilliseconds}ms');
// ~3000ms وليس 6000ms -- لأنها عملت بالتوازي!
}).catchError((error) {
print('One of the operations failed: $error');
});
}
Future.wait كلما كان لديك عدة عمليات غير متزامنة مستقلة. تشغيلها بالتوازي بدلاً من التسلسل يمكن أن يقلل بشكل كبير من إجمالي وقت الانتظار. في المثال أعلاه، إجمالي الوقت ~3 ثوانٍ (أبطأ عملية) بدلاً من ~6 ثوانٍ (مجموع جميع العمليات).Future.any -- أول من يكتمل
Future.any يعيد نتيجة أول Future يكتمل بنجاح. هذا مفيد للسباق بين مصادر بيانات متعددة أو تنفيذ أنماط المهلة الزمنية.
السباق بين مصادر بيانات متعددة
Future<String> fetchFromCDN1() =>
Future.delayed(Duration(seconds: 3), () => 'Data from CDN 1');
Future<String> fetchFromCDN2() =>
Future.delayed(Duration(seconds: 1), () => 'Data from CDN 2');
Future<String> fetchFromCDN3() =>
Future.delayed(Duration(seconds: 2), () => 'Data from CDN 3');
void main() {
Future.any([
fetchFromCDN1(),
fetchFromCDN2(),
fetchFromCDN3(),
]).then((fastest) {
print(fastest); // Data from CDN 2 (اكتمل أولاً)
});
}
تنفيذ مهلة زمنية مع Future.any
Future<String> fetchWithTimeout(Future<String> operation, Duration timeout) {
return Future.any([
operation,
Future.delayed(timeout, () => throw TimeoutException(
'Operation timed out after ${timeout.inSeconds}s',
)),
]);
}
void main() {
final slowRequest = Future.delayed(
Duration(seconds: 10),
() => 'Slow data',
);
fetchWithTimeout(slowRequest, Duration(seconds: 3))
.then((data) => print('Got data: $data'))
.catchError((error) => print('Error: $error'));
// Error: TimeoutException بعد 3 ثوانٍ
}
مثال عملي: تحميل بيانات التطبيق
دعنا نبني مثالاً واقعيًا يحاكي تحميل البيانات للشاشة الرئيسية لتطبيق جوال. هذا يجمع بين عدة أنماط Future التي تعلمتها.
محمّل بيانات التطبيق
import 'dart:math';
// دوال API محاكاة
Future<Map<String, dynamic>> fetchUserProfile() {
return Future.delayed(Duration(milliseconds: 800), () {
return {'name': 'Layla Mahmoud', 'avatar': 'layla.png'};
});
}
Future<List<String>> fetchRecentOrders() {
return Future.delayed(Duration(milliseconds: 1200), () {
return ['Order #1001', 'Order #1002', 'Order #1003'];
});
}
Future<List<String>> fetchRecommendations() {
return Future.delayed(Duration(milliseconds: 600), () {
if (Random().nextBool()) {
throw Exception('Recommendation service unavailable');
}
return ['Product A', 'Product B', 'Product C'];
});
}
Future<int> fetchCartCount() {
return Future.delayed(Duration(milliseconds: 300), () => 3);
}
void main() {
print('Loading home screen data...');
final stopwatch = Stopwatch()..start();
// جلب البيانات الحرجة بالتوازي
Future.wait([
fetchUserProfile(),
fetchRecentOrders(),
fetchCartCount(),
]).then((results) {
final profile = results[0] as Map<String, dynamic>;
final orders = results[1] as List<String>;
final cartCount = results[2] as int;
print('\nWelcome, ${profile["name"]}!');
print('Recent orders: ${orders.join(", ")}');
print('Cart items: $cartCount');
// جلب بيانات غير حرجة بشكل منفصل (قد تفشل بأمان)
return fetchRecommendations()
.then((recs) {
print('Recommended: ${recs.join(", ")}');
})
.catchError((error) {
print('Recommendations unavailable (showing defaults)');
});
}).whenComplete(() {
print('\nHome screen loaded in ${stopwatch.elapsedMilliseconds}ms');
});
}
Future.wait والبيانات غير الحرجة مع معالجة أخطاء أنيقة شائع جدًا في تطبيقات Flutter الحقيقية. يرى المستخدم المحتوى الرئيسي بسرعة بينما تُحمّل الميزات الاختيارية أو تعود للقيم الافتراضية.Zone الحالي، وفي وضع التصحيح سيطبع Dart تحذيرًا. أضف دائمًا .catchError() أو غلّف كودك غير المتزامن في try-catch (مع async/await، والتي سنتناولها في الدرس التالي).الملخص
في هذا الدرس، تعلمت أساسيات البرمجة غير المتزامنة في Dart:
- حلقة الأحداث -- آلية Dart أحادية الخيط للتعامل مع العمليات غير المتزامنة
- Future -- يمثل قيمة ستكون متاحة لاحقًا
- Future.delayed -- ينشئ Future يكتمل بعد مدة
- Future.value / Future.error -- ينشئ Futures مكتملة فورًا
- .then() / .catchError() / .whenComplete() -- التسلسل ومعالجة الأخطاء
- Future.wait -- تشغيل عدة Futures بالتوازي
- Future.any -- يعيد أول Future يكتمل
في الدرس التالي، ستتعلم عن async/await -- صيغة أنظف للعمل مع Futures تجعل الكود غير المتزامن يبدو ويتصرف مثل الكود المتزامن.