ایڈوانسڈ ڈارٹ: اسٹریمز، آئسولیشن اور ایونٹ لوپس کے ساتھ غیر مطابقت پذیر پروگرامنگ سیکھیں

میں ایک سال سے زیادہ عرصے سے فلٹر ایپس لکھ رہا ہوں اس سے پہلے کہ میں واقعی یہ سمجھتا ہوں کہ ڈارٹ ہم آہنگی کو کیسے ہینڈل کرتا ہے۔

میں اسے استعمال کرنے کا طریقہ جانتا تھا۔ await. میں جانتا تھا FutureBuilder اور StreamBuilder چیزوں کو شروع کرنے کے لیے بس کافی ہے۔ لیکن مجھے واقعی سمجھ نہیں آرہی تھی کہ نیچے کیا ہو رہا ہے۔ مجھے سمجھ نہیں آیا کہ کچھ کوڈ ایک خاص ترتیب میں کیوں چلتے ہیں، کیوں کچھ آپریشنز UI کو منجمد کرنے کا سبب بنتے ہیں، یا کیوں اسٹریم سبسکرپشنز ناقابل شناخت میموری لیک کا باعث بنتے رہتے ہیں۔

جس لمحے میں واقعتاً بیٹھ گیا اور ایونٹ لوپ سیکھا، باقی سب کچھ کلک کر گیا۔ کیوں mounted اپنے کام کی تصدیق کریں۔ کیوں compute() یہ موجود ہے۔ منسلک سامعین کی تعداد کے لحاظ سے اسٹریمز مختلف طریقے سے برتاؤ کرنے کی وجوہات وہ چیزیں نہیں ہیں جنہیں الگ سے حفظ کرنے کی ضرورت ہے۔ وہ سب ایک ہی بنیادی ماڈل کا نتیجہ تھے۔

یہ مضمون ایک ایسی چیز کی وضاحت کرتا ہے جو کاش مجھے جلد معلوم ہوتا۔ آئیے اس پر گہری نظر ڈالتے ہیں کہ ڈارٹ کا ایونٹ لوپ اصل میں کیسے کام کرتا ہے، اسٹریمز کے ذریعے وقت کے ساتھ آنے والے ڈیٹا کو کیسے کنٹرول کیا جاتا ہے، اور ایک عملی فلٹر مثال کے ذریعے جب حقیقی ہم آہنگی کی ضرورت ہو تو تنہائی کے ذریعے ایک ہی دھاگے سے کیسے بچنا ہے۔

انڈیکس

ڈارٹ کا سنگل تھریڈڈ ماڈل کیسے کام کرتا ہے۔

زیادہ تر زبانیں کوڈ کو ایک سے زیادہ تھریڈز پر بیک وقت چلنے کی اجازت دیتی ہیں۔ ایک تھریڈ نیٹ ورک کالز کو ہینڈل کرتا ہے، دوسرا تھریڈ صارف کے ان پٹ پر کارروائی کرتا ہے، اور دوسرا تھریڈ UI کو پیش کرتا ہے، یہ سب بیک وقت اور متوازی طور پر چلتے ہیں۔

ڈارٹ اس طرح کام نہیں کرتا ہے۔ ڈارٹ ہر چیز کو ایک ہی دھاگے میں چلاتا ہے۔ ایک وقت میں ایک۔ ہمیشہ

جب میں نے پہلی بار یہ سیکھا تو ایسا لگتا تھا کہ کوئی حد ہے۔ ایک ہی تھریڈ بیک وقت نیٹ ورک کالز، صارف کے بٹن ٹیپ، اور 60 فریم فی سیکنڈ پر رینڈرنگ کو کیسے ہینڈل کر سکتا ہے؟ جواب یہ ہے کہ وہ بیک وقت ان پر کارروائی نہیں کرتے۔ یعنی، ان پر ترتیب وار کارروائی کی جاتی ہے، جس کا انتظام ایونٹ لوپ کے ذریعے کیا جاتا ہے۔

باورچی خانے میں اکیلے کام کرنے والے شیف کی طرح اس کے بارے میں سوچیں۔ ایک شیف، ہاتھوں کا ایک جوڑا۔ ایک ہی وقت میں کاٹنا اور ہلانا ناممکن ہے۔ لیکن ایک اچھا باورچی صرف پانی کے ابلنے کے انتظار میں نہیں بیٹھتا۔ وہ سبزیاں تیار کرتے ہیں اور جب پانی تیار ہو جاتا ہے تو وہ واپس آتے ہیں اور اگلے کام پر چلے جاتے ہیں۔ ہر ایک کے دستیاب ہوتے ہی کاموں کے درمیان سوئچ کرکے نتیجہ خیز رہیں۔

ڈارٹ وہ شیف ہے۔ ایونٹ لوپ ایک ایسا نظام ہے جو فیصلہ کرتا ہے کہ اگلا کون سا عمل منتخب کرنا ہے۔

