StreamBuilder: ما هو، مبدأ العمل والتطبيق في Flutter

المؤلف: IT Sectr نُشر: 2026-07-03 وقت القراءة: 8 دق

StreamBuilder هو عنصر واجهة في Flutter يعيد بناء الواجهة تلقائياً عند استلام بيانات جديدة من تدفق غير متزامن. على عكس FutureBuilder الذي يعمل بنتيجة واحدة، يدعم StreamBuilder التحديث المستمر لواجهة المستخدم طوال دورة حياة التدفق. وفقاً للتوثيق الرسمي لـ Flutter (2026)، يُستخدم StreamBuilder في التطبيقات الفورية: الدردشات، خلاصات الأخبار، مراقبة أجهزة الاستشعار، ومؤشرات الأسهم المالية. إنها أداة رئيسية في البرمجة التفاعلية، حيث تعكس واجهة المستخدم حالة البيانات دون استدعاءات يدوية لـ setState.

الملامح الرئيسية

  • StreamBuilder — عنصر واجهة يقبل تدفقاً ولقطة بيانات للتقديم التفاعلي لواجهة المستخدم
  • اللقطة تحتوي على connectionState و data و error، وتحدد الحالة الحالية للتدفق
  • ConnectionState يمر بأربع مراحل: none، waiting، active، done
  • AsyncSnapshot — كائن غير قابل للتغيير يضمن اتساق البيانات في كل إطار
  • StreamController يدير التدفق: يضيف البيانات، يعالج الأخطاء، ويغلق التدفق

ما هو StreamBuilder

StreamBuilder هو عنصر واجهة من حزمة Flutter SDK يشترك في تدفق ويعيد بناء العنصر التابع له عند كل حدث جديد في التدفق. يقبل StreamBuilder كائن تدفق ويعيد عنصر واجهة استناداً إلى أحدث لقطة مستلمة من التدفق.

في بنية Flutter، ينتمي StreamBuilder إلى مجموعة عناصر Builder التي تفصل بناء واجهة المستخدم عن حالة البيانات. على عكس StatefulWidget حيث يتطلب تغيير الحالة استدعاءً صريحاً لـ setState، يتفاعل StreamBuilder مع الأحداث غير المتزامنة تلقائياً، مما يبسط الكود ويقلل من خطر أخطاء المزامنة.

على عكس FutureBuilder الذي يعالج قيمة غير متزامنة واحدة، صُمم StreamBuilder للتدفقات المستمرة للبيانات. ينتهي FutureBuilder بعد استلام النتيجة الأولى، بينما يستمر StreamBuilder في الاستماع للتدفق وتحديث واجهة المستخدم عند كل حدث جديد.

يُستخدم StreamBuilder في جميع السيناريوهات التي تصل فيها البيانات باستمرار: اتصالات WebSocket، استدعاءات أجهزة الاستشعار، إشعارات Firebase، قوائم انتظار أحداث Bluetooth، ونشر حالة التطبيق عبر BLoC. وفقاً لتحليل مشاريع Flutter على GitHub (2025)، يُعد StreamBuilder من بين أكثر ثلاثة عناصر Builder استخداماً إلى جانب FutureBuilder و LayoutBuilder.

الخلاصة: استخدم StreamBuilder كلما كان على واجهة المستخدم أن تعكس بيانات متغيرة باستمرار، متجنباً إدارة الحالة اليدوية عبر StatefulWidget.

كيف يعمل StreamBuilder

StreamBuilder يشترك في تدفق عند وقت البناء ويلغي الاشتراك عند تدمير العنصر. في كل مرة يصدر فيها التدفق حدثاً، يستلم StreamBuilder لقطة AsyncSnapshot جديدة ويستدعي دالة builder لإعادة بناء واجهة المستخدم.

تتكون العملية من ثلاث مراحل. الأولى: ينشئ StreamBuilder اشتراكاً في التدفق المُمرر عبر طريقة stream.listen. الثانية: عند كل حدث، يحدّث StreamBuilder AsyncSnapshot الداخلي ويُعلّم العنصر على أنه متسخ لإعادة البناء. الثالثة: يستدعي الإطار دالة builder مع اللقطة الجديدة، وتعرض واجهة المستخدم البيانات الحالية.

هام: يستخدم StreamBuilder StreamSubscription داخلياً. إذا تم تمرير التدفق مباشرة، يشترك StreamBuilder مرة واحدة أثناء التهيئة. إذا تغير التدفق (مثلاً أثناء إعادة بناء العنصر الأب)، يلغي StreamBuilder اشتراكه من التدفق القديم ويشترك في الجديد. يتم التحكم في هذا السلوك عبر المعاملين initialData و buildWhen، اللذين يسمحان بتحسين عدد مرات إعادة البناء.

الخلاصة: فهم دورة حياة الاشتراك هو أساس الاستخدام الصحيح لـ StreamBuilder. الإدارة غير الصحيحة للتدفقات تؤدي إلى تسرب الذاكرة أو بيانات قديمة في واجهة المستخدم.

