تخطَّ إلى المحتوى

البرمجة غير المتزامنة في Dart: async و await

الدرس 8 من 14 · Dart

تحميل صورة من الإنترنت، وقراءة ملف، وجلب بيانات من خادم: كلها عمليات تأخذ وقتاً. لو انتظر التطبيق كل عملية وهو متجمد، سيشعر المستخدم أن التطبيق معطل. الحل هو البرمجة غير المتزامنة (Asynchronous Programming): تبدأ العملية الطويلة، ويكمل التطبيق عمله، ثم تعود للنتيجة عندما تصل. وهذا من أهم الدروس التي ستحتاجها في Flutter.

تخيّل أنك في مطعم وطلبت وجبة تحتاج 20 دقيقة:

  • الطريقة المتزامنة: تقف أمام المطبخ 20 دقيقة لا تفعل شيئاً، والطابور خلفك يتوقف.
  • الطريقة غير المتزامنة: تأخذ جهاز تنبيه (Buzzer)، وتجلس مع أصدقائك. عندما تجهز الوجبة، يرن الجهاز فتذهب لتستلمها.

جهاز التنبيه هذا هو بالضبط Future في Dart.

Future هو وعد بقيمة ستصل لاحقاً. الدالة التي تأخذ وقتاً لا تُرجع النتيجة مباشرة، بل تُرجع Future:

Future<String> fetchUserName() {
return Future.delayed(Duration(seconds: 2), () => 'سارة');
}

Future<String> تعني: «وعد بنص سيصل في المستقبل». واستخدمنا Future.delayed لنحاكي عملية تأخذ ثانيتين، مثل طلب من الإنترنت.

لاحظ ماذا يحدث عند استدعائها مع then:

void main() {
print('1. بدأ البرنامج');
fetchUserName().then((name) => print('3. وصل الاسم: $name'));
print('2. البرنامج لم يتوقف!');
}
الناتج
1. بدأ البرنامج
2. البرنامج لم يتوقف!
3. وصل الاسم: سارة

السطر الثاني طُبع قبل وصول الاسم! البرنامج لم ينتظر، بل أكمل عمله، وعندما وصل الاسم بعد ثانيتين نُفذت الدالة داخل then.

async و await: كود يُقرأ بالترتيب

Section titled “async و await: كود يُقرأ بالترتيب”

then تعمل، لكن مع عمليات كثيرة متتالية يصبح الكود متداخلاً ومربكاً. الحل الأجمل: async و await:

Future<void> main() async {
print('جاري التحميل...');
var name = await fetchUserName();
print('أهلاً يا $name');
}
الناتج
جاري التحميل...
أهلاً يا سارة
  • async بعد أقواس الدالة: «هذه الدالة فيها عمليات غير متزامنة».
  • await قبل الـ Future: «انتظر هنا حتى تصل القيمة، ثم أكمل».
  • الدالة async تُرجع دائماً Future، لذلك كتبنا Future<void> main().

الكود الآن يُقرأ من الأعلى للأسفل كأنه عادي تماماً، لكنه لا يجمّد التطبيق: أثناء الانتظار، يستطيع التطبيق رسم الشاشة والاستجابة للمستخدم.

Future<int> fetchScore() async {
await Future.delayed(Duration(seconds: 1));
return 95;
}
Future<void> main() async {
var score = await fetchScore();
print('الدرجة: $score');
}
الناتج
الدرجة: 95

لاحظ: داخل الدالة نكتب return 95 عادياً، و Dart تغلفه تلقائياً في Future<int>.

العمليات عبر الإنترنت قد تفشل: انقطع الاتصال، أو الخادم لا يرد. استخدم try و catch تماماً كالكود العادي:

Future<String> fetchCourse(bool ok) async {
await Future.delayed(Duration(milliseconds: 500));
if (!ok) {
throw Exception('انقطع الاتصال بالإنترنت');
}
return 'Flutter';
}
Future<void> main() async {
try {
var c = await fetchCourse(false);
print(c);
} catch (e) {
print('حدث خطأ: $e');
} finally {
print('انتهت المحاولة');
}
}
الناتج
حدث خطأ: Exception: انقطع الاتصال بالإنترنت
انتهت المحاولة
  • throw ترمي خطأ، و catch تلتقطه، فلا ينهار البرنامج.
  • finally تُنفَّذ دائماً، نجحت العملية أو فشلت. مفيدة لإخفاء مؤشر التحميل مثلاً.

إذا كانت لديك عمليتان لا تعتمد إحداهما على الأخرى، فلماذا تنتظر الأولى ثم تبدأ الثانية؟ شغّلهما معاً:

var results = await Future.wait([fetchUserName(), fetchCourse(true)]);
print(results);
الناتج
[سارة, Flutter]

العملية الأولى تأخذ ثانيتين، والثانية نصف ثانية. معاً استغرقتا حوالي ثانيتين فقط بدلاً من ثانيتين ونصف، لأنهما عملتا في نفس الوقت.

Future تعطيك قيمة واحدة. أما Stream فتعطيك سلسلة من القيم عبر الوقت، مثل رسائل محادثة تصل واحدة بعد الأخرى، أو موقعك على الخريطة يتحدث كل ثانية:

Stream<int> countdown(int from) async* {
for (var i = from; i > 0; i--) {
await Future.delayed(Duration(milliseconds: 300));
yield i;
}
}
Future<void> main() async {
await for (var n in countdown(3)) {
print(n);
}
print('انطلق!');
}
الناتج
3
2
1
انطلق!
  • async* دالة تولّد Stream، و yield ترسل قيمة جديدة.
  • await for تستقبل القيم واحدة بواحدة كلما وصلت.

إذا كان Future مثل طرد بريدي واحد، فـ Stream مثل اشتراك في مجلة تصلك أعدادها تباعاً.

  1. اكتب دالة Future<String> downloadFile(String name) تنتظر ثانية ثم تُرجع «تم تحميل …».
  2. في main حمّل ثلاثة ملفات بالتتابع بـ await، واحسب الوقت بـ Stopwatch.
  3. ثم حمّلها معاً بـ Future.wait وقارن الوقت.
  4. تحدٍّ: اجعل الدالة ترمي خطأ إذا كان اسم الملف فارغاً، والتقطه بـ try و catch.
  • نسيان await: تطبع Instance of 'Future<String>' بدلاً من القيمة.
  • استخدام await خارج دالة async: خطأ يرفضه المحرر.
  • عدم معالجة الأخطاء: أي طلب عبر الإنترنت قد يفشل. ضع try و catch حول العمليات الحساسة دائماً.

سؤال

ماذا يمثل Future<int>؟

سؤال

ما فائدة Future.wait؟

  • العمليات الطويلة لا تجمّد التطبيق، بل تُرجع Future: وعداً بقيمة لاحقة.
  • async تعلّم الدالة، و await تنتظر النتيجة، والكود يُقرأ بالترتيب.
  • try و catch و finally لمعالجة فشل العمليات.
  • Future.wait لتشغيل عدة عمليات مستقلة معاً.
  • Stream لسلسلة قيم عبر الوقت، وله درس كامل في آخر الكورس.