ایونٹ لوپ اور دو قطاریں۔

ایونٹ لوپ آپ کی ڈارٹ ایپ کی پوری زندگی کے لیے چلتا ہے۔ کام آسان ہے۔ یہ دیکھنے کے لیے چیک کریں کہ آیا کچھ کرنا ہے، اسے کریں، اور پھر دوبارہ چیک کریں۔ یہ بار بار ایسا کرتا رہتا ہے جب تک کہ ایپ ختم نہ ہو جائے۔

ڈارٹ میں، چیزیں فوری طور پر نہیں ہوتی ہیں۔ چلنے کے لیے تیار ہونے پر، نیٹ ورک کا جواب آتا ہے، ٹائمر چلتا ہے، اور .then() کال بیک مکمل ہو گیا – قطار میں شامل کر دیا گیا۔ ایونٹ لوپ اس قطار میں ایک ایک وقت میں آئٹمز پر کارروائی کرتا ہے۔

Dart میں بالکل دو قطاریں ہیں، اور دونوں کو سمجھنا وہی ہے جو ڈویلپرز کو async استعمال کرنے والوں سے الگ کرتا ہے جو واقعی async کو سمجھتے ہیں۔

مائکروٹاسک قطار

یہ ایک اعلی ترجیحی قطار ہے۔ ایونٹ لوپ ہمیشہ کسی اور چیز کو دیکھنے سے پہلے اس قطار کو مکمل طور پر صاف کرتا ہے۔ .then() کال بیک اور Future.microtask() یہاں زمین۔

ان کے بارے میں تیز چیک آؤٹ لین کے طور پر سوچیں: مختصر، فوری کام جو موجودہ ہم آہنگی کوڈ کے مکمل ہونے کے بعد جلد از جلد مکمل کیے جائیں۔

تقریب کی قطار

بیرونی ہر چیز یہاں سے گزر جاتی ہے: ٹائمر کال بیکس، نیٹ ورک کے جوابات، یوزر ان پٹ ایونٹس، اسٹریم ڈیٹا وغیرہ۔ Future.delayed() مکمل ایونٹ لوپ اس قطار سے ایک آئٹم پر کارروائی کرتا ہے اور پھر واپس چلا جاتا ہے اور اگلے ایونٹ پر کارروائی کرنے سے پہلے مائیکرو ٹاسک قطار کو چیک کرتا ہے۔

اصل ترتیب حسب ذیل ہے:

void main() {
  print('1 — synchronous, runs immediately');

  // Goes into the EVENT queue — regular lane
  Future.delayed(Duration.zero, () {
    print('4 — event queue');
  });

  // Goes into the MICROTASK queue — high priority lane
  Future.microtask(() {
    print('3 — microtask queue');
  });

  print('2 — synchronous, runs immediately');
}

// Output:
// 1 — synchronous, runs immediately
// 2 — synchronous, runs immediately
// 3 — microtask queue
// 4 — event queue

آئٹم 1 اور 2 چونکہ یہ مطابقت پذیر ہے، یہ سب سے پہلے عملدرآمد کرتا ہے. یہ فوری طور پر چلتا ہے، بغیر کسی قطار کے۔ پھر 3 پہلے چلائیں 4 اس کی وجہ یہ ہے کہ مائیکرو ٹاسک ہمیشہ ایونٹ سے پہلے چلتا ہے، حالانکہ دونوں بغیر کسی تاخیر کے طے شدہ ہیں۔

یہ حکم آپ کے خیال سے کہیں زیادہ اہم ہے۔ اگر آپ متعدد کو جوڑتے ہیں۔ .then() جب بھی آپ اسے کال کرتے ہیں، ہر کال بیک ایک مائیکرو ٹاسک قطار میں چلا جاتا ہے۔ یہی وجہ ہے کہ یہ فوری طور پر محسوس ہوتا ہے اور ہمیشہ ٹائمر یا I/O کال بیکس سے پہلے چلتا ہے (حتی کہ بلا تاخیر کال بیکس بھی)۔

void main() {
  Future(() => print('event 1'));
  Future(() => print('event 2'));
  Future.microtask(() => print('microtask 1'));
  Future.microtask(() => print('microtask 2'));
  print('synchronous');
}

// Output:
// synchronous
// microtask 1
// microtask 2
// event 1
// event 2

دونوں مائیکرو ٹاسک ایونٹ سے پہلے چلتے ہیں قطع نظر اس کے کہ وہ کس ترتیب میں مقرر ہیں۔

async/await اس میں کیسے فٹ بیٹھتا ہے۔

async/await یہ کوئی نیا تھریڈ نہیں بناتا۔ وہ متوازی نہیں چلتے۔ یہ سنٹیکٹک شوگر ہے جو ایونٹ لوپ کے اوپر بنایا گیا ہے، کوڈ لکھنے کا ایک صاف طریقہ ہے جو ڈارٹ کے سنگل تھریڈڈ کنکرنسی ماڈل کے ساتھ کام کرتا ہے۔

اس کے بارے میں سوچنے کا بہترین طریقہ یہ ہے۔ تصور کریں کہ آپ ایک ریستوراں میں ویٹر ہیں اور آپ اپنی شفٹ میں واحد ویٹر ہیں۔ آپ ایک وقت میں صرف ایک کام کر سکتے ہیں، لیکن آپ کو اپنے کھانے کے انتظار میں باورچی خانے کے دروازے پر کھڑے ہونے کی ضرورت نہیں ہے۔ آپ کچن میں آرڈر دیں اور چلے جائیں۔ جاؤ اپنا پانی بھرو، دوسرا آرڈر لو، اور میز صاف کرو۔ جب کچن کی گھنٹی بجتی ہے تو کھانا اٹھا کر پہنچا دیا جاتا ہے۔

await یہ وہ لمحہ ہے جب آپ اپنا آرڈر کچن میں پہنچاتے ہیں اور چلے جاتے ہیں۔ آپ بلاک نہیں کر رہے ہیں، آپ صرف اس خاص کام کو روک رہے ہیں اور ایونٹ لوپ کو کہہ رہے ہیں کہ "جب آپ تیار ہوں تو واپس آجائیں۔” اب ایونٹ لوپ نیٹ ورک کالز، فائل ریڈنگ، یا ٹائمر کے چلنے کے دوران دیگر کاموں کو سنبھال سکتا ہے۔

جب وہ جس کام کا انتظار کر رہا ہے وہ مکمل ہو جاتا ہے، بقیہ فنکشن قطار میں شامل کر دیا جاتا ہے اور جب ایونٹ لوپ قطار میں واپس آجاتا ہے تو اسے انجام دیا جاتا ہے۔

Future loadUser() async {
  print('A — before await');

  // Dart pauses here and hands control back to the event loop.
  // The event loop is now free to handle other work —
  // rendering frames, processing other futures, handling taps —
  // while the network call is in progress.
  final user = await dio.get('/user');

  // This only runs when the network response arrives
  // and the event loop gets back to this function.
  print('B — after await, got user');
}

void main() {
  loadUser();

  // This runs before B because loadUser() paused at the await
  // and returned control here before the network call completed.
  print('C — main continues');
}

// Output:
// A — before await
// C — main continues
// B — after await, got user

ایونٹ لوپ کو بلاک کرنے سے فلٹر میں ہنگامہ کیوں ہوتا ہے۔

فلٹر کا UI رینڈرنگ اسی مقامی تنہائی میں چلتا ہے جیسا کہ ڈارٹ کوڈ۔ انجن کو 60fps پر فریم رینڈر کرنے کے لیے تقریباً ہر 16 ملی سیکنڈ بعد ایونٹ لوپ جاری کرنا چاہیے۔ کوئی بھی ہم وقت ساز آپریشن جس میں اس سے زیادہ وقت لگتا ہے ایونٹ لوپ کو مکمل طور پر روک دے گا۔ اس کا مطلب ہے کہ فریم پیش نہیں کیے جاتے، ٹیبز پر کارروائی نہیں ہوتی، اور UI منجمد ہو جاتا ہے۔

// This is dangerous in Flutter.
// Parsing a large JSON response synchronously
// can take 100-300ms on slower devices.
// The event loop is completely blocked the entire time.
// Flutter drops every frame during that window.
// The user sees a frozen screen.
final users = (response.data as List)
    .map((json) => User.fromJson(json))
    .toList();

await یہ یہاں مدد نہیں کرے گا کیونکہ کام CPU پابند ہے۔ چونکہ CPU ہمیشہ مصروف رہتا ہے، ایونٹ لوپ کے سانس لینے کے لیے کوئی قدرتی وقفہ نہیں ہے۔ یہ تنہائی کا مسئلہ ہے جس پر توجہ دینے کی ضرورت ہے، اور ہم اسے جلد ہی دیکھیں گے۔

اسٹریمز: وقت کے ساتھ آنے والے ڈیٹا کو کنٹرول کریں۔

کوئی راستہ نہیں Future ایک قدر پاس کریں اور آپ کا کام ہو گیا۔ کوئی راستہ نہیں Stream یہ وقت کے ساتھ متعدد اقدار پیش کرتا ہے اور منسوخ یا ختم ہونے تک کھلا رہتا ہے۔

اگر Future یہ کسی ریستوراں میں کھانے کا آرڈر دینے جیسا ہے۔ ایک بار جب آپ انتظار کرتے ہیں، ایک کھانا باہر آتا ہے اور آپ کا کام ہو جاتا ہے۔ Stream یہ سبسکرپشن نیوز لیٹر ہے۔ نئے ورژن وقت کے ساتھ ساتھ آتے رہیں گے، اور آپ ان کو اس وقت تک وصول کرتے رہیں گے جب تک آپ اپنی رکنیت منسوخ نہیں کر دیتے۔

// A stream that counts from 1 to 5, one number per second.
// async* marks this as a stream generator function.
// yield pushes a value into the stream and pauses
// until the listener is ready for the next value.
Stream countStream() async* {
  for (int i = 1; i <= 5; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield i;
  }
  // When the loop ends the stream closes automatically.
}

آپ اسٹریمز کو استعمال کر سکتے ہیں: await for یا .listen():

// Method 1 — await for: clean, readable for simple cases
await for (final number in countStream()) {
  print(number); // prints 1, 2, 3, 4, 5, one per second
}

// Method 2 — listen(): more control, can cancel midway
final subscription = countStream().listen(
  (number) => print(number),
  onError: (error) => print('Error: $error'),
  onDone: () => print('Stream closed'),
);

// Cancel after 3 seconds — stops receiving values
await Future.delayed(const Duration(seconds: 3));
subscription.cancel();

سنگل سبسکرپشن اور براڈکاسٹ سٹریم

یہ فرق بہت سے Flutter ڈویلپرز کو الجھا دیتا ہے، اور اسے سمجھنا مبہم غلطیوں کے پورے زمرے کو روک سکتا ہے۔

واحد رکنیت کا سلسلہ آپ کے پاس ایک وقت میں صرف ایک سننے والا ہو سکتا ہے۔ یہ پہلے سے طے شدہ ہے۔ زیادہ تر اسٹریمز، جیسے فائل ریڈز، HTTP رسپانس باڈیز، وغیرہ، واحد سبسکرپشنز ہیں۔ اگر آپ دو بار سنیں اور کوشش کریں۔ StateError.

final stream = countStream();

stream.listen(print); // fine
stream.listen(print); // throws: Stream has already been listened to

نشریاتی سلسلہ ایک ہی وقت میں، آپ جتنے چاہیں سامعین رکھ سکتے ہیں۔ وہ سب ایک ہی قیمت وصول کرتے ہیں۔ یہ وہی ہے جو آپ ایپ کے وسیع واقعات، صارف کے تعاملات، یا کسی بھی ایسی چیز کے لیے چاہتے ہیں جس کے لیے آپ کی ایپ کے متعدد حصوں کو رد عمل کا اظہار کرنے کی ضرورت ہو۔

// StreamController.broadcast() creates a stream
// that any number of listeners can subscribe to.
final controller = StreamController.broadcast();

controller.stream.listen((v) => print('Listener 1: $v'));
controller.stream.listen((v) => print('Listener 2: $v'));

// Both listeners receive this value
controller.sink.add('Hello');
// Listener 1: Hello
// Listener 2: Hello

// Always close the controller when you're done with it.
// An unclosed controller keeps resources alive indefinitely.
controller.close();

StreamController کا استعمال کرتے ہوئے دستی طور پر ایک سٹریم بنانا

StreamController مکمل دستی کنٹرول فراہم کرتا ہے۔ بالکل طے کریں کہ کب اقدار کو آگے بڑھانا ہے، کب غلطیوں کو آگے بڑھانا ہے، اور کب اسٹریمز کو بند کرنا ہے۔ اس طرح آپ شروع سے ایک ذمہ دار ڈیٹا سورس بناتے ہیں۔

class LocationService {
  // Broadcast so multiple widgets can listen to
  // location updates simultaneously.
  final _controller = StreamController.broadcast();

  // Expose only the stream publicly.
  // The controller stays private so only this class
  // can push new values into it.
  Stream get locationStream => _controller.stream;

  void startTracking() {
    Timer.periodic(const Duration(seconds: 2), (_) {
      final position = Position(lat: 0.3476, lng: 32.5825);
      // sink.add() pushes a value into the stream.
      // All active listeners receive it immediately.
      _controller.sink.add(position);
    });
  }

  void dispose() {
    // Always close the controller when you're done.
    // An unclosed controller is a memory leak.
    _controller.close();
  }
}

StreamBuilder کے ساتھ فلٹر میں اسٹریمز کا استعمال

StreamBuilder UI سے براہ راست اسٹریمز استعمال کرنے کے لیے ایک فلٹر ویجیٹ۔ ہر بار جب کوئی نئی قدر آتی ہے تو اسے دوبارہ بنایا جاتا ہے۔

StreamBuilder>(
  stream: firestore
      .collection('messages')
      .snapshots()
      .map((snapshot) => snapshot.docs
          .map((doc) => Message.fromJson(doc.data()))
          .toList()),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }

    if (!snapshot.hasData || snapshot.data!.isEmpty) {
      return const Text('No messages yet');
    }

    return ListView.builder(
      itemCount: snapshot.data!.length,
      itemBuilder: (context, index) {
        return MessageBubble(message: snapshot.data![index]);
      },
    );
  },
)

اسٹریمز سے ہمیشہ ان سبسکرائب کریں۔ dispose

یہ فلٹر ایپس میں میموری کے سب سے عام لیکس میں سے ایک ہے اور براہ راست اسٹریمز کو نہ سمجھنے سے آتا ہے۔

ایک فعال رکنیت سٹریم کے کال بیکس کو فعال رکھتی ہے۔ اگر اس سے تعلق رکھنے والا ویجیٹ غائب ہو گیا ہے لیکن سبسکرپشن ابھی بھی چل رہی ہے تو حذف شدہ ویجیٹ پر کال بیک کی جائے گی۔ setState کے بعد بلایا جائے گا disposeجن اشیاء کو آزاد کرنے کی ضرورت ہوتی ہے وہ میموری میں رہتی ہیں۔

class _ChatScreenState extends State {
  StreamSubscription? _subscription;

  @override
  void initState() {
    super.initState();
    _subscription = messageStream.listen((message) {
      if (mounted) setState(() => messages.add(message));
    });
  }

  @override
  void dispose() {
    // cancel() unsubscribes from the stream.
    // Without this, the callback keeps firing
    // even after this screen is removed from the tree.
    _subscription?.cancel();
    super.dispose();
  }
}

StreamTransformers اور جدید اسٹریم کنٹرول

ایک بار جب آپ اسٹریمز کو سمجھ لیتے ہیں، تو آپ کو فوری طور پر احساس ہوتا ہے کہ خام دھارے آپ کو وہی کچھ دیتے ہیں جو آپ چاہتے ہیں۔ آپ کو اقدار کو فلٹر کرنے، انہیں تبدیل کرنے، تیز اخراج کو کم کرنے، یا متعدد سلسلے کو یکجا کرنے کی ضرورت پڑ سکتی ہے۔ یہ اسٹریم آپریٹر ہے اور StreamTransformer اندر آجاؤ۔

ڈارٹ Stream کلاس میں بلٹ ان تبادلوں کے طریقوں کا ایک بھرپور سیٹ ہے۔

final stream = countStream();

// map — transform each value before it reaches listeners
stream
    .map((number) => number * 2)
    .listen(print); // 2, 4, 6, 8, 10

// where — filter out values that don't match a condition
stream
    .where((number) => number.isEven)
    .listen(print); // 2, 4

// take — only emit the first N values, then close
stream
    .take(3)
    .listen(print); // 1, 2, 3

// skip — ignore the first N values
stream
    .skip(2)
    .listen(print); // 3, 4, 5

// distinct — only emit when the value changes from the last one
Stream.fromIterable([1, 1, 2, 2, 3])
    .distinct()
    .listen(print); // 1, 2, 3

مزید پیچیدہ تبدیلیوں کے لیے، آپ اپنی مرضی کے مطابق تبدیلیاں بنا سکتے ہیں۔ StreamTransformer. یہ اس وقت تک پہنچنے کا نمونہ ہے جب بلٹ ان آپریٹرز آپ کے استعمال کے معاملے کا احاطہ نہیں کرتے ہیں (مثال کے طور پر، جب آپ کو کسی قدر کو اس طریقے سے تبدیل کرنے کی ضرورت ہوتی ہے جس میں اخراج کے درمیان حالت کو برقرار رکھنے کی ضرورت ہوتی ہے)۔

// A StreamTransformer that only emits values above a threshold
// and prefixes each one with a label.
StreamTransformer aboveThreshold(int threshold) {
  return StreamTransformer.fromHandlers(
    handleData: (value, sink) {
      // sink.add() pushes a transformed value downstream.
      // If we don't call sink.add(), the value is filtered out.
      if (value > threshold) {
        sink.add('Above threshold: $value');
      }
    },
    handleError: (error, stackTrace, sink) {
      // Forward errors downstream unchanged.
      sink.addError(error, stackTrace);
    },
    handleDone: (sink) {
      // Close the output stream when the input stream closes.
      sink.close();
    },
  );
}

// Usage
countStream()
    .transform(aboveThreshold(3))
    .listen(print);
// Above threshold: 4
// Above threshold: 5

پھڑپھڑاہٹ میں اسٹریمز کے ساتھ ڈیباؤنس کرنا

فلٹر ایپس کے لیے سب سے زیادہ عملی اسٹریم پیٹرن میں سے ایک سرچ فیلڈز کو ڈیباؤنس کرنا ہے۔ ڈی باؤنس کیے بغیر، ہر کی اسٹروک API کال کو متحرک کرتا ہے۔ ڈیباؤنسنگ کے ساتھ، یہ انتظار کرتا ہے کہ صارف کو عمل کرنے سے پہلے ٹائپ کرنا بند کر دے۔

class _SearchScreenState extends State {
  final _searchController = TextEditingController();
  final _searchStream = StreamController();
  StreamSubscription? _subscription;
  List _results = [];

