تخطَّ إلى المحتوى

الـ Streams في Dart: سلسلة قيم تصل مع الوقت

الدرس 14 من 14 · Dart

في درس async و await تعرفت على Future: وعد بقيمة واحدة تصل لاحقاً. لكن ماذا عن رسائل المحادثة التي تصل واحدة بعد الأخرى؟ أو موقع السائق الذي يتحدث كل ثانية؟ هذه سلسلة قيم تصل مع الوقت، واسمها في Dart هو Stream (التدفق). في هذا الدرس، وهو الأخير في الكورس، ستتعلم كيف تنشئ تدفقاً وتستمع له وتتحكم فيه.

تخيّل الفرق بين طرد بريدي واشتراك في مجلة. الطرد يصل مرة واحدة وينتهي الأمر، وهذا Future. أما المجلة فيصلك عدد جديد منها كل شهر حتى ينتهي الاشتراك، وهذا Stream.

Future Stream
عدد القيم واحدة صفر أو أكثر، مع الوقت
طريقة الانتظار await await for أو listen
مثال جلب ملف المستخدم رسائل المحادثة

أسهل طريقة لصنع تدفق هي دالة معلَّمة بـ 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('انطلاق!');
}
الناتج
3
2
1
انطلاق!

الحلقة await for تشبه for-in العادية، لكنها تنتظر وصول كل قيمة. ولا يُطبع «انطلاق!» إلا بعد أن ينتهي التدفق كله. جرّب الكود وستلاحظ أن الأرقام تظهر واحداً بعد الآخر بفاصل زمني.

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() عليه.

التدفق له توابع تشبه توابع القوائم التي تعرفها: 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);
}
الناتج
20
40
60
[5, 10, 15]

تخيّل التدفق مثل أنبوب ماء: where فلتر يمنع بعض القطرات، و map يلوّن كل قطرة تمر. و toList تجمع كل القيم في قائمة بعد انتهاء التدفق، وتعيدها في Future.

أحياناً لا تأتي القيم من حلقة، بل من أحداث في تطبيقك: المستخدم ضغط زراً، أو وصلت رسالة. الكلاس 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().

اكتب دالة Stream<String> typeWriter(String text) بـ async* ترسل حروف النص واحداً واحداً بفاصل 100 مللي ثانية، كأنها آلة كاتبة. ثم استمع لها بـ await for واطبع كل حرف، وبعد النهاية اطبع عدد الحروف. جرّبها بالكلمة Dart:

الناتج
D
a
r
t
عدد الحروف: 4
  • نسيان النجمة: async بدون * لا تسمح بـ yield. دوال التدفق تُعلَّم بـ async*.
  • استخدام await العادية مع Stream: await countdown(3) لا تعطيك القيم. استخدم await for أو listen.
  • الاستماع مرتين لتدفق عادي: يسبب الخطأ Stream has already been listened to. استخدم تدفق بث.
  • عدم إلغاء الاشتراك: اشتراك منسي يظل يعمل بعد إغلاق الشاشة، ويستهلك الذاكرة والبطارية.

سؤال

ما الفرق بين yield و return؟

سؤال

أي ودجت في Flutter يعيد بناء الواجهة مع كل قيمة جديدة من تدفق؟

  • Stream سلسلة قيم تصل مع الوقت، و Future قيمة واحدة.
  • async* و yield تنشئان تدفقاً، و await for و listen تستمعان له.
  • where و map و take تحوّل التدفق كما تحوّل القوائم.
  • StreamController يعطيك تدفقاً ترسل فيه القيم بنفسك، وأغلقه دائماً.
  • التدفق العادي لمستمع واحد، وتدفق البث لعدة مستمعين.

مبروك! أنهيت كورس Dart كاملاً. أنت الآن جاهز تماماً للانتقال إلى كورس Flutter وبناء تطبيقات حقيقية.