البرمجة غير المتزامنة (Asynchronous): فهم Future و async/await


مرحباً بكم في هذا الدرس الاحترافي! اليوم سنتعمق في فهم البرمجة غير المتزامنة في Dart، وكيفية استخدام Future و async/await لبناء تطبيقات سلسة وفعالة تستجيب للمستخدم دائماً.

1. فهم أساسيات البرمجة غير المتزامنة و Future

البرمجة غير المتزامنة تسمح لتطبيقك بتنفيذ مهام تستغرق وقتاً طويلاً (مثل جلب البيانات من الإنترنت أو قراءة ملف) دون حظر الواجهة الرسومية أو تعطيل تجربة المستخدم. في Dart، يتم تمثيل هذه المهام بواسطة الكائنات من نوع Future.

Future هو كائن يمثل نتيجة عملية قد لا تكون مكتملة بعد. يمكن أن تنجح العملية (وتعود بقيمة) أو تفشل (وتعود بخطأ). عند اكتمالها، تقوم بتسليم قيمتها أو خطئها عبر آلية تسمى event loop.

لنبدأ بمثال بسيط يوضح كيف تعمل Future وكيف يمكننا التعامل مع نتائجها باستخدام .then().

import 'dart:io'; // لاستخدام sleep (لأغراض المحاكاة في الأمثلة الطرفية)

void main() {
  print('بدء العملية الرئيسية...');

  // تعريف دالة ترجع Future<String>
  Future<String> fetchData() {
    // محاكاة لعملية تستغرق وقتاً طويلاً (مثل طلب شبكة)
    return Future.delayed(Duration(seconds: 2), () {
      print('جلب البيانات من السيرفر...');
      return 'البيانات جاهزة!'; // القيمة التي ستعود بها الـ Future عند اكتمالها
    });
  }

  // استدعاء الدالة غير المتزامنة والتعامل مع نتيجتها
  // .then() يتم تنفيذها عندما يكتمل الـ Future بنجاح
  fetchData().then((data) {
    print('تم استلام البيانات: $data');
  }).catchError((error) {
    // .catchError() يتم تنفيذها إذا فشل الـ Future
    print('حدث خطأ: $error');
  });

  print('العملية الرئيسية مستمرة...');
  // يمكن للعمليات الأخرى أن تستمر هنا دون انتظار fetchData
  sleep(Duration(seconds: 1)); // محاكاة لعمل آخر في الـ main thread
  print('العملية الرئيسية انتهت.');
}

في هذا الجزء، نرى كيف أن fetchData() تبدأ، لكن 'العملية الرئيسية مستمرة...' تطبع فوراً لأن fetchData() لا تحظر التنفيذ. عندما تنتهي fetchData() بعد ثانيتين، يتم تنفيذ الكتلة داخل .then()، مما يضمن أن تطبيقك يظل مستجيباً.

2. تبسيط الكود باستخدام async و await

على الرغم من أن .then() مفيدة، إلا أن التعامل مع سلاسل طويلة من Future يمكن أن يؤدي إلى ما يسمى "callback hell" (جحيم الاستدعاءات) أو يجعل الكود أقل قابلية للقراءة. هنا يأتي دور الكلمتين المفتاحيتين async و await لتبسيط الكود وجعله يبدو وكأنه متزامن.

تُستخدم الكلمة المفتاحية async لتعريف دالة على أنها دالة غير متزامنة، مما يعني أنها يمكن أن تحتوي على عمليات await. تُستخدم الكلمة المفتاحية await لانتظار اكتمال Future قبل الاستمرار في تنفيذ بقية الكود في الدالة async، دون حظر مؤشر الترابط (thread) الرئيسي.

لنعد كتابة المثال السابق باستخدام async/await:

import 'dart:io';

// دالة غير متزامنة لجلب البيانات
Future<String> fetchDataAsync() async {
  print('بدء جلب البيانات بشكل غير متزامن...');
  // await هنا يعني "انتظر حتى تنتهي هذه الـ Future ثم تابع"، لكن دون حظر الـ thread الرئيسي
  await Future.delayed(Duration(seconds: 3)); // محاكاة تأخير أطول
  print('تم جلب البيانات من السيرفر بشكل غير متزامن.');
  return 'البيانات الجديدة جاهزة!';
}