ConnectionState: أربع حالات للتدفق

تحدد الخاصية connectionState لكائن AsyncSnapshot في أي مرحلة من معالجة التدفق يوجد StreamBuilder. هناك أربع حالات: none، waiting، active، done.

ConnectionState.none

None هي الحالة الابتدائية عندما لم يبدأ التدفق بعد في إرسال البيانات. في هذه الحالة، snapshot.connectionState يساوي ConnectionState.none و snapshot.data يساوي null. عادةً ما يتم عرض عنصر نائب أو مؤشر انتظار في هذه الحالة. إذا لم يوفر التدفق بيانات أولية، يبدأ StreamBuilder في هذه الحالة.

ConnectionState.waiting

Waiting هي حالة انتظار البيانات من تدفق غير متزامن. التدفق نشط لكن البيانات لم تصل بعد. تحدث هذه الحالة، على سبيل المثال، عند تحميل البيانات من الشبكة أو فتح اتصال طويل الأمد. في هذه الحالة، من المعتاد عرض CircularProgressIndicator أو هيكل عظمي للتحميل.

ConnectionState.active

Active — يصدر التدفق بيانات وتعرض واجهة المستخدم المعلومات الحالية. في هذه الحالة، snapshot.hasData يساوي true و snapshot.data يحتوي على أحدث قيمة من التدفق. إذا كان التدفق من نوع Broadcast Stream، يمكن أن تتعايش الحالة النشطة مع انتظار بيانات جديدة.

ConnectionState.done

Done — اكتمل التدفق، ولن تصل بيانات جديدة. يحتوي Snapshot.data على آخر قيمة تم إرسالها قبل إغلاق التدفق. إذا اكتمل التدفق بنجاح، snapshot.hasError يساوي false. تُستخدم هذه الحالة لعرض النتيجة النهائية: رسالة مثل «اكتمل التحميل» أو الانتقال إلى الشاشة التالية.

الخلاصة: عند بناء واجهة المستخدم عبر StreamBuilder، يجب معالجة جميع الحالات الأربع لكي تعرض الواجهة التحميل والبيانات والأخطاء والاكتمال بشكل صحيح.

استخدام StreamController لإدارة التدفق

StreamController هو فئة من حزمة dart:async تنشئ وتدير تدفقاً. يسمح StreamController بإضافة البيانات ومعالجة الأخطاء وإغلاق التدفق، والتحكم في دورة حياته.

يوجد نوعان من StreamController: single-subscription (مشترك واحد) و broadcast (مشتركون متعددون). يقبل جهاز التحكم single-subscription مستمعاً واحداً فقط في كل مرة — الاشتراك الثاني سيثير استثناءً. يسمح جهاز التحكم broadcast لعدة عناصر StreamBuilder بالاستماع إلى نفس التدفق في وقت واحد، وهو مفيد لـ BLoC وحالة التطبيق المشتركة.

عند إنشاء StreamController عبر StreamController<T>.broadcast()، لا يتم إعادة إنتاج البيانات المضافة قبل أول اشتراك للمشتركين الجدد. للحصول على آخر قيمة عند الاتصال، يُستخدم BehaviourSubject من حزمة rxdart الذي يخزن مؤقتاً آخر حدث.

بعد الانتهاء من العمل مع جهاز التحكم، يجب استدعاء controller.close(). عدم استدعاء close يؤدي إلى تسرب الموارد: يبقى التدفق مفتوحاً، ويبقى المشتركون في الذاكرة، ولا يحرر جامع القمامة الكائنات المرتبطة.

الخلاصة: استخدم StreamController مع إدارة صريحة لدورة الحياة. للتدفقات single-subscription، استخدم جهاز التحكم القياسي؛ للحالة المشتركة، استخدم جهاز تحكم broadcast أو BehaviourSubject.

أمثلة برمجية مع StreamBuilder

مثال 1 يوضح عداداً تنازلياً باستخدام StreamController و StreamBuilder.

dart
import 'dart:async';

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

  final StreamController<int> controller = StreamController<int>();

  void startTimer() {
    int count = 0;
    Timer.periodic(Duration(seconds: 1), (timer) {
      controller.sink.add(count++);
      if (count > 10) {
        controller.close();
        timer.cancel();
      }
    });
  }
}

في المثال، يتم إنشاء جهاز تحكم لتوليد أرقام من 0 إلى 10 بفاصل زمني قدره ثانية واحدة. بعد الوصول إلى 10، يتم استدعاء close وينتهي التدفق. StreamBuilder المشترك في تدفق هذا المتحكم سيعرض كل قيمة جديدة.

مثال 2 — استخدام StreamBuilder مع Broadcast Stream لعرض البيانات من مصادر متعددة.

dart
final StreamController<String> broadcastController =
    StreamController<String>.broadcast();

StreamBuilder<String>(
  stream: broadcastController.stream,
  initialData: 'Waiting for data...',
  builder: (context, AsyncSnapshot<String> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }
    if (snapshot.hasError) {
      return Text('خطأ: ${snapshot.error}');
    }
    return Text('البيانات: ${snapshot.data}');
  },
)

