ميزات Dart المتقدمة

async/await بالتفصيل

50 دقيقة الدرس 2 من 16

من الاستدعاءات الراجعة إلى async/await

في الدرس السابق، تعلمت تسلسل Futures باستخدام .then() و .catchError() و .whenComplete(). بينما هذا النهج القائم على الاستدعاءات الراجعة قوي، يمكن أن يصبح صعب القراءة عندما يكون لديك عمليات معتمدة كثيرة. يوفر Dart الكلمات المفتاحية async و await التي تتيح لك كتابة كود غير متزامن يبدو ويُقرأ مثل الكود المتزامن.

الاستدعاءات الراجعة مقابل async/await -- جنبًا إلى جنب

// نمط الاستدعاءات الراجعة (من الدرس السابق)
void loadDataCallbacks() {
  fetchUserId()
      .then((userId) => fetchProfile(userId))
      .then((profile) => fetchPosts(profile.id))
      .then((posts) => print('Got ${posts.length} posts'))
      .catchError((e) => print('Error: $e'));
}

// نمط async/await (أنظف!)
Future<void> loadDataAsync() async {
  try {
    final userId = await fetchUserId();
    final profile = await fetchProfile(userId);
    final posts = await fetchPosts(profile.id);
    print('Got ${posts.length} posts');
  } catch (e) {
    print('Error: $e');
  }
}
ملاحظة: كلا النهجين ينتجان سلوكًا متطابقًا. نسخة async/await هي سكر نحوي يحوله مترجم Dart إلى كود قائم على الاستدعاءات الراجعة تحت الغطاء. اختر async/await لسهولة القراءة، خاصة عندما تعتمد العمليات على بعضها البعض.

الكلمة المفتاحية async

توضع الكلمة المفتاحية async قبل جسم الدالة لوسمها كغير متزامنة. دالة async تعيد دائمًا Future، حتى لو أعدت قيمة عادية.

إعلان دوال async

// تعيد Future<String> تلقائيًا
Future<String> greet(String name) async {
  return 'Hello, $name!';
}

// حتى بدون نوع إرجاع صريح، تعيد Future
Future<int> computeScore() async {
  return 42 * 3;
}

// دوال async من نوع void تعيد Future<void>
Future<void> logMessage(String msg) async {
  print('[LOG] $msg');
}

void main() async {
  final greeting = await greet('Ahmed');
  print(greeting);  // Hello, Ahmed!

  final score = await computeScore();
  print(score);  // 126
}
تحذير: دالة async التي تعيد قيمة عادية لا تزال تغلفها في Future. إذا كتبت return 'hello'; داخل دالة async، يستقبل المستدعي Future<String> وليس String. يجب عليك استخدام await أو .then() للحصول على القيمة الفعلية.

الكلمة المفتاحية await

الكلمة المفتاحية await توقف تنفيذ الدالة async الحالية حتى يكتمل Future، ثم تعيد النتيجة. يمكن استخدامها فقط داخل دوال async.

استخدام await

Future<String> fetchUserName() {
  return Future.delayed(Duration(seconds: 1), () => 'Sara');
}

Future<int> fetchUserAge() {
  return Future.delayed(Duration(seconds: 1), () => 28);
}

Future<void> displayUser() async {
  print('Loading user...');

  // يتوقف التنفيذ هنا حتى يكتمل fetchUserName
  final name = await fetchUserName();
  print('Name: $name');

  // ثم يتوقف هنا حتى يكتمل fetchUserAge
  final age = await fetchUserAge();
  print('Age: $age');

  print('User loaded!');
}

void main() {
  displayUser();
  print('Main continues while displayUser runs...');
}

// الإخراج:
// Loading user...
// Main continues while displayUser runs...
// (بعد ثانية واحدة)
// Name: Sara
// (بعد ثانية أخرى)
// Age: 28
// User loaded!
نصيحة: الكلمة المفتاحية await توقف فقط الدالة غير المتزامنة الحالية، وليس البرنامج بأكمله. الكود الآخر ومعالجات الأحداث وتحديثات واجهة المستخدم تستمر في العمل أثناء الانتظار. لهذا السبب تبقى تطبيقات Flutter سريعة الاستجابة حتى أثناء انتظار استدعاءات الشبكة.

معالجة الأخطاء مع try-catch-finally

واحدة من أكبر مزايا async/await هي أنك تستطيع استخدام كتل try-catch-finally القياسية لمعالجة الأخطاء، تمامًا كما مع الكود المتزامن.

try-catch مع async/await

Future<String> fetchData() {
  return Future.delayed(Duration(seconds: 1), () {
    throw Exception('Server returned 500');
  });
}

Future<void> loadData() async {
  try {
    final data = await fetchData();
    print('Data: $data');
  } on FormatException catch (e) {
    print('Format error: $e');
  } on Exception catch (e) {
    print('General error: $e');
  } catch (e) {
    print('Unknown error: $e');
  } finally {
    print('Cleanup complete');
  }
}

void main() async {
  await loadData();
  // الإخراج:
  // General error: Exception: Server returned 500
  // Cleanup complete
}

أنواع أخطاء متعددة

class NetworkException implements Exception {
  final String message;
  final int statusCode;
  NetworkException(this.message, this.statusCode);

  @override
  String toString() => 'NetworkException($statusCode): $message';
}

class AuthException implements Exception {
  final String message;
  AuthException(this.message);

  @override
  String toString() => 'AuthException: $message';
}

Future<Map<String, dynamic>> fetchProtectedData(String token) async {
  if (token.isEmpty) {
    throw AuthException('No authentication token provided');
  }
  if (token == 'expired') {
    throw AuthException('Token has expired');
  }

  return Future.delayed(Duration(seconds: 1), () {
    return {'data': 'Secret information', 'timestamp': DateTime.now().toString()};
  });
}

Future<void> loadProtectedData() async {
  try {
    final data = await fetchProtectedData('expired');
    print('Got data: $data');
  } on AuthException catch (e) {
    print('Auth failed: $e');
    // إعادة التوجيه لشاشة تسجيل الدخول
  } on NetworkException catch (e) {
    print('Network issue ($e.statusCode): ${e.message}');
    // إظهار زر إعادة المحاولة
  } catch (e) {
    print('Unexpected error: $e');
  }
}

التنفيذ التسلسلي مقابل المتوازي

مهارة حاسمة مع async/await هي معرفة متى تشغل العمليات بالتسلسل مقابل بالتوازي. التسلسل ضروري عندما تعتمد العمليات على بعضها البعض. التوازي أفضل عندما تكون العمليات مستقلة.

التنفيذ التسلسلي (بطيء)

Future<String> fetchUser() =>
    Future.delayed(Duration(seconds: 2), () => 'Ahmed');

Future<List<String>> fetchPosts() =>
    Future.delayed(Duration(seconds: 3), () => ['Post 1', 'Post 2']);

Future<int> fetchFollowers() =>
    Future.delayed(Duration(seconds: 1), () => 150);

// بطيء: 2 + 3 + 1 = 6 ثوانٍ إجمالاً
Future<void> loadSequential() async {
  final stopwatch = Stopwatch()..start();

  final user = await fetchUser();        // ينتظر 2 ثانية
  final posts = await fetchPosts();      // ثم ينتظر 3 ثوانٍ
  final followers = await fetchFollowers(); // ثم ينتظر 1 ثانية

  print('User: $user, Posts: ${posts.length}, Followers: $followers');
  print('Time: ${stopwatch.elapsedMilliseconds}ms');  // ~6000ms
}

التنفيذ المتوازي (سريع)

// سريع: max(2, 3, 1) = 3 ثوانٍ إجمالاً
Future<void> loadParallel() async {
  final stopwatch = Stopwatch()..start();

  // بدء جميع العمليات الثلاث في نفس الوقت
  final results = await Future.wait([
    fetchUser(),
    fetchPosts(),
    fetchFollowers(),
  ]);

  print('User: ${results[0]}');
  print('Posts: ${(results[1] as List).length}');
  print('Followers: ${results[2]}');
  print('Time: ${stopwatch.elapsedMilliseconds}ms');  // ~3000ms
}

// أفضل: نتائج متوازية مسماة باستخدام السجلات (Dart 3)
Future<void> loadParallelNamed() async {
  final (user, posts, followers) = await (
    fetchUser(),
    fetchPosts(),
    fetchFollowers(),
  ).wait;

  print('User: $user, Posts: ${posts.length}, Followers: $followers');
}
تحذير: خطأ شائع جدًا هو كتابة كود تسلسلي عن طريق الخطأ عندما يكون التوازي مناسبًا. إذا كان استدعاءان await لا يعتمدان على بعضهما البعض، فكر في استخدام Future.wait أو تفكيك السجلات في Dart 3 لتشغيلهما بالتوازي. هذا يمكن أن يقلل أوقات التحميل بشكل كبير.

المولدات غير المتزامنة مع async*

كما تجعل async الدالة تعيد Future، تجعل الكلمة المفتاحية async* الدالة تعيد Stream. داخل دالة async*، تستخدم yield لإصدار القيم واحدة تلو الأخرى.

مولد async* أساسي

Stream<int> countDown(int from) async* {
  for (int i = from; i >= 0; i--) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}

void main() async {
  print('Countdown starting...');

  await for (final number in countDown(5)) {
    print(number);
  }

  print('Liftoff!');
}

// الإخراج (واحد في الثانية):
// Countdown starting...
// 5
// 4
// 3
// 2
// 1
// 0
// Liftoff!

yield* -- التفويض لتيار آخر

Stream<String> fetchPages(String query) async* {
  int page = 1;
  while (page <= 3) {
    await Future.delayed(Duration(milliseconds: 500));
    yield 'Page $page results for "$query"';
    page++;
  }
}

Stream<String> searchAll(List<String> queries) async* {
  for (final query in queries) {
    yield '--- Searching: $query ---';
    yield* fetchPages(query);  // يفوض جميع القيم من fetchPages
  }
}

void main() async {
  await for (final result in searchAll(['dart', 'flutter'])) {
    print(result);
  }
}
ملاحظة: yield يصدر قيمة واحدة. yield* (yield-star) يفوض لتيار أو Iterable آخر، ويمرر جميع قيمه. دالة async* تتوقف عند كل نقطة yield حتى يكون المستمع جاهزًا للقيمة التالية.

تحويل الاستدعاءات الراجعة إلى Futures

العديد من واجهات برمجة التطبيقات القديمة والمكتبات الخارجية تستخدم أنماطًا قائمة على الاستدعاءات الراجعة. يمكنك تغليفها في Futures باستخدام Completer لجعلها متوافقة مع async/await.

استخدام Completer لتغليف الاستدعاءات الراجعة

import 'dart:async';

// تخيل أن هذا API قديم قائم على الاستدعاءات الراجعة
void oldApiCall(String url, Function(String) onSuccess, Function(String) onError) {
  Future.delayed(Duration(seconds: 1), () {
    if (url.startsWith('https')) {
      onSuccess('Data from $url');
    } else {
      onError('Insecure URL: $url');
    }
  });
}

// تغليفه في Future باستخدام Completer
Future<String> modernApiCall(String url) {
  final completer = Completer<String>();

  oldApiCall(
    url,
    (data) => completer.complete(data),
    (error) => completer.completeError(Exception(error)),
  );

  return completer.future;
}

void main() async {
  try {
    final data = await modernApiCall('https://api.example.com/users');
    print(data);  // Data from https://api.example.com/users
  } catch (e) {
    print('Error: $e');
  }

  try {
    final data = await modernApiCall('http://insecure.example.com');
    print(data);
  } catch (e) {
    print('Error: $e');  // Error: Exception: Insecure URL: http://insecure.example.com
  }
}
تحذير: يمكن إكمال Completer مرة واحدة فقط. استدعاء complete() أو completeError() مرة ثانية يطرح StateError. تأكد دائمًا من أن منطق الاستدعاء الراجع يستدعي واحدة فقط من هذه الطرق مرة واحدة فقط.

أنماط معالجة الأخطاء

تحتاج التطبيقات الواقعية إلى معالجة أخطاء قوية. إليك بعض الأنماط الشائعة المستخدمة في كود Dart و Flutter الإنتاجي.

نمط إعادة المحاولة

Future<T> retry<T>(
  Future<T> Function() operation, {
  int maxAttempts = 3,
  Duration delay = const Duration(seconds: 1),
}) async {
  int attempt = 0;

  while (true) {
    attempt++;
    try {
      return await operation();
    } catch (e) {
      if (attempt >= maxAttempts) {
        print('All $maxAttempts attempts failed. Giving up.');
        rethrow;  // طرح الخطأ الأخير
      }
      print('Attempt $attempt failed: $e. Retrying in ${delay.inSeconds}s...');
      await Future.delayed(delay * attempt);  // تراجع أسي
    }
  }
}

// الاستخدام
int callCount = 0;

Future<String> unreliableApi() async {
  callCount++;
  if (callCount < 3) {
    throw Exception('Server busy');
  }
  return 'Success on attempt $callCount!';
}

void main() async {
  try {
    final result = await retry(() => unreliableApi());
    print(result);  // Success on attempt 3!
  } catch (e) {
    print('Failed: $e');
  }
}

خط أنابيب جلب البيانات مع بديل احتياطي

Future<String> fetchFromPrimary() async {
  await Future.delayed(Duration(seconds: 1));
  throw Exception('Primary server down');
}

Future<String> fetchFromBackup() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Data from backup server';
}

Future<String> fetchFromCache() async {
  return 'Stale data from cache';
}

Future<String> fetchWithFallback() async {
  try {
    return await fetchFromPrimary();
  } catch (e) {
    print('Primary failed: $e');
  }

  try {
    return await fetchFromBackup();
  } catch (e) {
    print('Backup failed: $e');
  }

  // الملاذ الأخير
  print('Using cached data');
  return await fetchFromCache();
}