void main() async { // يجب أن تكون main دالة async لاستخدام await بداخلها
  print('بدء العملية الرئيسية (async)...');

  // استدعاء الدالة غير المتزامنة والانتظار لنتيجتها
  // هنا يتوقف تنفيذ main مؤقتًا حتى تعود fetchDataAsync بقيمة
  String data = await fetchDataAsync(); 
  print('تم استلام البيانات (async): $data');

  print('العملية الرئيسية (async) مستمرة بعد الانتظار...');
  sleep(Duration(seconds: 1));
  print('العملية الرئيسية (async) انتهت.');
}

في هذا المثال، على الرغم من أن main هي دالة async، فإن استخدام await يجعلها تنتظر fetchDataAsync(). الفرق الجوهري هو أن await لا يحظر الـ thread الرئيسي للتطبيق، بل يسمح لـ Dart بتنفيذ مهام أخرى (مثل تحديث الواجهة الرسومية) بينما تنتظر Future. عندما تكتمل Future، يستأنف تنفيذ الدالة async من حيث توقفت، مما يجعل الكود سهل القراءة وكأنه متزامن.

3. التعامل مع الأخطاء والمزامنة المتعددة

عند استخدام async/await، يمكننا التعامل مع الأخطاء باستخدام كتل try-catch تماماً كما نفعل مع الكود المتزامن، مما يجعل إدارة الأخطاء أكثر وضوحاً. سنتعلم أيضاً كيفية تنفيذ عدة مهام غير متزامنة بالتوازي لزيادة الكفاءة.

import 'dart:io';

// دالة غير متزامنة قد تفشل
Future<String> fetchUserData() async {
  print('بدء جلب بيانات المستخدم...');
  await Future.delayed(Duration(seconds: 2));
  // محاكاة فشل في بعض الأحيان
  if (DateTime.now().second % 2 == 0) { // إذا كان رقم الثواني زوجي، نفشل عمدًا
    throw Exception('فشل في جلب بيانات المستخدم!');
  }
  return 'اسم المستخدم: أحمد، البريد: ahmed@example.com';
}

// دالة غير متزامنة لجلب إعدادات التطبيق
Future<String> fetchAppSettings() async {
  print('بدء جلب إعدادات التطبيق...');
  await Future.delayed(Duration(seconds: 1));
  return 'Theme: Dark, Language: Arabic';
}

void main() async {
  print('بدء العملية الرئيسية مع معالجة الأخطاء والمهام المتوازية...');

  try {
    // تنفيذ مهمتين غير متزامنتين بالتوازي باستخدام Future.wait
    // Future.wait تنتظر اكتمال جميع الـ Futures في القائمة وترجع قائمة بالنتائج بنفس الترتيب
    List<String> results = await Future.wait([
      fetchUserData(),      // قد تفشل هذه الـ Future
      fetchAppSettings(),
    ]);

    print('تم استلام جميع النتائج بنجاح:');
    print('  - بيانات المستخدم: ${results[0]}');
    print('  - إعدادات التطبيق: ${results[1]}');

  } catch (e) {
    // التعامل مع أي خطأ يحدث في أي من الـ Futures المراقبة بواسطة Future.wait
    print('حدث خطأ أثناء جلب البيانات: $e');
  }

  print('العملية الرئيسية انتهت بعد معالجة الأخطاء.');
}

في هذا الجزء، استخدمنا try-catch لمعالجة الأخطاء المحتملة من fetchUserData()، مما يجعل الكود أكثر قوة. كما قدمنا Future.wait()، وهي طريقة قوية لتشغيل عدة Future في نفس الوقت والانتظار حتى تكتمل جميعها، مما يحسن من أداء التطبيق بشكل كبير من خلال الاستفادة من التوازي في المهام غير المتزامنة.

الكود النهائي الكامل

إليك الكود كاملاً والذي يجمع بين المفاهيم التي تعلمناها، مع تعليقات إضافية لتوضيح كل جزء:

import 'dart:io'; // لاستخدام sleep (لأغراض المحاكاة في الأمثلة الطرفية)

// دالة غير متزامنة لجلب البيانات الأساسية (تستخدم مع .then() في البداية)
Future<String> fetchData() {
  print('--> [fetchData] جلب البيانات الأساسية من السيرفر (بعد 2 ثانية)...');
  return Future.delayed(Duration(seconds: 2), () {
    return 'البيانات الأساسية جاهزة!';
  });
}

// دالة غير متزامنة قد تفشل أحياناً (تستخدم مع async/await و try-catch)
Future<String> fetchUserData() async {
  print('--> [fetchUserData] بدء جلب بيانات المستخدم (بعد 2 ثانية)...');
  await Future.delayed(Duration(seconds: 2));
  if (DateTime.now().second % 2 == 0) { // تفشل إذا كانت الثواني زوجية (لإظهار معالجة الخطأ)
    throw Exception('فشل في جلب بيانات المستخدم!');
  }
  return 'اسم المستخدم: سارة، البريد: sara@example.com';
}

// دالة غير متزامنة لجلب إعدادات التطبيق (تستخدم مع Future.wait() للتوازي)
Future<String> fetchAppSettings() async {
  print('--> [fetchAppSettings] بدء جلب إعدادات التطبيق (بعد 1 ثانية)...');
  await Future.delayed(Duration(seconds: 1));
  return 'Theme: Light, Language: English';
}

void main() async {
  print('--- بدء الدرس: البرمجة غير المتزامنة ---');

  // الجزء الأول: استخدام Future.then() للتعامل مع اكتمال الـ Future
  print('\n--- استخدام Future.then() ---\n');
  fetchData().then((data) {
    print('*** تم استلام البيانات الأساسية (then): $data');
  }).catchError((error) {
    print('*** خطأ في البيانات الأساسية (then): $error');
  });
  print('العمليات الأخرى في main تستمر بينما Future تعمل في الخلفية...');
  sleep(Duration(seconds: 1)); // محاكاة عمل آخر في الـ main thread

  // الجزء الثاني: استخدام async/await لتبسيط التعامل مع الـ Futures
  print('\n--- استخدام async/await ---\n');
  try {
    print('بدء عملية جلب بيانات المستخدم بـ async/await...');
    String userData = await fetchUserData(); // هنا يتوقف تنفيذ main مؤقتًا (دون حظر الـ thread)
    print('*** تم استلام بيانات المستخدم (await): $userData');
  } catch (e) {
    print('*** خطأ في بيانات المستخدم (await): $e');
  }

  // الجزء الثالث: مهام متوازية مع Future.wait() لتحسين الأداء
  print('\n--- استخدام Future.wait() لمهام متوازية ---\n');
  try {
    print('بدء جلب بيانات المستخدم وإعدادات التطبيق وبيانات إضافية بالتوازي...');
    // Future.wait تنتظر اكتمال كل الـ Futures في القائمة
    List<String> allResults = await Future.wait([
      fetchUserData(),      // قد تفشل هذه الـ Future
      fetchAppSettings(),
      fetchData(),
    ]);
    print('*** تم استلام جميع النتائج المتوازية بنجاح:');
    allResults.forEach((res) => print('    - $res'));
  } catch (e) {
    print('*** حدث خطأ في إحدى المهام المتوازية (Future.wait): $e');
  }

  print('\n--- انتهاء الدرس: البرمجة غير المتزامنة ---');
}

النتيجة المتوقعة

عند تشغيل السكربت، ستلاحظ تسلسلاً للأحداث يوضح كيفية عمل البرمجة غير المتزامنة في Dart:

  1. ستطبع الرسائل الأولى من main فوراً، بما في ذلك رسالة "العمليات الأخرى في main تستمر..."، مما يبرهن على أن fetchData() تبدأ في الخلفية دون حظر.
  2. بعد ثانيتين، ستظهر نتيجة fetchData().then().
  3. ثم سيبدأ الجزء الخاص بـ async/await، وستتوقف main مؤقتاً عند await fetchUserData() حتى يتم جلب البيانات (أو يحدث خطأ). ستلاحظ أن الرسائل الداخلية لـ fetchUserData ستظهر قبل رسالة "تم استلام بيانات المستخدم".
  4. أخيراً، سيبدأ جزء Future.wait(). ستلاحظ أن المهام الثلاث (fetchUserData، fetchAppSettings، fetchData) ستبدأ في نفس الوقت تقريباً، وستنتظر Future.wait() حتى تكتمل أطول مهمة منها قبل طباعة النتائج المجمعة.
  5. إذا كان رقم الثواني في وقت تشغيل دالة fetchUserData زوجياً، فستظهر رسالة خطأ بدلاً من بيانات المستخدم، مما يوضح معالجة الأخطاء الفعالة باستخدام try-catch.

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