آبزرور ڈیزائن پیٹرن ہینڈ بک: ڈارٹ میں ایونٹ سے چلنے والا آرکیٹیکچر اور ڈومین سے چلنے والا ڈیزائن

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

جب کوئی صارف لاگ ان ہوتا ہے، تو آپ کی ایپ کو ٹوکن کو ذخیرہ کرنا، صارف پروفائل کو کیش کرنا، تجزیاتی واقعات کو فائر کرنا، اور ہوم اسکرین پر نیویگیٹ کرنا چاہیے۔

ادائیگی کی تصدیق ہونی چاہیے، انوینٹری کو اپ ڈیٹ کیا جانا چاہیے، صارف کو ایک رسید ملنی چاہیے، اور تکمیل کے نظام کو ڈیلیوری شروع کرنی چاہیے۔

سینسر ریڈنگز بدل جاتی ہیں اور نئی اقدار کو بیک وقت تین مختلف UI پینلز میں منعکس کرنے کی ضرورت ہے۔

ایک سادہ حل یہ ہو گا کہ تمام منطق کو ایک جگہ لکھ دیا جائے۔ ایک فنکشن جو یہ سب کرتا ہے، یا ایک کلاس جو سب کچھ جانتا ہے۔

یہ سب سے پہلے کام کرتا ہے. پھر تقاضے بدل جاتے ہیں۔ آپ کو ایک نیا ردعمل شامل کرنے کی ضرورت ہے۔ آپ کو موجودہ اندراج کو ہٹانے کی ضرورت ہوگی۔ ضمنی اثرات ناکام ہونے لگتے ہیں اور باقی سب کچھ ان کے ساتھ گر جاتا ہے۔ کوڈ ذمہ داری کی ایک دیوار بن جاتا ہے جو ناقابل برداشت، بڑھانا مشکل اور چھونا خطرناک ہے۔

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

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

آخر میں، آپ پیٹرن کو نہیں سمجھ پائیں گے۔ آپ اپنے پروڈکشن کوڈ میں جان بوجھ کر اسے استعمال کرنے کا طریقہ سیکھیں گے۔

انڈیکس

مبصر ڈیزائن پیٹرن کیا ہے؟

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

اخبار کی رکنیت کی خدمت پر غور کریں۔ اخبار پبلشرز نہیں جانتے کہ ان کے انفرادی سبسکرائبرز کون ہیں۔ ہم ہر قاری کو ذاتی طور پر نہیں کہتے ہیں۔ ہر سبسکرائبر جو کاغذ شائع کرتا ہے اور اسے رجسٹر کرتا ہے اسے وصول کرتا ہے۔

سبسکرائبرز کسی بھی وقت منسوخ کر سکتے ہیں۔ نئے سبسکرائبرز کسی بھی وقت شامل ہو سکتے ہیں۔ پبلشر کا کام کبھی نہیں بدلتا۔ بس اسے شائع کریں۔

یہ مبصر پیٹرن ہے.

اسے پبلشر کہتے ہیں۔ موضوع. وہ سبسکرائبر کہلاتے ہیں۔ مبصر. اخبار ہے۔ واقعہ.

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

مسئلہ یہ حل کرتا ہے

آئیے دیکھتے ہیں کہ آبزرور پیٹرن کے بغیر کیا ہوتا ہے۔

ہم کہتے ہیں کہ ہمارے پاس لاگ ان فنکشن ہے۔ آپ کا لاگ ان کامیاب ہونے کے بعد، آپ کو چار چیزیں کرنے کی ضرورت ہے:

  • تصدیقی ٹوکن کو محفوظ اسٹوریج میں اسٹور کریں۔

  • صارف پروفائل ڈیٹا کیش

  • ہوم اسکرین پر جائیں۔

  • تجزیاتی ایونٹس چلائیں۔

لاگ ان فنکشن کے اندر یہ سب ڈالنا ایک آسان طریقہ ہے۔

Future login(String email, String password) async {
  final response = await _authRepository.login(email, password);

  await _secureStorage.write(key: 'token', value: response.token);
  await _userCache.save(response.user);
  _navigationService.navigateTo('/home');
  _analytics.track('login_success', {'userId': response.user.id});
}

پہلی نظر میں یہ ٹھیک لگتا ہے۔ لیکن اندازہ لگائیں کہ اس واحد فنکشن کو تبدیل کرنے کی کتنی وجوہات ہیں؟

  • ٹوکن اسٹوریج کی حکمت عملی میں تبدیلی۔ اس فنکشن کو درست کریں:

  • نیویگیشن کی منزل بدل جاتی ہے۔ اس فنکشن کو درست کریں:

  • تجزیاتی ایونٹ کا نام یا پے لوڈ تبدیلیاں۔ اس فنکشن کو درست کریں:

  • صارف کی کیشنگ منطق میں تبدیلیاں۔ اس فنکشن کو درست کریں:

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

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

اسے ٹائٹ کپلنگ کہتے ہیں۔ لاگ ان منطق کو کامیاب لاگ ان کے ہر ایک نتیجہ کے ساتھ ملایا جاتا ہے۔

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

بنیادی اجزاء

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

موضوع

موضوع وہ چیز ہے جس پر کچھ ہوتا ہے۔ یہ مبصرین کی فہرست رکھتا ہے اور واقعات رونما ہونے پر انہیں مطلع کرنے کا ذمہ دار ہے۔ ایسے طریقوں کو بے نقاب کرتا ہے جو مبصرین کو خود کو رجسٹر کرنے اور غیر رجسٹر کرنے کی اجازت دیتے ہیں۔ مضمون کو اس بات کی کوئی پرواہ نہیں ہے کہ مبصر نوٹیفکیشن کے ساتھ کیا کرتا ہے۔ بس اسے منتقل کریں۔

مبصر

مبصر ایک انٹرفیس یا تجریدی کلاس ہے جو ایک معاہدے کی وضاحت کرتا ہے جس پر تمام مبصرین کو عمل کرنا چاہیے۔ یہ اس طریقہ کا اعلان کرتا ہے جس کو مطلع کرتے وقت موضوع کال کرے گا۔ کوئی بھی طبقہ جو واقعات پر ردعمل ظاہر کرنا چاہتا ہے اسے اس انٹرفیس کو نافذ کرنا ہوگا۔

مخصوص موضوع

ٹھوس موضوع موضوع کا حقیقی نفاذ ہے۔ یہ لائیو مبصرین کی فہرست کا انتظام کرتا ہے، سبسکرپشنز کو سنبھالتا ہے، اور صحیح وقت پر اطلاعات کو فائر کرتا ہے۔

ٹھوس مبصر

یہ اصل کلاس ہے جو آبزرور انٹرفیس کو نافذ کرتی ہے۔ ہر شخص کے پاس ایک مخصوص، توجہ مرکوز کام ہوتا ہے جس کو انجام دینے کے لیے وہ اطلاع موصول کرتے ہیں۔ ایک ٹوکن ذخیرہ کرتا ہے۔ ایک دریافت کرتا ہے۔ ایک تجزیہ شروع کرتا ہے۔ وہ نہیں جانتے اور ایک دوسرے کو جاننے کی ضرورت نہیں ہے۔

یہاں یہ ہے کہ وہ ایک دوسرے سے کیسے متعلق ہیں:

Subject (LoginService)
    |
    |-- subscribe(observer)    <- observer registers itself
    |-- unsubscribe(observer)  <- observer removes itself
    |-- notifySuccess(data)    <- fires when login succeeds
    |-- notifyFailure(error)   <- fires when login fails
         |
         |-----> TokenObserver.onLoginSuccess()
         |-----> UserObserver.onLoginSuccess()
         |-----> NavigationObserver.onLoginSuccess()
         |-----> AnalyticsObserver.onLoginSuccess()

موضوع ان سب کو مطلع کرے گا۔ وہ ہر ایک آزادانہ طور پر اپنا کام کرتے ہیں۔

ڈارٹ میں ایک مبصر کو نافذ کرنا

آئیے قدم بہ قدم ایک پیٹرن بنائیں۔

مرحلہ 1: آبزرور انٹرفیس کی وضاحت کریں۔

abstract class LoginObserver {
  void onLoginSuccess(UserDto user);
  void onLoginFailed(AppException error);
}

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

onLoginSuccess لاگ ان کامیاب ہونے اور صارف کا ڈیٹا موصول ہونے پر کال کی جاتی ہے۔ onLoginFailed لاگ ان ناکام ہونے اور غلطی موصول ہونے پر کال کی جاتی ہے۔

مرحلہ 2: موضوع کے انٹرفیس کی وضاحت کریں۔

abstract class LoginSubject {
  void subscribe(LoginObserver observer);
  void unsubscribe(LoginObserver observer);
  void notifySuccess(UserDto user);
  void notifyFailure(AppException error);
}

subscribe مبصرین کو اطلاع کی فہرست میں حصہ لینے کی اجازت دیتا ہے۔ unsubscribe مبصرین کو اطلاع کی فہرست چھوڑنے کی اجازت دیتا ہے۔ notifySuccess تمام رجسٹرڈ مبصرین کے لیے صارف کے ڈیٹا پر مشتمل ایک کامیاب ایونٹ نشر کرتا ہے۔ notifyFailure تمام رجسٹرڈ مبصرین کے لیے خرابی کے ساتھ ناکامی کا واقعہ نشر کرتا ہے۔

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

مرحلہ 3: مخصوص عنوانات کو نافذ کریں۔

class LoginService implements LoginSubject {
  final List _observers = [];

  @override
  void subscribe(LoginObserver observer) {
    _observers.add(observer);
  }

  @override
  void unsubscribe(LoginObserver observer) {
    _observers.remove(observer);
  }

  @override
  void notifySuccess(UserDto user) {
    for (final observer in List.of(_observers)) {
      try {
        observer.onLoginSuccess(user);
      } catch (e) {
        debugPrint('Observer error on success: $e');
      }
    }
  }

  @override
  void notifyFailure(AppException error) {
    for (final observer in List.of(_observers)) {
      try {
        observer.onLoginFailed(error);
      } catch (e) {
        debugPrint('Observer error on failure: $e');
      }
    }
  }
}

اس نفاذ میں دو اہم فیصلے ہیں جن کو یاد کرنا آسان ہے۔

1. سنیپ شاٹ دہرائیں۔ List.of()

براہ راست تکرار کرنے کے بجائے _observersہم دہراتے ہیں List.of(_observers)لوپ چلنے سے پہلے فہرست کی ایک کاپی بناتا ہے۔

یہ کیوں ضروری ہے؟ تصور کریں۔ NavigationObserver ہوم اسکرین پر جائیں اور ان سبسکرائب کریں کیونکہ اب آپ کو سننے کی ضرورت نہیں ہے۔ اگر آپ کال کریں unsubscribe دوران notifySuccess اگر لوپ اب بھی اسی فہرست پر چل رہا ہے تو، ڈارٹ اس طرح کی غلطی پھینک دے گا: ConcurrentModificationError. فہرست پڑھتے وقت اس میں ترمیم کی جاتی ہے۔

List.of() اس کو مکمل طور پر روکتا ہے۔ لوپ سنیپ شاٹس کے ذریعے چلتا ہے۔ اصل فہرست میں بغیر کسی غلطی کے تکرار کے دوران آزادانہ طور پر ترمیم کی جا سکتی ہے۔

2. مبصر کے ذریعے پکڑنے کی کوشش

ہر مبصر کال کو اس کے اپنے ٹرائی/کیچ بلاک میں لپیٹا جاتا ہے۔ یہ جان بوجھ کر کیا گیا انتخاب ہے۔ اگر TokenObserver محفوظ اسٹوریج کے لیے لکھتے وقت ایک استثناء ہوتا ہے۔ مجھے یہ نہیں چاہیے NavigationObserver اور AnalyticsObserver خاموشی سے فائر نہ کریں۔ ہر مبصر کو دوڑنے کا موقع ملتا ہے قطع نظر اس کے کہ دوسرے کیا کرتے ہیں۔

اس کے بغیر، ایک ناکام مبصر پورے نوٹیفکیشن چین کو روک دے گا۔ یہ ایک پوشیدہ بگ ہے جسے پروڈکشن میں ٹریک کرنا بہت مشکل ہے۔

حقیقی زندگی کی مثال: لاگ ان فلو

اب آئیے پورے لاگ ان فلو کو بنانے کے لیے اس فاؤنڈیشن کا استعمال کریں۔

لاگ ان منطق

class LoginLogic {
  final LoginSubject _subject;
  final AuthRepository _repository;

  LoginLogic({
    required LoginSubject subject,
    required AuthRepository repository,
  })  : _subject = subject,
        _repository = repository;

  Future callLogin(LoginRequest request) async {
    try {
      final user = await _repository.login(request);
      _subject.notifySuccess(user);
    } on AppException catch (e) {
      _subject.notifyFailure(e);
    } catch (e) {
      _subject.notifyFailure(AppException.unknown(message: e.toString()));
    }
  }
}

آئیے اس کو غور سے دیکھتے ہیں۔

LoginLogic یہ کنسٹرکٹر کے ذریعے دو انحصار لیتا ہے: LoginSubject اور AuthRepository. چیک کریں کہ یہ کیا لیتا ہے۔ LoginSubjectتجرید نہیں LoginServiceکنکریٹ کلاس۔ اس کا مطلب یہ ہے کہ آپ بغیر کسی تبدیلی کے جانچ یا دوسرے ماحول میں عمل درآمد کو تبدیل کر سکتے ہیں۔ LoginLogic کوئی بھی

اندر callLoginمنطق سادہ ہے۔ ہم اصل لاگ ان کرنے کے لیے ریپوزٹری کو کال کرتے ہیں۔ اگر کامیاب ہو، تو ہم کال کریں: notifySuccess اگر یہ اسے واپس آنے والے صارف سے متعلق کسی موضوع پر پھینک دیتا ہے: AppExceptionیہ کال کرتا ہے notifyFailure اس غلطی کے ساتھ۔ اگر کچھ غیر متوقع ہوتا ہے، تو اسے اس طرح لپیٹیں: AppException.unknown اور یہ ناکامی کا اشارہ دیتا ہے۔

کس چیز پر توجہ دیں۔ LoginLogic میں نہیں کرتا ٹوکنز محفوظ نہیں ہیں۔ یہ کہیں بھی نہیں جاتا ہے۔ یہ کچھ بھی کیش نہیں کرتا ہے۔ تجزیہ شروع نہیں کرتا۔ اور ہم نہیں جانتے کہ وہاں کتنے مبصر ہیں یا وہ کیا کرتے ہیں۔

آپ کی پوری ذمہ داری لاگ ان کرنا اور نتائج سے آپ کو مطلع کرنا ہے۔

ٹھوس مبصر

class TokenObserver implements LoginObserver {
  final SecureStorageService _storage;

  TokenObserver(this._storage);

  @override
  void onLoginSuccess(UserDto user) {
    _storage.write(key: 'auth_token', value: user.token);
  }

  @override
  void onLoginFailed(AppException error) {
    _storage.delete(key: 'auth_token');
  }
}

TokenObserver ایک کام ہے۔ یہ توثیق ٹوکن کے انتظام کے بارے میں ہے۔ اگر کامیاب ہو تو ٹوکن محفوظ ہو جاتا ہے۔ اگر یہ ناکام ہو جاتا ہے، تو یہ کسی بھی پرانے ٹوکن کو صاف کر دیتا ہے جو اسٹوریج میں ہو سکتے ہیں۔ میں نیویگیشن، کیشنگ، یا تجزیات کے بارے میں کچھ نہیں جانتا ہوں۔

class UserObserver implements LoginObserver {
  final UserCacheService _cache;

  UserObserver(this._cache);

  @override
  void onLoginSuccess(UserDto user) {
    _cache.save(user);
  }

  @override
  void onLoginFailed(AppException error) {
    _cache.clear();
  }
}

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

class NavigationObserver implements LoginObserver {
  final NavigationService _navigation;

  NavigationObserver(this._navigation);

  @override
  void onLoginSuccess(UserDto user) {
    _navigation.navigateTo('/home');
  }

  @override
  void onLoginFailed(AppException error) {
    _navigation.showError(error.message);
  }
}

NavigationObserver اس کا ایک کام ہے: لاگ ان کی کوشش کے بعد نیویگیشن کو ہینڈل کریں۔ میں انجیکشن فارمولہ استعمال کرتا ہوں۔ NavigationService تجرید کے بجائے BuildContext. یہ جان بوجھ کر ہے۔ منحصر مبصر BuildContext یہ ویجیٹ لائف سائیکل سے منسلک ہے۔ خلاصہ اس مبصر کو UI پرت سے مکمل طور پر آزاد رہنے کی اجازت دیتا ہے۔

class AnalyticsObserver implements LoginObserver {
  final AnalyticsService _analytics;

  AnalyticsObserver(this._analytics);

  @override
  void onLoginSuccess(UserDto user) {
    _analytics.track('login_success', {'userId': user.id});
  }

  @override
  void onLoginFailed(AppException error) {
    _analytics.track('login_failed', {'reason': error.message});
  }
}

AnalyticsObserver کام ایک ہے۔ یہ ہر نتیجہ کے لیے صحیح تجزیہ ایونٹ کو فائر کرنے کے بارے میں ہے۔ اسٹوریج، نیویگیشن یا کیشنگ کا کوئی علم نہیں۔

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

ایک ساتھ وائرنگ

void setupLogin() {
  final service = LoginService();

  service
    ..subscribe(TokenObserver(secureStorage))
    ..subscribe(UserObserver(userCache))
    ..subscribe(NavigationObserver(navigationService))
    ..subscribe(AnalyticsObserver(analyticsService));

  final loginLogic = LoginLogic(
    subject: service,
    repository: authRepository,
  );
}

یہ ترتیب کا مرحلہ ہے۔ تمام مبصرین اپنے انحصار کا استعمال کرتے ہوئے بنائے گئے ہیں اور سروس کے ساتھ رجسٹرڈ ہیں۔ جھرن آپریٹر .. فون کال subscribe ایک ہی چیز کئی بار service ایک ایسی چیز جس میں سیٹنگز پڑھنے کے قابل ہوں۔

LoginLogic استقبالیہ service اس کا LoginSubject. اب سے، ہر بار callLogin جب کال کی جاتی ہے اور نتیجہ آتا ہے تو چاروں مبصرین کو خود بخود مطلع کر دیا جاتا ہے۔

پانچویں مبصر کو شامل کرنے سے ہمیں ملتا ہے: PushNotificationObserverاس کا مطلب ہے ایک کلاس بنائیں اور اس میں ایک لائن شامل کریں: ..subscribe(PushNotificationObserver(pushService)). پورے کوڈ بیس میں اور کچھ نہیں بدلتا۔

عام EventBus کا استعمال کرتے ہوئے اسے پروڈکشن گریڈ بنائیں

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

سبجیکٹ اور آبزرور انٹرفیس کو فنکشن بہ فنکشن کی بنیاد پر دوبارہ لکھنا بار بار اور غیر ضروری ہے۔ ایک بہتر نقطہ نظر ایک عمومی ہے۔ EventBus تمام خصوصیات دستیاب ہیں۔

abstract class DomainObserver {
  void onSuccess(T data);
  void onFailure(AppException error);
}

DomainObserver میں ایک غیر معمولی مبصر ہوں۔ پیرامیٹر ٹائپ کریں۔ T ڈیٹا کی قسم کی نشاندہی کرتا ہے جب مبصر کامیاب ہونے کی توقع کرتا ہے۔ لاگ ان مبصر اس طرح لگتا ہے: DomainObserver. تنخواہ مبصر DomainObserver. انٹرفیس ایک ہی ہے۔ ڈیٹا کی اقسام فنکشن سے فنکشن میں تبدیل ہوتی ہیں۔

class EventBus {
  final List> _observers = [];

  void subscribe(DomainObserver observer) {
    _observers.add(observer);
  }

  void unsubscribe(DomainObserver observer) {
    _observers.remove(observer);
  }

  void publishSuccess(T data) {
    for (final observer in List.of(_observers)) {
      try {
        observer.onSuccess(data);
      } catch (e) {
        debugPrint('[EventBus] Observer error on success: $e');
      }
    }
  }

  void publishFailure(AppException error) {
    for (final observer in List.of(_observers)) {
      try {
        observer.onFailure(error);
      } catch (e) {
        debugPrint('[EventBus] Observer error on failure: $e');
      }
    }
  }
}

EventBus یہ ایک عمومی موضوع ہے۔ یہ مبصرین کی درج کردہ فہرست کا انتظام کرتا ہے اور انہیں اسی سنیپ شاٹ تکرار اور فی مبصر غلطی تنہائی کے ساتھ مطلع کرتا ہے جیسا کہ پہلے ترتیب دیا گیا تھا۔

اب تمام فنکشنز کو پیٹرن کی ایک لائن ڈپلیکیٹ کیے بغیر ایک ہی انفراسٹرکچر ملتا ہے۔

final loginBus = EventBus();
final paymentBus = EventBus();
final orderBus = EventBus();

ہر بس کو اس کے ڈومین تصور کے مطابق ٹائپ کیا جاتا ہے۔ کے ساتھ رجسٹرڈ آبزرور loginBus آپ کو غلطی سے ادائیگی کے واقعات موصول نہیں ہوں گے۔ قسم کا نظام درستگی کو نافذ کرتا ہے۔

مبصر پہلے سے ہی آپ کے فلٹر کوڈ میں ہے۔

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

اسٹریمز اور اسٹریم کنٹرولرز:

final controller = StreamController();

controller.stream.listen((event) {
  print('Observed: $event');
});

controller.sink.add('Login succeeded');

StreamController یہ موضوع ہے۔ stream.listen ہے subscribe. sink.add ہے notifyObservers. تمام سٹریم سبسکرپشنز مبصر ہیں۔ پیٹرن ایک ہی ہے. پھڑپھڑانے نے اسے صرف ایک مختلف نام دیا ہے۔

تبدیلی کی اطلاع:

class CounterModel extends ChangeNotifier {
  int _count = 0;

  void increment() {
    _count++;
    notifyListeners();
  }
}

notifyListeners() تمام رجسٹرڈ سامعین پر اعادہ کرتا ہے۔ وہ سننے والا مبصر ہے۔ addListener ہے subscribe. removeListener ہے unsubscribe. ChangeNotifier یہ ایک مخصوص موضوع ہے۔

بلاک:

جب BLoC ایک نئی حالت کا اخراج کرتا ہے، تو اس کو لپیٹنے والے تمام ویجٹ ہوتے ہیں۔ BlocBuilder یا BlocListener رد عمل BLOC ایک موضوع ہے۔ معمار اور سننے والے مبصر ہیں۔ ریاستی اخراج اطلاعات ہیں۔

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

ایونٹ سے چلنے والے فن تعمیر کے بارے میں مزید جانیں۔

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

واقعہ سے چلنے والا فن تعمیر کیا ہے؟

واقعہ سے چلنے والا فن تعمیر ایک ڈیزائن کا نمونہ ہے جس میں کسی ایپلیکیشن کے بہاؤ کا تعین واقعات سے ہوتا ہے۔ ایک دوسرے کو براہ راست کال کرنے کے بجائے، اجزاء مشترکہ بس یا چینل پر ایونٹس تیار کرکے اور استعمال کرکے بات چیت کرتے ہیں۔

روایتی درخواست پر مبنی بہاؤ میں، درج ذیل ہوتا ہے:

Component A calls Component B directly
Component B does its work and returns a result
Component A waits for that result and then continues

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

تاہم، جزو A میں اضافہ ہوتا ہے۔ جزو A ان نتائج کو مربوط کرنے کے لیے ذمہ دار ہو گا جس سے وہ لاعلم ہے۔

واقعہ سے چلنے والے بہاؤ میں، اس کے بجائے، درج ذیل ہوتا ہے:

Component A publishes an event to the EventBus
EventBus delivers the event to whoever is registered

Component B handles the event
Component C handles the event
Component D handles the event

جزو A B، C اور D کے بارے میں نہیں جانتا۔ اس کا انتظار نہ کریں۔ کیا ہوا ظاہر کریں اور آگے بڑھیں۔ آپ جزو A کو چھوئے بغیر نئے ہینڈلرز شامل کر سکتے ہیں۔ یہ آبزرور پیٹرن ہے جسے آرکیٹیکچرل سطح تک بڑھایا گیا ہے۔

واقعات حقائق ہیں احکام نہیں۔

یہ امتیاز واقعہ سے چلنے والے فن تعمیر میں سب سے اہم تصورات میں سے ایک ہے۔

حکم کہتا ہے، "یہ کرو۔” یہ ایک ایسا حکم ہے جس سے آپ انکار کر سکتے ہیں۔ میں آپ کے جواب کا منتظر ہوں۔

واقعہ کہے گا "یہ ہوا”۔ یہ حقیقت کا ایک ناقابل تغیر ریکارڈ ہے۔ مجھے جواب کی توقع نہیں ہے۔ اس سے کوئی فرق نہیں پڑتا کہ اسے کون سنبھالتا ہے۔

SaveUserToken یہ ایک حکم ہے۔ UserLoggedIn یہ ایک واقعہ ہے۔

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

ڈارٹ میں ماڈلنگ ڈومین ایونٹس

ایونٹس کو ناقابل تغیر قدر اشیاء ہونا چاہیے۔ یہ سچ ہے۔ حقائق رونما ہونے کے بعد نہیں بدلتے۔

abstract class DomainEvent {
  final DateTime occurredAt;
  final String eventId;

  const DomainEvent({
    required this.occurredAt,
    required this.eventId,
  });
}

DomainEvent سسٹم میں تمام واقعات کے لیے بیس کلاس۔ تمام واقعات کا ٹائم اسٹیمپ ہوتا ہے (occurredAt) واقعہ کے وقت ریکارڈ اور منفرد شناخت کنندہ (eventId) ٹریس ایبلٹی کے لیے۔

class UserLoggedIn extends DomainEvent {
  final UserDto user;

  const UserLoggedIn({
    required this.user,
    required super.occurredAt,
    required super.eventId,
  });
}

class LoginFailed extends DomainEvent {
  final AppException error;

  const LoginFailed({
    required this.error,
    required super.occurredAt,
    required super.eventId,
  });
}

UserLoggedIn صارف کا ڈیٹا لے جاتا ہے۔ LoginFailed ایک خرابی ہوتی ہے۔ دونوں ناقابل تغیر ہیں۔ دونوں کے پاس ٹائم اسٹیمپ اور ایک شناخت کنندہ ہے۔ ڈومین میں کیا ہوا اس کے بارے میں دونوں مخصوص حقائق ہیں۔

ٹائپ سیف ڈومین ایونٹ بس

اب ہم ایک ایونٹ بس بنا سکتے ہیں جو خاص طور پر ڈومین ایونٹس کو فیڈ کرتی ہے۔

abstract class EventHandler {
  void handle(T event);
}

EventHandler یہ اس فن تعمیر کا مبصر انٹرفیس ہے۔ کوئی بھی کلاس جو ڈومین ایونٹس کو ہینڈل کرنا چاہتی ہے وہ مخصوص ایونٹ کی دلچسپی کا استعمال کرتے ہوئے اسے لاگو کرتی ہے۔

class DomainEventBus {
  final _handlers = >{};

  void register(EventHandler handler) {
    _handlers.putIfAbsent(T, () => []).add(handler);
  }

  void publish(T event) {
    final handlers = List.of(_handlers[T] ?? []);
    for (final handler in handlers) {
      try {
        (handler as EventHandler).handle(event);
      } catch (e) {
        debugPrint('[DomainEventBus] Handler error for ${T}: $e');
      }
    }
  }
}

چلو گزرتے ہیں DomainEventBus ہوشیار رہو۔

_handlers یہ ایک چابی والا نقشہ ہے۔ Type (ایونٹ کلاس خود ہے۔ UserLoggedIn) قدر اس ایونٹ کی قسم کے لیے رجسٹرڈ تمام ہینڈلرز کی فہرست ہے۔

register ایک ہینڈلر حاصل کرتا ہے اور اسے قسم کی فہرست میں شامل کرتا ہے۔ T. putIfAbsent اگر یہ اس ایونٹ کی قسم کا پہلا ہینڈلر ہے، تو دیکھیں کہ آیا یہ فہرست بناتا ہے۔

publish شائع ہونے والے ایونٹ کی قسم کے لیے رجسٹرڈ تمام ہینڈلرز کو تلاش کرتا ہے اور ہر ہینڈلر کو کال کرتا ہے۔ handle طریقہ سنیپ شاٹ List.of() فی ہینڈلر ٹرائی/کیچ انہی وجوہات کی بنا پر موجود ہیں جو ہم نے پہلے قائم کی ہیں۔

ہینڈلر کو رجسٹر کرنے اور ایونٹ پوسٹ کرنے کا طریقہ یہاں ہے:

// Registration happens once at startup
eventBus.register(TokenHandler(secureStorage));
eventBus.register(UserCacheHandler(userCache));
eventBus.register(NavigationHandler(navigationService));
eventBus.register(AnalyticsHandler(analyticsService));

eventBus.register(ReceiptHandler(receiptService));
eventBus.register(InventoryHandler(inventoryService));

// Publishing happens at the use case level
eventBus.publish(UserLoggedIn(
  user: user,
  occurredAt: DateTime.now(),
  eventId: const Uuid().v4(),
));

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

ڈومین پر مبنی ڈیزائن کے لیے درخواست

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

کلیدی DDD تصورات جو آپ کو جاننے کی ضرورت ہے۔

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

UserLoggedIn یہ ایک ڈومین ایونٹ ہے۔ LoginResponseDto ڈیٹا ٹرانسفر آبجیکٹ۔ تفریق بہت اہم ہے۔ واقعات ڈومین ماڈل سے تعلق رکھتے ہیں اور کاروباری زبان کی نمائندگی کرتے ہیں۔ ڈی ٹی اوز ڈیٹا لیئر سے تعلق رکھتے ہیں اور ڈیٹا ڈھانچے کی نمائندگی کرتے ہیں۔

تعداد یہ ڈومین کے واقعات کا قدرتی ذریعہ ہے۔ ایک مجموعی ڈومین اشیاء کا ایک جھرمٹ ہے جو مستقل مزاجی کی حد بناتا ہے۔ ایگریگیٹس کاروباری قواعد کو نافذ کرتے ہیں اور ڈومین کے واقعات کو بڑھاتے ہیں جب ان کے اندر اہم ریاستی تبدیلیاں رونما ہوتی ہیں۔

کیسز استعمال کریں۔ یہ آرکیسٹریٹرز ہیں۔ استعمال کا معاملہ ریپوزٹری کو کال کرنا، نتیجہ حاصل کرنا، مناسب ڈومین ایونٹ کو بڑھانا، اور نتیجہ واپس کرنا ہے۔ یہ براہ راست ضمنی اثرات سے نمٹنے نہیں کرتا ہے۔ رجسٹرڈ ہینڈلر کو بتائیں کہ کیا ہوا اور رجسٹرڈ ہینڈلر کو سنبھالنے دیں۔

جہاں ہر چیز صاف ستھرے فن تعمیر میں رہتی ہے۔

lib/
  core/
    events/
      domain_event.dart           <- Base DomainEvent class
      domain_event_bus.dart       <- The DomainEventBus
      event_handler.dart          <- Base EventHandler interface

  features/
    auth/
      domain/
        events/
          user_logged_in.dart     <- Domain event (pure Dart, no Flutter)
          login_failed.dart       <- Domain event
        handlers/
          token_handler.dart      <- Handles token storage
          user_cache_handler.dart <- Handles user caching
          analytics_handler.dart  <- Handles analytics
        entities/
          user.dart
        repositories/
          auth_repository.dart    <- Abstract interface only
        usecases/
          login_usecase.dart      <- Orchestrates, publishes events

      data/
        repositories/
          auth_repository_impl.dart
        datasources/
          auth_remote_datasource.dart

      presentation/
        providers/
          login_provider.dart     <- Riverpod notifier (thin)
        pages/
          login_page.dart

اہم اصول: ڈومین پرت خالص ڈارٹ ہے۔ کوئی فلٹر درآمدات نہیں ہیں۔ ریور پوڈ درآمد نہیں کرتا ہے۔ کوئی HTTP بازیافت نہیں ہے۔ کہ DomainEventBusڈومین ایونٹس، ہینڈلرز، اور استعمال کے معاملات سبھی ڈومین لیئر میں رہتے ہیں اور ان کا کوئی فریم ورک انحصار نہیں ہوتا ہے۔

اس کا مطلب ہے کہ وہی ڈومین منطق فلٹر، سرور سائڈ ڈارٹ، یا سی ایل آئی ٹولز میں بغیر ایک لائن کو تبدیل کیے کام کرے گی۔ ریور پوڈ 2.x سے مستقبل کے ورژن تک فریم ورک اپ گریڈ آپ کے ڈومین کو نہیں چھوئے گا۔ آپ کے ڈومین کے یونٹ ٹیسٹ بغیر کسی ویجیٹ ٹیسٹنگ اوور ہیڈ کے ملی سیکنڈ میں چلتے ہیں۔

DDD کے لیے لاگ ان استعمال کے کیسز

class LoginUseCase {
  final AuthRepository _repository;
  final DomainEventBus _eventBus;

  LoginUseCase({
    required AuthRepository repository,
    required DomainEventBus eventBus,
  })  : _repository = repository,
        _eventBus = eventBus;

  Future> execute(LoginRequest request) async {
    try {
      final user = await _repository.login(request);

      _eventBus.publish(UserLoggedIn(
        user: user,
        occurredAt: DateTime.now(),
        eventId: const Uuid().v4(),
      ));

      return Result.success(user);
    } on AppException catch (e) {
      _eventBus.publish(LoginFailed(
        error: e,
        occurredAt: DateTime.now(),
        eventId: const Uuid().v4(),
      ));

      return Result.failure(e);
    }
  }
}

آئیے ان مراحل سے قدم بہ قدم چلتے ہیں۔

LoginUseCase یہ دو انحصار لیتا ہے: AuthRepository خلاصہ اور DomainEventBus. یہ ایک کنکریٹ کلاس بھی نہیں ہے۔ دونوں کو جانچ میں تبدیل کیا جا سکتا ہے۔

اندر executeلاگ ان کرنے کے لیے ہم ریپوزٹری کو کال کرتے ہیں۔ اگر آپ کامیابی سے لاگ ان ہوتے ہیں۔ UserLoggedIn ایونٹ کو بس تک پہنچانے سے، تمام رجسٹرڈ ہینڈلرز کو فوری طور پر مطلع کیا جاتا ہے۔ پھر Result.success صارف کے ڈیٹا کو لپیٹتا ہے۔

اگر AppException پکڑا جائے تو شائع ہو جاتا ہے۔ LoginFailed ایونٹ کو بس تک پہنچا کر تمام ناکام ہینڈلرز کو مطلع کرتا ہے۔ پھر Result.failure غلطی کو سمیٹتا ہے۔

آپ کے استعمال کے معاملے میں، آپ نہیں جانتے کہ کتنے ہینڈلرز رجسٹرڈ ہیں۔ وہ نہیں جانتے کہ وہ کیا کر رہے ہیں۔ ایک آپریشن کرتا ہے، نتیجہ کو ڈومین ایونٹ کے طور پر شائع کرتا ہے، اور نتیجہ واپس کرتا ہے۔

کہ Result type ایک ریٹرن ویلیو ہے جو کال کرنے والے (Riverpod Notifier) ​​کو نتیجہ جاننے دیتی ہے۔ ڈومین ایونٹس کو تمام سائیڈ ایفیکٹ ہینڈلرز پر نشر کیا جاتا ہے۔ دونوں ایک ہی استعمال کے کیس کال سے منتقل ہوتے ہیں۔ یہ وہی چیز ہے جو عمارت کو صاف کرتی ہے۔

ریور پوڈ ہائبرڈ: اصلی صاف فن تعمیر

یہ وہ جگہ ہے جہاں ایک حقیقی فلٹر ایپلی کیشن میں سب کچھ اکٹھا ہوتا ہے۔

مسائل جو ہم حل کر رہے ہیں۔

فلٹر ایپس کے ساتھ دو عام مسائل ہیں جو ریور پوڈ استعمال کرتی ہیں۔

ویجیٹ میں Fat ref.listen:

// This is messy
ref.listen>(loginProvider, (previous, next) {
  next.whenData((user) {
    if (user != null) {
      secureStorage.write(key: 'token', value: user.token);
      userCache.save(user);
      context.go('/home');
      analytics.track('login_success');
    }
  });
});

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

چربی کی یاد دہانی:

// Notifier doing too much
Future login(LoginRequest request) async {
  state = const AsyncLoading();
  try {
    final user = await _loginUseCase.execute(request);
    await _secureStorage.write(key: 'token', value: user.token);
    await _userCache.save(user);
    _navigationService.navigateTo('/home');
    _analytics.track('login_success');
    state = AsyncData(user);
  } catch (e, st) {
    state = AsyncError(e, st);
  }
}

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

صاف قوانین

حل کو دیکھنے سے پہلے، واضح طور پر درج ذیل اصول مرتب کریں:

استعمال کیس ڈومین کے نتائج کا مالک ہے۔ اطلاع دہندگان کی اپنی UI حالت۔ وجیٹس کسی چیز کے مالک نہیں ہیں۔

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

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

AsyncNotifier کو سمجھنا

یاد دہانی بنانے سے پہلے، آئیے سمجھتے ہیں کہ کیا ہے۔ AsyncNotifier یہ کیا ہے اور یہ کیسے کام کرتا ہے۔

AsyncNotifier ایک ریور پوڈ 2.0 کلاس خاص طور پر غیر مطابقت پذیر ریاستوں کے لئے ڈیزائن کیا گیا ہے۔ یہ رکھتا ہے AsyncValueایک مہربند قسم ہے جو تین میں سے ایک ہو سکتی ہے:

AsyncData اس کا مطلب ہے کہ آپریشن کامیاب رہا اور ڈیٹا دستیاب ہے۔ AsyncLoading یعنی کام جاری ہے۔ AsyncError اس کا مطلب ہے کہ آپریشن ناکام ہو گیا۔

توسیع کرتے وقت AsyncNotifierتم ہو build ایک طریقہ لکھیں جو ابتدائی حالت کو لوٹائے اور ایک طریقہ لکھیں جو اسے تبدیل کرے۔ state جیسا کہ غیر مطابقت پذیر آپریشنز آگے بڑھ رہے ہیں۔

کوڈ جنریشن کا استعمال کرتے ہوئے @riverpodکلاس کی تشریح کریں اور اسے چلائیں۔ flutter pub run build_runner build. جنریٹر خود بخود فراہم کنندہ اور تمام بوائلر پلیٹ تیار کرتا ہے۔ آپ پوری توجہ منطق پر مرکوز رکھتے ہیں۔

کوڈ جنریشن کے لیے مکمل سیٹ اپ مندرجہ ذیل ہے:

# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5

dev_dependencies:
  riverpod_generator: ^2.4.0
  build_runner: ^2.4.9

پتلی یاد دہانی

// login_provider.dart
part 'login_provider.g.dart';

@riverpod
class LoginNotifier extends _$LoginNotifier {

  @override
  AsyncValue build() {
    return const AsyncData(null);
  }

  Future login(LoginRequest request) async {
    state = const AsyncLoading();

    final result = await ref.read(loginUseCaseProvider).execute(request);

    result.fold(
      onSuccess: (user) => state = AsyncData(user),
      onFailure: (error) => state = AsyncError(error, StackTrace.current),
    );
  }
}