يوضح المثال الثاني معالجة جميع الحالات: initialData للعرض الأولي، waiting لمؤشر التحميل، hasError للأخطاء، و data للنتيجة الناجحة. هذا النمط هو المعيار لرمز الإنتاج مع StreamBuilder.

الخلاصة: استخدم initialData لتجنب شاشة فارغة في البداية وتعامل دائماً مع hasError لعرض الأخطاء بشكل صحيح للمستخدم.

الأخطاء الشائعة عند العمل مع StreamBuilder

الخطأ 1: إنشاء تدفق جديد عند كل إعادة بناء للعنصر الأب. إذا تم تمرير التدفق عبر تعبير ينشئ كائناً جديداً عند كل بناء، يلغي StreamBuilder اشتراكه من القديم ويشترك في الجديد، مسبباً حلقة لا نهائية من إعادة البناء. الحل: استخدام متغير remembered أو StatefulWidget بتدفق ثابت.

الخطأ 2: عدم معالجة الأخطاء. يمكن للتدفق إصدار أخطاء عبر controller.sink.addError، وإذا لم يتحقق builder من snapshot.hasError، يرى المستخدم شاشة فارغة أو تحميلاً لا نهائياً. الحل: تحقق دائماً من hasError واعرض رسالة واضحة.

الخطأ 3: تسرب الذاكرة بسبب StreamController غير مغلق. إذا لم يُغلق جهاز التحكم في dispose، يستمر التدفق في الوجود ولا يحرر جامع القمامة الذاكرة. الحل: استدع controller.close() في dispose واستمع لحدث done للإجراءات النهائية.

الخطأ 4: استخدام StreamBuilder مع دالة builder بطيئة. نظراً لأن builder يُستدعى عند كل حدث في التدفق، تؤدي الحسابات الثقيلة داخله إلى فقدان الإطارات. الحل: انقل الحسابات إلى isolate منفصل أو استخدم Stream.map لتحويل البيانات.

الخلاصة: StreamBuilder أداة قوية لكنها تتطلب عناية. راقب دورة حياة التدفق، تعامل مع الأخطاء، وتجنب العمليات الثقيلة في builder.

الأسئلة الشائعة

ما الفرق بين StreamBuilder و FutureBuilder؟

FutureBuilder مصمم لنتيجة غير متزامنة واحدة: يشترك في Future، يستلم قيمة واحدة، وينتهي. StreamBuilder يشترك في تدفق يمكن أن يصدر قيماً متعددة عبر الزمن، ويعيد بناء واجهة المستخدم عند كل حدث جديد.

ما هو AsyncSnapshot في StreamBuilder؟

AsyncSnapshot هو كائن غير قابل للتغيير يحتوي على حالة الاشتراك الحالية (connectionState)، آخر قيمة مستلمة (data)، وكائن الخطأ (error) إذا أصدر التدفق استثناءً.

كيف نتعامل مع الأخطاء في StreamBuilder؟

الأخطاء تُعالج عبر خاصيتي snapshot.hasError و snapshot.error في دالة builder. إذا أصدر التدفق خطأً عبر sink.addError، يستقبل AsyncSnapshot الخطأ، ويجب على builder عرض رسالة مناسبة أو واجهة احتياطية.

هل يمكن استخدام تدفق واحد في عدة StreamBuilder؟

نعم، إذا كان التدفق من نوع broadcast (منشأ عبر StreamController.broadcast). التدفق single-subscription يسمح بمشترك واحد فقط. لمشاركة تدفق واحد بين عدة عناصر واجهة، استخدم جهاز تحكم broadcast أو حزمة rxdart مع BehaviourSubject.

كيف نتجنب إعادة بناء StreamBuilder عند كل حدث؟

استخدم المعامل buildWhen لتصفية الأحداث التي يجب أن تؤدي إلى إعادة بناء واجهة المستخدم. يمكنك أيضاً تطبيق Stream.transformer أو Stream.where لتصفية البيانات قبل تمريرها إلى StreamBuilder.

الخلاصة

  • StreamBuilder — عنصر واجهة للبناء التفاعلي لواجهة المستخدم من تدفق بيانات غير متزامن، يدعم التحديث المستمر للواجهة
  • AsyncSnapshot يحتوي على connectionState (none، waiting، active، done) و data و error — جميع حالات التدفق
  • StreamController يدير دورة حياة التدفق: إضافة البيانات، معالجة الأخطاء، وإغلاق التدفق
  • Broadcast Stream يسمح لعدة عناصر StreamBuilder بالاشتراك في تدفق واحد، single-subscription يسمح لواحد فقط
  • معالجة الأخطاء إلزامية: بدون التحقق من hasError، قد يعلق التطبيق في حالة التحميل
  • تسرب الذاكرة هو المشكلة الأكثر شيوعاً: أغلق StreamController دائماً في dispose
  • توصية: حدد دائماً initialData وتعامل مع جميع قيم connectionState الأربع لتجربة مستخدم سلسة

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا