مرحباً بكم في هذا الدرس الاحترافي! اليوم سنتعمق في فهم البرمجة غير المتزامنة في 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:
- ستطبع الرسائل الأولى من
mainفوراً، بما في ذلك رسالة"العمليات الأخرى في main تستمر..."، مما يبرهن على أنfetchData()تبدأ في الخلفية دون حظر. - بعد ثانيتين، ستظهر نتيجة
fetchData().then(). - ثم سيبدأ الجزء الخاص بـ
async/await، وستتوقفmainمؤقتاً عندawait fetchUserData()حتى يتم جلب البيانات (أو يحدث خطأ). ستلاحظ أن الرسائل الداخلية لـfetchUserDataستظهر قبل رسالة"تم استلام بيانات المستخدم". - أخيراً، سيبدأ جزء
Future.wait(). ستلاحظ أن المهام الثلاث (fetchUserData،fetchAppSettings،fetchData) ستبدأ في نفس الوقت تقريباً، وستنتظرFuture.wait()حتى تكتمل أطول مهمة منها قبل طباعة النتائج المجمعة. - إذا كان رقم الثواني في وقت تشغيل دالة
fetchUserDataزوجياً، فستظهر رسالة خطأ بدلاً من بيانات المستخدم، مما يوضح معالجة الأخطاء الفعالة باستخدامtry-catch.
المخرجات الدقيقة قد تختلف قليلاً في الترتيب الزمني لبعض الرسائل الجانبية حسب توقيت التشغيل، خاصةً مع دالة fetchUserData التي تعتمد على رقم الثواني الحالي للفشل أو النجاح، ولكن النمط العام لتنفيذ المهام غير المتزامنة سيبقى واضحاً.