الـ Streams في Dart: سلسلة قيم تصل مع الوقت
في درس async و await تعرفت على Future: وعد بقيمة واحدة تصل لاحقاً. لكن ماذا عن رسائل المحادثة التي تصل واحدة بعد الأخرى؟ أو موقع السائق الذي يتحدث كل ثانية؟ هذه سلسلة قيم تصل مع الوقت، واسمها في Dart هو Stream (التدفق). في هذا الدرس، وهو الأخير في الكورس، ستتعلم كيف تنشئ تدفقاً وتستمع له وتتحكم فيه.
Future أم Stream؟
Section titled “Future أم Stream؟”تخيّل الفرق بين طرد بريدي واشتراك في مجلة. الطرد يصل مرة واحدة وينتهي الأمر، وهذا Future. أما المجلة فيصلك عدد جديد منها كل شهر حتى ينتهي الاشتراك، وهذا Stream.
Future |
Stream |
|
|---|---|---|
| عدد القيم | واحدة | صفر أو أكثر، مع الوقت |
| طريقة الانتظار | await |
await for أو listen |
| مثال | جلب ملف المستخدم | رسائل المحادثة |
إنشاء تدفق: async* و yield
Section titled “إنشاء تدفق: async* و yield”أسهل طريقة لصنع تدفق هي دالة معلَّمة بـ async* (لاحظ النجمة). داخلها نستخدم yield لإرسال قيمة، والدالة تكمل عملها بعد كل إرسال، عكس return التي تنهيها:
Stream<int> countdown(int from) async* { for (var i = from; i >= 1; i--) { await Future.delayed(Duration(milliseconds: 300)); yield i; }}
Future<void> main() async { await for (var value in countdown(3)) { print(value); } print('انطلاق!');}321انطلاق!الحلقة await for تشبه for-in العادية، لكنها تنتظر وصول كل قيمة. ولا يُطبع «انطلاق!» إلا بعد أن ينتهي التدفق كله. جرّب الكود وستلاحظ أن الأرقام تظهر واحداً بعد الآخر بفاصل زمني.
الاستماع بـ listen
Section titled “الاستماع بـ listen”await for توقف الدالة حتى ينتهي التدفق. إذا أردت أن يكمل برنامجك عمله بينما تصل القيم، فاستخدم listen وأعطها دالة تُستدعى مع كل قيمة:
void main() { var ticks = Stream.periodic( Duration(milliseconds: 200), (count) => count + 1, ).take(3);
ticks.listen( (value) => print('وصل: $value'), onDone: () => print('انتهى التدفق'), );
print('بدأ الاستماع');}بدأ الاستماعوصل: 1وصل: 2وصل: 3انتهى التدفق«بدأ الاستماع» طُبعت أولاً، لأن listen لا تنتظر. و Stream.periodic تنتج قيمة كل فترة زمنية بلا نهاية، فاستخدمنا take(3) لنكتفي بأول ثلاث. و onDone تُستدعى عندما ينتهي التدفق.
التابع listen يعيد كائن اشتراك، وتستطيع إيقاف الاستماع في أي وقت باستدعاء cancel() عليه.
تحويل التدفق
Section titled “تحويل التدفق”التدفق له توابع تشبه توابع القوائم التي تعرفها: where و map و take. كل تابع يعيد تدفقاً جديداً:
Future<void> main() async { var numbers = Stream.fromIterable([1, 2, 3, 4, 5, 6]);
var result = numbers.where((n) => n.isEven).map((n) => n * 10);
await for (var value in result) { print(value); }
var all = await Stream.fromIterable([5, 10, 15]).toList(); print(all);}204060[5, 10, 15]تخيّل التدفق مثل أنبوب ماء: where فلتر يمنع بعض القطرات، و map يلوّن كل قطرة تمر. و toList تجمع كل القيم في قائمة بعد انتهاء التدفق، وتعيدها في Future.
StreamController: أنت تتحكم
Section titled “StreamController: أنت تتحكم”أحياناً لا تأتي القيم من حلقة، بل من أحداث في تطبيقك: المستخدم ضغط زراً، أو وصلت رسالة. الكلاس StreamController يعطيك تدفقاً ترسل فيه القيم بنفسك متى شئت بالتابع add:
import 'dart:async';
Future<void> main() async { var controller = StreamController<String>();
controller.stream.listen((message) => print('رسالة جديدة: $message'));
controller.add('أهلاً'); controller.add('كيف حالك؟');
await controller.close(); print('أُغلقت المحادثة');}رسالة جديدة: أهلاًرسالة جديدة: كيف حالك؟أُغلقت المحادثةالمتحكم له طرفان: add للإرسال من جهة، و stream للاستماع من الجهة الأخرى. وهذه طريقة شائعة ليتواصل جزءان من التطبيق دون أن يعرف أحدهما الآخر.
مستمع واحد أم عدة مستمعين؟
Section titled “مستمع واحد أم عدة مستمعين؟”التدفق العادي يقبل مستمعاً واحداً فقط. لو حاولت الاستماع مرتين فسيظهر خطأ:
import 'dart:async';
void main() { var controller = StreamController<int>(); controller.stream.listen(print);
try { controller.stream.listen(print); } on StateError catch (e) { print(e.message); }
controller.add(1); controller.close();}Stream has already been listened to.1المحاولة الثانية للاستماع رُفضت، والقيمة وصلت للمستمع الأول فقط.
إذا احتجت عدة مستمعين (مثل شاشتين تعرضان العدّاد نفسه)، فأنشئ تدفق بث بـ StreamController<int>.broadcast()، أو حوّل تدفقاً موجوداً بـ asBroadcastStream().
جرّب بنفسك
Section titled “جرّب بنفسك”اكتب دالة Stream<String> typeWriter(String text) بـ async* ترسل حروف النص واحداً واحداً بفاصل 100 مللي ثانية، كأنها آلة كاتبة. ثم استمع لها بـ await for واطبع كل حرف، وبعد النهاية اطبع عدد الحروف. جرّبها بالكلمة Dart:
Dartعدد الحروف: 4أخطاء شائعة
Section titled “أخطاء شائعة”- نسيان النجمة:
asyncبدون*لا تسمح بـyield. دوال التدفق تُعلَّم بـasync*. - استخدام
awaitالعادية مع Stream:await countdown(3)لا تعطيك القيم. استخدمawait forأوlisten. - الاستماع مرتين لتدفق عادي: يسبب الخطأ
Stream has already been listened to. استخدم تدفق بث. - عدم إلغاء الاشتراك: اشتراك منسي يظل يعمل بعد إغلاق الشاشة، ويستهلك الذاكرة والبطارية.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ما الفرق بين yield و return؟
yield تضيف قيمة إلى التدفق ثم تواصل الدالة تنفيذها، ولذلك تستطيع إرسال قيم كثيرة.
سؤال
أي ودجت في Flutter يعيد بناء الواجهة مع كل قيمة جديدة من تدفق؟
StreamBuilder يستمع للتدفق، ويعيد البناء مع كل قيمة. أما FutureBuilder فلقيمة واحدة.
الخلاصة
Section titled “الخلاصة”Streamسلسلة قيم تصل مع الوقت، وFutureقيمة واحدة.async*وyieldتنشئان تدفقاً، وawait forوlistenتستمعان له.whereوmapوtakeتحوّل التدفق كما تحوّل القوائم.StreamControllerيعطيك تدفقاً ترسل فيه القيم بنفسك، وأغلقه دائماً.- التدفق العادي لمستمع واحد، وتدفق البث لعدة مستمعين.
مبروك! أنهيت كورس Dart كاملاً. أنت الآن جاهز تماماً للانتقال إلى كورس Flutter وبناء تطبيقات حقيقية.