  @override
  void initState() {
    super.initState();

    _subscription = _searchStream.stream
        // Wait 300ms after the last keystroke before emitting.
        // If a new value arrives within 300ms, the timer resets.
        // This prevents firing an API call on every keystroke.
        .asyncExpand((query) async* {
          await Future.delayed(const Duration(milliseconds: 300));
          yield query;
        })
        // Ignore duplicate queries — no point re-fetching
        // if the user typed the same thing again.
        .distinct()
        // For each query, call the API and emit the results.
        // asyncMap cancels the previous call if a new query
        // arrives before the previous one completes.
        .asyncMap((query) => _repository.search(query))
        .listen((results) {
          if (mounted) setState(() => _results = results);
        });

    _searchController.addListener(() {
      _searchStream.add(_searchController.text);
    });
  }

  @override
  void dispose() {
    _searchController.dispose();
    _subscription?.cancel();
    _searchStream.close();
    super.dispose();
  }
}

تنہائی: واحد دھاگے سے فرار

ڈارٹ سنگل تھریڈڈ ہے، لیکن اس کا مطلب یہ نہیں ہے کہ آپ ہمیشہ کے لیے ایک تھریڈ تک محدود ہیں۔ الگ تھلگ ڈارٹ کا کوڈ کو مکمل طور پر الگ تھریڈ میں چلانے کا طریقہ ہے۔ دوسری زبانوں میں تھریڈز سے ایک اہم فرق ہے۔

زیادہ تر زبانوں میں، تھریڈز میموری کا اشتراک کرتے ہیں۔ دو تھریڈز ایک ہی وقت میں ایک ہی متغیر کو پڑھ اور لکھ سکتے ہیں، جس سے ریس کی کیفیت پیدا ہوتی ہے اور اسے روکنے کے لیے احتیاط سے لاکنگ کی ضرورت ہوتی ہے۔

ڈارٹ تنہائی میموری کا اشتراک نہیں کرتی ہے۔ ہر تنہائی کا اپنا الگ میموری ہیپ ہوتا ہے۔ قرنطینہ میں رکھے گئے دو افراد کے درمیان بات چیت کرنے کا واحد طریقہ پیغامات بھیجنا ہے، جیسے کہ وائٹ بورڈ کا اشتراک کرنے کے بجائے دیوار میں سلاٹ کے ذریعے نوٹ بھیجنا۔

یہ ڈیزائن کے لحاظ سے تنہائی کو محفوظ بناتا ہے۔ دوڑ کے حالات نہیں ہیں کیونکہ مقابلہ کرنے کے لیے کچھ نہیں ہے۔ ہر قرنطینہ مکمل طور پر اپنے ڈیٹا کا مالک ہے۔

Main Isolate                    Worker Isolate
─────────────────               ─────────────────
Own memory heap                 Own memory heap
Own event loop                  Own event loop
UI rendering                    Heavy computation
User input                      No UI access
│                               │
│──── sends data ──────────────→│
│                               │ (processes independently)
│←─── receives result ──────────│

جب قرنطینہ درحقیقت ضروری ہو۔

اہم فرق CPU- پابند اور I/O- پابند آپریشنز کے درمیان ہے۔

  • I/O پابند آپریشن: نیٹ ورک کے جواب کا انتظار کرنا، فائل پڑھنا — بس اسے استعمال کریں۔ await. انتظار کے دوران، CPU بیکار ہے، لہذا ایونٹ لوپ مفت رہتا ہے۔

  • سی پی یو سے منسلک کام: اگر آپ واقعی کسی چیز کا حساب لگا رہے ہیں، ڈیٹا پر کارروائی کر رہے ہیں، بڑی فائلوں کو پارس کر رہے ہیں، وغیرہ، تو آپ کو تنہائی کی ضرورت ہے۔ کیونکہ CPU ہمیشہ مصروف رہتا ہے۔ await میں تمہاری مدد نہیں کر سکتا۔

اگر API کے جواب کو پارس کرنے میں 200ms لگتے ہیں، await یہ آپ کو نہیں بچائے گا۔ ایونٹ لوپ 200ms تک بلاک ہو جائے گا قطع نظر۔ کام کو الگ الگ قرنطینہ مقام پر منتقل کیا جانا چاہیے۔

Isolate.run() – ایک جدید طریقہ

Isolate.run() اسے ڈارٹ 2.19 میں شامل کیا گیا تھا اور بیک گراؤنڈ آئسولیشن میں ایک وقت کے کاموں کو چلانے کا صاف ترین طریقہ ہے۔ قرنطینہ بناتا ہے، کسی فنکشن کو انجام دیتا ہے، نتائج واپس کرتا ہے، اور خود بخود قرنطینہ بند کر دیتا ہے۔

// In your repository:
Future> getUsers() async {
  // Step 1 — network call is I/O-bound.
  // We await it and the event loop stays free while waiting.
  final response = await dio.get('/users');

  // Step 2 — parsing thousands of users is CPU-bound.
  // We move it to a separate isolate with Isolate.run().
  // The main isolate's event loop stays free the whole time.
  // Flutter keeps rendering frames normally.
  final users = await Isolate.run(() {
    final data = response.data as List;
    return data
        .map((json) => User.fromJson(json as Map))
        .toList();
  });

  return users;
}

compute() – فلٹر کے بلٹ ان مددگار

compute() یہ پچھلی قرنطینہ آئٹم کے گرد فلٹر کا ریپر ہے۔ Isolate.run(). یہ اب بھی وسیع پیمانے پر استعمال ہوتا ہے اور اچھی طرح کام کرتا ہے، لیکن اس کی ایک حد ہے۔ آپ جو فنکشن پاس کرتے ہیں وہ ایک اعلی درجے کا یا جامد فنکشن ہونا چاہیے، نہ کہ ایسا بند ہونا جو مقامی متغیرات کو پکڑتا ہے۔

// The function must be top-level or static.
// It can't be a closure because closures that capture
// state can't be sent across isolate boundaries.
List parseUsers(dynamic data) {
  return (data as List)
      .map((json) => User.fromJson(json as Map))
      .toList();
}

// In your repository:
final users = await compute(parseUsers, response.data);

زیادہ تر استعمال کے معاملات میں Isolate.run() یہ آسان اور زیادہ لچکدار ہے۔ compute() اگر آپ کو 2.19 سے نیچے فلٹر ورژن کو سپورٹ کرنے کی ضرورت ہو تب بھی یہ مفید ہے۔

اس کے ساتھ مواصلات کو مکمل طور پر الگ تھلگ کریں: SendPort اور ReceivePort

طویل عرصے سے چلنے والے پس منظر کے کام جن کو متعدد پیغامات بھیجنے اور وصول کرنے کی ضرورت ہوتی ہے (بیک گراؤنڈ سنکرونائزیشن سروس، ریئل ٹائم ڈیٹا پروسیسر، فائل واچر) ان کے لیے مکمل تنہائی کی ضرورت ہوتی ہے: SendPort اور ReceivePort.

void main() async {
  // ReceivePort is how the main isolate listens
  // for messages coming back from the worker.
  final receivePort = ReceivePort();

  // Spawn the worker isolate and give it a SendPort
  // so it can send messages back to us.
  await Isolate.spawn(
    workerFunction,
    receivePort.sendPort,
  );

  // Listen for messages from the worker.
  receivePort.listen((message) {
    print('Main received: $message');
  });
}

// This function runs entirely in the worker isolate.
// It has its own memory heap, completely separate
// from the main isolate. It cannot access any
// variables from main() directly.
void workerFunction(SendPort sendPort) {
  for (int i = 0; i < 5; i++) {
    // sendPort.send() passes a message to the main isolate.
    // The message is copied, not shared — no shared memory.
    sendPort.send('Processed item $i');
  }
}

صحیح نقطہ نظر کا انتخاب کریں۔

صورت حال استعمال کریں
ایک وقتی پس منظر کا کام Isolate.run()
فلٹر 2.19 یا اس سے کم کو سپورٹ کرنا ضروری ہے۔ compute()
طویل عرصے سے چلنے والا پس منظر کارکن مکمل طور پر الگ SendPort
نیٹ ورک یا فائل I/O کا انتظار کر رہے ہیں۔ صرف await - علیحدگی کی ضرورت نہیں ہے۔

پھڑپھڑاہٹ میں سب کچھ ایک ساتھ باندھنا

یہاں ایک مکمل مثال ہے جو ایک ہی فلٹر فنکشن میں تینوں تصورات (ایونٹ لوپ، اسٹریم اور آئسولیشن) کو ایک ساتھ استعمال کرتی ہے۔ یعنی، ایک سرچ اسکرین جو فرضی API سے نتائج لیتی ہے، پس منظر کی تنہائی میں ان کا تجزیہ کرتی ہے، اور انہیں ایک سلسلہ سے گزرتی ہے۔

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

// Model
class SearchResult {
  final String id;
  final String title;
  const SearchResult({required this.id, required this.title});
}

// Top-level function — required for Isolate.run()
// because it can't be a closure
List parseResults(List data) {
  // Simulate expensive parsing work
  return data.map((item) => SearchResult(
    id: item['id'].toString(),
    title: item['title'] as String,
  )).toList();
}

// Repository
class SearchRepository {
  // Mock data — in a real app this would be a network call
  final List> _mockData = List.generate(
    100,
    (i) => {'id': i, 'title': 'Result ${i + 1}'},
  );

  Future> search(String query) async {
    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 500));

    // Filter mock data
    final filtered = _mockData
        .where((item) =>
            (item['title'] as String)
                .toLowerCase()
                .contains(query.toLowerCase()))
        .toList();

    // Parse in a background isolate so the main
    // isolate's event loop stays free
    return Isolate.run(() => parseResults(filtered));
  }
}

// Screen
class SearchScreen extends StatefulWidget {
  const SearchScreen({super.key});

  @override
  State createState() => _SearchScreenState();
}

class _SearchScreenState extends State {
  final _controller = TextEditingController();
  final _repository = SearchRepository();

  bool _isLoading = false;
  List _results = [];
  String? _error;

  Future _search(String query) async {
    if (query.trim().isEmpty) {
      setState(() => _results = []);
      return;
    }

    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      final results = await _repository.search(query);

      // mounted check — the user might have navigated away
      // while the search was running
      if (!mounted) return;

      setState(() {
        _results = results;
        _isLoading = false;
      });
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _error="Search failed. Please try again.";
        _isLoading = false;
      });
    }
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Search')),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: TextField(
              controller: _controller,
              decoration: const InputDecoration(
                labelText: 'Search',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.search),
              ),
              onChanged: _search,
            ),
          ),
          Expanded(child: _buildBody()),
        ],
      ),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_error != null) {
      return Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(_error!),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => _search(_controller.text),
              child: const Text('Try again'),
            ),
          ],
        ),
      );
    }

    if (_results.isEmpty) {
      return const Center(child: Text('No results found.'));
    }

    return ListView.builder(
      itemCount: _results.length,
      itemBuilder: (context, index) {
        final result = _results[index];
        return ListTile(
          leading: Text(result.id),
          title: Text(result.title),
        );
      },
    );
  }
}

void main() {
  runApp(const MaterialApp(home: SearchScreen()));
}

یہ مثال ہر اس چیز کو مضبوط کرتی ہے جس کا ہم نے اب تک احاطہ کیا ہے۔

  • کہ ایونٹ لوپ نقلی نیٹ ورک کی تاخیر کے دوران UI کی ردعمل کو برقرار رکھتا ہے۔ await ہم کنٹرول کو واپس ایونٹ لوپ پر دے دیتے ہیں تاکہ Flutter فریموں کو پیش کرنا جاری رکھ سکے۔

  • قرنطینہ یہ پس منظر میں تصریف کی کارروائیوں کو سنبھالتا ہے، مین تھریڈ کو آزاد رکھتا ہے یہاں تک کہ جب نتیجہ سیٹ بڑا ہو۔

  • کہ نصب چیک تلاش جاری ہونے کے دوران وجیٹس کو حذف ہونے سے روکتا ہے۔

  • تمام چار UI ریاستیں (لوڈ، خرابی، خالی، نتیجہ) کو واضح طور پر سنبھالا جاتا ہے۔

حتمی خیالات

ایونٹ لوپس، اسٹریمز اور آئسولیشن کو سمجھنے سے آپ کو یہ سمجھنے میں مدد ملے گی کہ ڈارٹ اس طرح کیوں کام کرتا ہے۔ ایک بار جب ایک ذہنی نمونہ اپنی جگہ پر آجاتا ہے، تو بہت سی چیزیں جو پہلے صوابدیدی محسوس ہوتی تھیں، معنی میں آنے لگتی ہیں۔

آپ کو اس کی ضرورت کیوں ہے؟ mounted چیک کریں کیونکہ await فنکشن کو روکتا ہے اور کنٹرول کو ایونٹ لوپ پر لوٹاتا ہے۔ فنکشن کے دوبارہ شروع ہونے سے پہلے ویجیٹ کو حذف کیا جا سکتا ہے۔ کیوں compute() کیا آپ ہکلاتے ہیں؟ اس کی وجہ یہ ہے کہ سی پی یو سے منسلک ٹاسک ایونٹ کے لوپس کو روکتے ہیں، اور انہیں تنہائی میں منتقل کرنے سے لوپ ٹوٹ جاتا ہے اور رینڈرنگ کو جاری رکھنے کی اجازت دیتا ہے۔ نشریاتی سلسلے کیوں موجود ہیں؟ اس کی وجہ یہ ہے کہ پہلے سے طے شدہ واحد سبسکرپشن سٹریم صرف ایک سننے کو اجازت دیتا ہے، اور کچھ ڈیٹا کے ذرائع کے لیے ایپ کے متعدد حصوں کو بیک وقت پیش کرنے کی ضرورت ہوتی ہے۔

یہ حفظ کرنے کے الگ الگ اصول نہیں ہیں۔ اگر آپ اسے شروع سے سمجھتے ہیں، تو یہ سب ایک ہی تھریڈڈ کنکرنسی ماڈل کا نتیجہ ہیں۔

اگر آپ پہلے ہی آرام سے ہیں۔ await اور FutureBuilderاس مضمون سے ایک تصور چنیں اور اس ہفتے مزید تفصیل کے ساتھ اس کا جائزہ لیں۔ اسٹریم ڈیباؤنس کی مثال بنائیں۔ سخت کوشش کریں Isolate.run() آپ کی ایپس میں سے ایک اصل تجزیہ کا کام کرے گی۔ Flutter DevTools میں دیکھیں کہ فریم ریٹ سے پہلے اور بعد میں کیا ہوتا ہے۔ جب آپ اپنے کوڈ کو عمل میں دیکھتے ہیں تو سمجھ بہت تیزی سے آتی ہے۔

اوپر تک سکرول کریں۔