آئیے اس سطر کے ذریعے چلتے ہیں۔

part 'login_provider.g.dart' ڈارٹ کو مطلع کرتا ہے کہ تیار کردہ فائل اس لائبریری کا حصہ ہے۔ کہ @riverpod تبصرے اور _$LoginNotifier بیس کلاس تیار کردہ فائل سے آتی ہے۔

build() یہ شروع کرنے کا طریقہ ہے۔ اس وقت چلتا ہے جب فراہم کنندہ کو پہلی بار پڑھا جاتا ہے۔ یہ واپس آتا ہے AsyncData(null)یعنی، ابتدائی حالت ایک کامیاب ریاست ہے جس میں ابھی تک کوئی صارف نہیں ہے۔ یہ درست ہے کیونکہ میں نے کبھی لاگ ان کرنے کی کوشش نہیں کی۔

اندر loginپہلی چیز جو ہم کریں گے وہ سیٹ اپ ہے۔ state = const AsyncLoading(). یہ اس فراہم کنندہ کو دیکھنے والے کسی بھی ویجٹ کو فوری طور پر مطلع کرتا ہے کہ آپریشن جاری ہے۔ UI لوڈنگ انڈیکیٹر دکھا سکتا ہے۔

پھر اپنے استعمال کے کیس کو کال کریں اور await نتیجے میں. استعمال کا معاملہ ہے۔ Resultیہ ایک ایسی قسم ہے جو یا تو کامیابی کی قدر رکھتی ہے یا ناکامی کی قدر، لیکن دونوں نہیں۔ ہم کال کرتے ہیں fold یہ ہر کیس پر کارروائی کے لیے استعمال ہوتا ہے۔

پر onSuccess شاخ، ہم نے مقرر کیا state = AsyncData(user). یہ UI کو بتاتا ہے کہ آپریشن کامیاب رہا اور رینڈر کرنے کے لیے صارف کا ڈیٹا موجود ہے۔

پر onFailure شاخ، ہم نے مقرر کیا state = AsyncError(error, StackTrace.current). یہ UI کو بتاتا ہے کہ کچھ غلط ہو گیا ہے لہذا یہ ایک مناسب خرابی کی حالت کو ظاہر کر سکتا ہے۔

یہ مکمل یاد دہانی ہے۔ یہ بالکل ایک کام کرتا ہے: استعمال کے معاملے کے نتائج UI حالت میں ظاہر ہوتے ہیں۔

یہاں کوئی ٹوکن ذخیرہ نہیں ہے۔ کوئی نیویگیشن، کیشنگ یا اینالیٹکس نہیں ہے۔ اس سب کا پہلے ہی خیال رکھا گیا ہے۔ جس لمحے استعمال کا کیس کہا جاتا ہے۔ _eventBus.publish(UserLoggedIn(...)) اندر executeتمام رجسٹرڈ ہینڈلرز کو خود بخود عمل میں لایا جاتا ہے۔ تب تک result جب ہم اس نوٹیفائر پر واپس آتے ہیں، تو تمام ضمنی اثرات پہلے ہی مکمل ہو چکے ہیں۔ اطلاع دہندگان کو صرف اپنے UI کو اپ ڈیٹ کرنے کی ضرورت ہے۔

یہ سب سے صاف علیحدگی ممکن ہے۔ استعمال کیس ڈومین کے نتائج کا مالک ہے۔ نوٹیفائر رینڈرنگ اسٹیٹ کے مالک ہیں۔ ہر ایک کی بالکل ایک ذمہ داری ہے۔

ویجیٹ

class LoginPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final loginState = ref.watch(loginNotifierProvider);

    return Scaffold(
      body: loginState.when(
        data: (_) => const LoginForm(),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, _) => ErrorView(message: error.toString()),
      ),
    );
  }
}

class LoginForm extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        ElevatedButton(
          onPressed: () {
            ref.read(loginNotifierProvider.notifier).login(
              LoginRequest(email: 'user@example.com', password: 'secret'),
            );
          },
          child: const Text('Login'),
        ),
      ],
    );
  }
}

ref.watch(loginNotifierProvider) اس ویجیٹ کو نوٹیفائر کی حالت کے ساتھ رجسٹر کرتا ہے۔ ہر بار state نوٹیفائر کے اندر تبدیلیاں؛ build اسے دوبارہ بلایا جائے گا اور ویجیٹ کو دوبارہ پیش کیا جائے گا۔

loginState.when یہاں ہر کیس کو ہینڈل کرنے کا طریقہ ہے: AsyncValue. جب ریاست میں AsyncDataلاگ ان فارم رینڈر کریں۔ اس وقت AsyncLoadingلوڈنگ انڈیکیٹر پیش کرتا ہے۔ اس وقت AsyncErrorغلطی کا منظر پیش کرتا ہے۔

ویجیٹ ٹوکن، نیویگیشن، یا کیشنگ کے بارے میں کچھ نہیں جانتا ہے۔ یہ وہی پیش کرتا ہے جو ریاست اسے پیش کرنے کو کہتی ہے۔ بس۔

ساخت جڑ کنکشن

ریور پوڈ فراہم کنندہ کے اندر ایپ کے آغاز پر تمام ہینڈلر رجسٹریشن ایک بار ہوتی ہے۔

@riverpod
DomainEventBus eventBus(EventBusRef ref) {
  final bus = DomainEventBus();

  bus.register(
    TokenHandler(ref.read(secureStorageProvider)),
  );
  bus.register(
    UserCacheHandler(ref.read(userCacheProvider)),
  );
  bus.register(
    NavigationHandler(ref.read(navigationServiceProvider)),
  );
  bus.register(
    AnalyticsHandler(ref.read(analyticsServiceProvider)),
  );

  bus.register(
    AnalyticsFailureHandler(ref.read(analyticsServiceProvider)),
  );

  return bus;
}

eventBus فراہم کنندہ جو اسے تخلیق کرتا ہے۔ DomainEventBus یہ تمام ہینڈلرز کو اس وقت رجسٹر کرتا ہے جب اسے پہلی بار پڑھا جاتا ہے۔ چونکہ ریور پوڈ فراہم کنندہ بطور ڈیفالٹ سست ہے اور اسے پہلی بار بننے کے بعد کیش کیا جاتا ہے، یہ ایک بار چلتا ہے اور بس پورے ایپ سیشن تک برقرار رہتی ہے۔

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

کہ LoginUseCase یہ اس ایونٹ بس کو اپنے فراہم کنندہ کے ذریعے انحصار کے طور پر حاصل کرتا ہے۔

@riverpod
LoginUseCase loginUseCase(LoginUseCaseRef ref) {
  return LoginUseCase(
    repository: ref.read(authRepositoryProvider),
    eventBus: ref.read(eventBusProvider),
  );
}

