Kodokon kodokon.com

اللاتزامن في Dart: Future وasync/await وFutureBuilder

أتقِن Future وasync/await وFutureBuilder دون تجميد الواجهة أو إعادة إطلاق طلباتك عن غير قصد.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

تُشغِّل Dart تطبيقك على خيط واحد، تقوده حلقة أحداث. إن Future ليس خيطًا: إنه وعد بقيمة قادمة. ولا يحجب await ذلك الخيط أبدًا - بل يعلّق الدالة الحالية، ويعيد التحكم إلى الحلقة (فتظل الواجهة تعمل)، ثم يستأنف عندما تصل النتيجة. ونتيجةً لذلك: لا يوازي async/await أيّ شيء في الحساب الصرف؛ أما العمل المُكثِّف لوحدة المعالجة (CPU)، فيتطلب عازلًا (isolate).

DART
Future<double> fetchRate() async {
  await Future<void>.delayed(
    const Duration(milliseconds: 300),
  );
  return 1.08;
}

Future<void> main() async {
  try {
    final rate = await fetchRate();
    print('EUR/USD rate: $rate');
  } on Exception catch (error) {
    print('Rate loading failed: $error');
  }
}
تُستهلَك الدالة اللاتزامنية باستخدام await وtry/catch.

يُنفَّذ استدعاءان متتاليان لـ await بالتسلسل: فلا يبدأ الثاني إلا بعد الأول. وعندما تكون العمليات مستقلة، أطلِقها معًا باستخدام Future.wait. والمقايضة التي ينبغي معرفتها: إن Future.wait سريع الفشل - فأول خطأ يرفض العملية بأكملها وتضيع نتائج الـ futures الأخرى، حتى وإن نجحت. فإذا احتجت نتائج جزئية، فعالِج الخطأ داخل كل future.

DART
Future<String> fetchProfile() async => 'profile';

Future<String> fetchOrders() async => 'orders';

Future<void> main() async {
  final [profile, orders] = await Future.wait(
    [fetchProfile(), fetchOrders()],
  );
  print('$profile / $orders');
}
الموازاة باستخدام Future.wait والتفكيك في Dart 3.

في جانب الواجهة، يربط FutureBuilder بين Future وbuild عبر snapshot. والمزلق الأول عمليًا: إنشاء الـ future مباشرةً داخل build. فكل إعادة بناء - ويكفي setState بسيط من أب - سيعيد عندئذٍ إطلاق الطلب. خزِّن الـ future في initState، واختبر دائمًا الخطأ قبل البيانات.

DART
import 'package:flutter/material.dart';

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() =>
      _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  late final Future<String> _nameFuture;

  @override
  void initState() {
    super.initState();
    _nameFuture = _loadName();
  }

  Future<String> _loadName() async {
    await Future<void>.delayed(
      const Duration(seconds: 1),
    );
    return 'Ada Lovelace';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: _nameFuture,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return Text('Error: ${snapshot.error}');
        }
        if (!snapshot.hasData) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }
        return Text(snapshot.data!);
      },
    );
  }
}
يُنشأ الـ future مرة واحدة فقط، في initState.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. ماذا يفعل await فعليًا على الخيط الرئيسي؟
    • يحجب الخيط حتى يُحَل Future.
    • ينقل الحساب تلقائيًا إلى خيط ثانوي.
    • يعلّق الدالة الحالية ويعيد التحكم إلى حلقة الأحداث: فتظل الواجهة تعمل.
  2. مع Future.wait([a(), b()])، ماذا يحدث إذا فشل a()؟
    • يرفض Future.wait مع خطأ a()؛ وتضيع نتيجة b().
    • ينتظر Future.wait انتهاء b()، ثم يُرجِع نتيجته وحدها.
    • يُتجاهَل الخطأ ما دام b() ينجح.
  3. لماذا نتجنّب كتابة future: fetchUser() مباشرةً داخل build مع FutureBuilder؟
    • لا يمكن أن يحتوي build على استدعاء لاتزامني، فلا يُصرَّف الكود.
    • ستنشئ كل إعادة بناء Future جديدًا، ومن ثمّ تعيد إطلاق طلب الشبكة.
    • سيظل snapshot عالقًا في حالة انتظار.