void main() async {
  final data = await fetchWithFallback();
  print('Got: $data');
  // الإخراج:
  // Primary failed: Exception: Primary server down
  // Got: Data from backup server
}
نصيحة: نمط إعادة المحاولة مع التراجع الأسي (ضرب التأخير بعدد المحاولات) هو من أفضل الممارسات في الصناعة للتعامل مع الأعطال المؤقتة مثل انقطاعات الشبكة أو تحديد معدل الطلبات. يعطي الخادم وقتًا للتعافي بين المحاولات.

مثال عملي: خط أنابيب جلب البيانات

دعنا نبني خط أنابيب بيانات كاملاً يوضح العمليات التسلسلية والتحميل المتوازي ومعالجة الأخطاء ومنطق إعادة المحاولة تعمل معًا.

خط أنابيب بيانات كامل

import 'dart:async';

// النماذج
class User {
  final String id;
  final String name;
  User(this.id, this.name);
}

class Post {
  final String title;
  final int likes;
  Post(this.title, this.likes);
}

// محاكاة API
Future<String> authenticate(String email, String password) async {
  await Future.delayed(Duration(milliseconds: 500));
  if (email.isEmpty || password.isEmpty) {
    throw AuthException('Credentials required');
  }
  return 'token_abc123';
}

Future<User> fetchUser(String token) async {
  await Future.delayed(Duration(milliseconds: 800));
  return User('u1', 'Omar Al-Rashid');
}

Future<List<Post>> fetchUserPosts(String userId) async {
  await Future.delayed(Duration(milliseconds: 1000));
  return [
    Post('Getting Started with Dart', 42),
    Post('Flutter State Management', 88),
    Post('Async Programming Guide', 156),
  ];
}

Future<Map<String, int>> fetchAnalytics(String userId) async {
  await Future.delayed(Duration(milliseconds: 700));
  return {'views': 12500, 'followers': 340, 'engagement': 78};
}

class AuthException implements Exception {
  final String message;
  AuthException(this.message);
  @override
  String toString() => message;
}

// خط الأنابيب الرئيسي
Future<void> loadDashboard(String email, String password) async {
  final stopwatch = Stopwatch()..start();
  print('=== Dashboard Loading ===\n');

  try {
    // الخطوة 1: المصادقة (يجب أن تكتمل أولاً)
    print('Authenticating...');
    final token = await authenticate(email, password);
    print('Authenticated! (${stopwatch.elapsedMilliseconds}ms)\n');

    // الخطوة 2: جلب المستخدم (يعتمد على الرمز)
    final user = await fetchUser(token);
    print('Welcome, ${user.name}! (${stopwatch.elapsedMilliseconds}ms)\n');

    // الخطوة 3: جلب المنشورات والتحليلات بالتوازي (كلاهما يعتمد على المستخدم)
    print('Loading dashboard data...');
    final (posts, analytics) = await (
      fetchUserPosts(user.id),
      fetchAnalytics(user.id),
    ).wait;

    // عرض النتائج
    print('\nYour Posts:');
    for (final post in posts) {
      print('  - ${post.title} (${post.likes} likes)');
    }

    print('\nAnalytics:');
    analytics.forEach((key, value) {
      print('  $key: $value');
    });

    print('\nDashboard loaded in ${stopwatch.elapsedMilliseconds}ms');
  } on AuthException catch (e) {
    print('Authentication failed: $e');
  } catch (e) {
    print('Dashboard error: $e');
  }
}

void main() async {
  await loadDashboard('omar@example.com', 'password123');
}
ملاحظة: لاحظ كيف يستخدم خط الأنابيب await تسلسلي للعمليات المعتمدة (المصادقة قبل جلب المستخدم) وتنفيذ متوازي مع تفكيك السجلات للعمليات المستقلة (المنشورات والتحليلات). هذا هو النمط الأمثل لتحميل البيانات في العالم الحقيقي.

الملخص

في هذا الدرس، أتقنت async/await في Dart:

  • async -- يوسم الدالة كغير متزامنة، ويعيد Future
  • await -- يوقف التنفيذ حتى يكتمل Future
  • try-catch-finally -- معالجة أخطاء قياسية مع الكود غير المتزامن
  • التسلسلي مقابل المتوازي -- استخدم await للعمليات المعتمدة، Future.wait للمستقلة
  • async* / yield -- مولدات غير متزامنة تنتج تيارات
  • Completer -- يغلف واجهات برمجة التطبيقات القائمة على الاستدعاءات الراجعة في Futures
  • أنماط إعادة المحاولة والبديل الاحتياطي -- معالجة أخطاء بمستوى إنتاجي

في الدرس التالي، ستتعمق في التيارات (Streams) -- تجريد Dart القوي للتعامل مع تسلسلات من الأحداث غير المتزامنة.