آپ کے استعمال کے کیس کو ایونٹ بس سے جوڑنے کے لیے یہ واحد جگہ ہے۔ نوٹیفائر صرف استعمال کے معاملات کے لیے سنتے ہیں۔ ویجیٹ صرف نوٹیفائر کی حیثیت حاصل کرتا ہے۔ ہر پرت صرف اس کے نیچے والی پرت کے بارے میں جانتی ہے اور دوسری تہوں کے بارے میں کچھ بھی نہیں۔

لاگ ان میں ایک نیا ضمنی اثر شامل کرنے کا مطلب ہے ایک نئی ہینڈلر کلاس بنانا اور شامل کرنا۔ bus.register ساخت کی جڑ کی لکیر۔ نوٹیفائر، کیس لاجک استعمال کریں، ویجٹ، اور تمام موجودہ ہینڈلرز پوری طرح برقرار ہیں۔

آبزرور آرکیٹیکچر ٹیسٹنگ

اس فن تعمیر کے سب سے اہم فوائد میں سے ایک یہ ہے کہ یہ جانچ کے مسائل کو کس طرح واضح طور پر الگ کرتا ہے۔ ہر درجے کی اپنی فوکسڈ ٹیسٹنگ کوریج ہوتی ہے۔

کیس ٹیسٹنگ کا استعمال کریں۔

void main() {
  group('LoginUseCase', () {
    late LoginUseCase useCase;
    late MockAuthRepository mockRepository;
    late MockDomainEventBus mockEventBus;

    setUp(() {
      mockRepository = MockAuthRepository();
      mockEventBus = MockDomainEventBus();
      useCase = LoginUseCase(
        repository: mockRepository,
        eventBus: mockEventBus,
      );
    });

    test('publishes UserLoggedIn event on success', () async {
      final user = UserDto(id: '1', token: 'token123');
      when(() => mockRepository.login(any())).thenAnswer((_) async => user);

      await useCase.execute(LoginRequest(email: 'a@b.com', password: '123'));

      verify(() => mockEventBus.publish(any())).called(1);
    });

    test('publishes LoginFailed event on error', () async {
      when(() => mockRepository.login(any()))
          .thenThrow(AppException.unauthorized(message: 'Invalid credentials'));

      await useCase.execute(LoginRequest(email: 'a@b.com', password: 'wrong'));

      verify(() => mockEventBus.publish(any())).called(1);
    });
  });
}

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

ہر ہینڈلر کی جانچ کریں۔

void main() {
  group('TokenHandler', () {
    late TokenHandler handler;
    late MockSecureStorageService mockStorage;

    setUp(() {
      mockStorage = MockSecureStorageService();
      handler = TokenHandler(mockStorage);
    });

    test('writes token to secure storage on UserLoggedIn', () {
      final event = UserLoggedIn(
        user: UserDto(id: '1', token: 'abc123'),
        occurredAt: DateTime.now(),
        eventId: 'event-1',
      );

      handler.handle(event);

      verify(
        () => mockStorage.write(key: 'auth_token', value: 'abc123'),
      ).called(1);
    });
  });
}

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

نوٹیفائر ٹیسٹ

void main() {
  group('LoginNotifier', () {
    test('transitions from loading to data on success', () async {
      final mockUseCase = MockLoginUseCase();
      final user = UserDto(id: '1', token: 'token123');

      when(() => mockUseCase.execute(any()))
          .thenAnswer((_) async => Result.success(user));

      final container = ProviderContainer(overrides: [
        loginUseCaseProvider.overrideWithValue(mockUseCase),
      ]);

      final notifier = container.read(loginNotifierProvider.notifier);

      await notifier.login(LoginRequest(email: 'a@b.com', password: '123'));

      expect(
        container.read(loginNotifierProvider),
        isA>(),
      );
    });

    test('transitions from loading to error on failure', () async {
      final mockUseCase = MockLoginUseCase();
      final error = AppException.unauthorized(message: 'Invalid credentials');

      when(() => mockUseCase.execute(any()))
          .thenAnswer((_) async => Result.failure(error));

      final container = ProviderContainer(overrides: [
        loginUseCaseProvider.overrideWithValue(mockUseCase),
      ]);

      final notifier = container.read(loginNotifierProvider.notifier);

      await notifier.login(LoginRequest(email: 'a@b.com', password: 'wrong'));

      expect(
        container.read(loginNotifierProvider),
        isA(),
      );
    });
  });
}

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

مبصر پیٹرن کا استعمال کب کریں۔

مندرجہ ذیل صورتوں میں آبزرور کا استعمال کریں:

  • ایک واقعہ کو متعدد آزاد ردعمل کو متحرک کرنا چاہیے۔

  • میں ایونٹ کے ماخذ میں ترمیم کیے بغیر ردعمل کو شامل کرنا یا ہٹانا چاہتا ہوں۔

  • ضمنی اثرات کو کاروباری منطق سے الگ کیا جانا چاہئے۔

  • ہر ردعمل کو آزادانہ طور پر جانچنے کے قابل ہونا چاہئے۔

  • نظام کے متعدد حصوں کو ایک ہی حالت کی تبدیلی پر ردعمل ظاہر کرنا چاہیے۔

  • ہم ایسی خصوصیات بنا رہے ہیں جن کے ضمنی اثرات وقت کے ساتھ ساتھ بڑھ رہے ہیں۔

کب استعمال نہ کریں۔

مندرجہ ذیل صورتوں میں آبزرور کا استعمال نہ کریں:

  • صرف ایک صارف ہے اور مزید صارفین کی کوئی حقیقت پسندانہ توقع نہیں ہے۔

  • پروڈیوسرز اور صارفین کے درمیان تعلق سادہ اور سیدھا ہے۔

  • پیٹرن بغیر کسی معنی خیز فائدہ کے ساختی اوور ہیڈ کو جوڑتا ہے۔

  • اسٹریمز، ChangeNotifier یا Riverpod کی اندرونی ردعمل پہلے سے ہی قدرتی طور پر مسئلے کو حل کر رہی ہے۔

  • ضمنی اثرات کا سخت ترتیب دینا ضروری ہے اور فین آؤٹ اس بات کی ضمانت دینا مشکل بنا دیتا ہے۔

نتیجہ

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

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

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

اس کے بعد ہم نے پیٹرن کو ایونٹ سے چلنے والے فن تعمیر پر لاگو کیا جہاں واقعات ناقابل تغیر ڈومین حقائق بن جاتے ہیں اور سسٹم کو پروڈیوسرز اور صارفین کے درمیان براہ راست جوڑے کے بغیر بنایا جاتا ہے۔

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

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

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

مبارک کوڈنگ!

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