‌از سیر تا پیازِ BLoc

سلام دوستان. امیدوارم که حالتون خوب باشه. من علی حسین پور هستم و امروز میخوام با همدیگه Bloc رو یاد بگیریم.

اول از همه چیز بگم که از هم اکنون میتوانید ویدیوهای مربوط به آموزش برنامه نویسی مخصوصا فلاتر و دارت رو در کانال یوتوب من دنبال کنید. لینکشم میزارم همین پایین :

https://www.youtube.com/c/FlutterStan

توی این مقاله میخوام هر چیزی که نیازه که درباره Bloc بدونین رو بهتون بگم. پس همراه من بیاید.???

واسه اینکه به درک خوبی از Bloc برسیم چندتا چیز رو باید از قبلش بلد باشیم به همین خاطر من این مقاله رو به ۳ تا بخش مهم و اصلی تقسیم کردم.

بخش اول معرفی اجمالی Stream هاست.

بخش دوم معرفی خود الگوی BLoC هست.

بخش سوم و نهایی معرفی و نحوه استفاده از پکیج Bloc هست.(flutter_bloc).

تو هر بخش یه سری مثال و نمونه کد نشونتون میدم و سعی میکنم خیلی باحوصله و یواش جلو برم تا بتونم این Bloc که خیلیا یه غول ازش ساختن رو براتون آسونش کنم. ??



بخش اول معرفی اجمالی Stream :

با یه مثال ساده واسه توضیح Stream شروع میکنم. شاید خیلی هاتون نوارهای نقاله توی کارخونه ها رو دیده باشین که روش محصولات رو قرار میدن و اون محصول روی اون نوار حرکت میکنه تا یه جایی یه نفر دیگه اون رو از رو نوار برداره.

نوار نقاله
نوار نقاله


خب حالا ممکنه بگید این چه ربطی به Stream ها داره ؟؟ ????

ببینید نوار نقاله همیشه در حال حرکته و اینجور کار میکنه که یه نفر ابتدای اون نوار جسمی رو روش قرار میده و چون این نوار در حال حرکته این جسم روی این نوار حرکت میکنه تا یه جایی که یه نفر دیگه به این نوار داره نگاه میکنه و هر وقت جسمی اوومد سمتش اون رو از رو نوار برمیداره.

این عمل دقیقا توی Stream ها هم اتفاق میوفته. Stream به معنی یک جریان هست و دقیقا مثله این نوار نقاله هست که ما میتونیم توی این جریان داده ای رو قرار بدیم و جای دیگه به این جریان گوش کنیم و هر وقت داده ای اوومد خبر دار شیم و از اون داده استفاده کنیم.

حالا که با مفهوم کلیه Stream آشنا شدیم چجور میتونیم توی کدمون ازش استفاده کنیم ؟؟??

من یه سوال مطرح میکنم و سعی میکنیم با هم دیگه حلش کنیم.

سوال اینه که فرض کنید من میخوام توی یک متد یه حلقه داشته باشم که از ۱ تا ۱۰ میشماره و هر عدد رو بعد از ۲ ثانیه از توی متد به بیرون میفرسته بدون اینکه اجرای متد متوقف شه (return).

خب نکته اول اینه که چون میخوام یه جای کد ۲ ثانیه منتظر وایسم و هیچ کاری نکنم نیازه که از Future ها استفاده کنم و اگه کمی با زبان دارت‌ آشنا باشید میدونید که واسه استفاده از Future ها نیازه که متد ما async بشه حالا async چی هست ؟؟

در مدل برنامه نویسی sync(همگام) در هر لحظه از زمان فقط یه عملیات میتونه انجام بشه. ینی اجرای کدها از خط اول شروع میشه و به ترتیب و یک ‌به ‌یک خط‌های برنامه اجرا میشه . اگر توی این حین، برنامه به یک متد برسه اجرای برنامه متوقف میشه تا اون متد اجرا بشه، اجرای اون متد از خط اول شروع میشه و تا زمانی که خط‌به‌خط اون متد اجرا نشه و نتیجه رو برنگردونه هیچ بخش دیگه‌ای از برنامه اجرا نمیشه. مثلا فرض کنید قراره ما یه دیتایی از سمت سرور بگیریم که چند ثانیه هم گرفتن اون داده و parse کردنش طول میکشه حالا اگه این کار به صورت کامل sync انجام بشه اون زمانی که برنامه داره دیتا رو از سرور میگیره چون کار زمان بری هست باعث میشه برنامه اونجا وایسه تا جواب از سمت سرور برگرده که اینجوری باعث میشه ui برنامه قفل شه و هیچ کاری نتونیم بکنیم. اما حالا فرض کنید زمانی که دارین دیتا رو از سمت سرور میگیرین میخواین اون زمانی که منتظر جواب هستین به کاربر یه loading نمایش بدین این کار با روش async قابل انجامه.

در مدل برنامه نویسی async(ناهمگام) در هر لحظه از زمان میتونه چندین عملیات انجام بشه.

فرض کنید شما یک تابع نوشتین... در حین اینکه اون تابع داره اجرا میشه بخش‌های دیگه‌ای از برنامه هم در حال اجرا شدن هستن، بعد از این که تابع اجرا شد و نتیجه رو برگردوند از اون نتیجه، توی بخش‌های دیگه‌ی برنامه در صورت لزوم استفاده میشه. الان یه مثال ساده میزنم تا بهتر متوجه بشین.

فرض کنین شما میرین رستوران و مثلا پیتزا سفارش میدین =)) ... در حین اینکه پیتزای شما داره آماده میشه گارسون میره سر میز‌های دیگه و سفارش بقیه‌ی افراد رو میگیره. به این نوع عملکرد میگن async .

حالا فرض کنین گارسون سفارش شما رو میگیره و تا وقتی که پیتزایی که شما سفارش دادین رو به دستتون نرسونه سراغ بقیه ی مشتری‌ها نمیره، این میشه sync.

به زبون برنامه نویسی خودمون پیتزا رو ریکوئست در نظر بگیرین، آشپزخونه‌ی رستوران رو سرور و مشتری‌هارو کلاینت. گارسون هم میشه راه ارتباطی بین کلاینت و سرور (API). حالا شما فرض کنین کد ما sync باشه... واقعا چی میشه ؟

فکر کنم با این مثال اهمیت برنامه‌نویسی async دیگه مشخص شد!

خب حالا برگردیم به سوال اصلیه خودمون. من اول یه کد مینویسم و اون رو با هم تحلیل میکنیم :

Future<int> getNumberFromMethod () async {
  for (int i = 0 ; i < 10 ; i++){
    await Future.delayed(const Duration(seconds: 2));
    return i;
  }
}

خب توی کد بالا متد رو async کردم اول و اوومدم توی بدنه یه حلقه گذاشتم که ۱۰ بار بچرخه و بعد اوومدم ۲ ثانیه هم صبر کردم و اون مقدار رو return کردم به نظرتون این کد درسته ؟؟ ???

قطعا نه چون وقتی به return i میرسه عدد ۱ رو به بیرون میفرسته ولی دیگه اون متد هم متوقف میشه چون ما return کردیم. ??? پس چکار کنم که این درست شه و بدون توقف متد هر ۱۰ تا عدد رو برگردونه ؟

بله نیاز به Stream داریم تا بیاد یه جریان واسه ما درست کنه و این ۱۰ تا عدد رو واسه ما توی این جریان بفرسته.??

خب همچنان متد ما باید async باشه ولی به جای اینکه Future برگردونه باید Stream برگردونه و واسه اینکه ما بتونیم از قابلیتی استفاده کنیم که بدون توقف متد یه داده رو به ما برگردونه به جای کلمه کلیدی async باید از async* استفاده کنیم. async* دقیقا همین کارو میکنه که توی زمان اجرا میگه این متد بعد از برگشت دادن یه داده هنوز ادامه داره و متوقف نمیشه. حالا با این اطلاعاتی که گفتم یه کد دیگه مینویسم.??

Stream<int> getNumberFromMethod() async* {
  for (int i = 0; i < 10; i++) {
    await Future.delayed(const Duration(seconds: 2));
    yield i;
  }
}

این کد الان کاری که دقیقا میخوایم رو انجام میده. کلمه کلیدی yield دقیقا میگه که این مقدار i رو برگردون ولی حواست باشه که اجرای متد متوقف نمیشه و ادامه داره.

خب ما الان یه کدی نوشتیم که یه جریانی از int ها واسه ما باز میکنه و ما داده هامون رو توش میفرستیم ولی چجور بهش گوش بدیم ؟؟

هر Stream یه متد داره به اسم listen() که ما میتونیم روی اون جریان داده شنود کنیم و هر وقت داده ای به دست ما رسید متوجه بشیم.

getNumberFromMethod().listen(
  (event) {
    print(event);
  },
);

متد getNumberFromMethod چون یه Stream به ما برمیگردونه پس میتونیم از متد listen() روش استفاده کنیم و داده ای که توی این جریان به دست ما میرسه رو بفهمیم چیه.

خب تا الان با Stream و متد listern() توی استریم ها آشنا شدیم یه دوتا چیز دیگه مونده که اونا هم الان با هم یاد میگیریم .

اولین مورد StreamController هست. شما فرض کنید میخواید یه استریم داشته باشید که هر وقت که خواستید بتونید یه داده ای رو روی اون استریم بفرستید. توی این مورد میتونید از StreamController استفاده کنید این StreamController به شما یه استریم برمیگردونه و همچنین یه سری متد که میتونید با استفاده از اون متد ها به اون استریم داده اضافه کنید یا شنود کنید و در کل کنترل داشته باشید روی اون استریم. با یه سری کد این StreamController رو توضیح میدم.

StreamController<int> streamController = StreamController();

این نحوه ساخت یک StreamController هست. نکته خیلی خیلی مهمی که وجود داره وقتی شما یک StreamController میسازید حتما باید یه جایی اون استریم رو ببندید(close) چون اگه این کار رو نکنید توی برنامتون memory leak خواهید داشت. بهترین جا واسه بستن این استریم ها توی متد dispose ویجت کلاس هاست.(Widget class)

@override
void dispose() {
  streamController.close();
  super.dispose();
}

خب ما الان میخوایم روی این استریم یه داده ای رو بفرستیم این کار به ۲ صورت میتونه انجام بشه :

روش اول : استفاده از متد add به صورت مستقیم

روش دوم : استفاده از sink و صدا زدن متد add روی sink

بین این دو روش هیچ تفاوتی وجود نداره. اول کدهاشونو میبینیم بعد تحلیلشون میکنیم.

streamController.sink.add(4);
streamController.add(7);

هر دوی این ها یه داده روی استریم قرار میدن بدون هیچ تفاوتی اما یه نکته وجود داره.

فرض کنید شما یه کلاس دارید که یه متد توی این کلاس هست که فقط و فقط وظیفه داره روی استریم یه داده بزاره ینی قرار نیست بتونه کار دیگه ای با اون استریم انجام بده (مثلا اون رو شنود کنه و .... فقط میتونه داده بزاره).اگه من خود StreamController رو پاس بدم به اون کلاس اون وقت اون متد میتونه کنترل کامل روی اون استریم داشته باشه در صورتی که ما میخوایم دسترسی اون متد محدود باشه به گذاشتن داده، واسه همین StreamController یه قابلیتی داره به اسم sink که این در واقع یه ورودی واسه استریم هست ینی ما با sink فقط میتونیم داده روی استریم قرار بدیم همین و بس واسه همین کافیه به جای فرستادن کل StreamController به اون کلاس StreamController.sink رو به اون کلاس بفرستیم. اینجوری اون متد توی کلاس فقط میتونه روی استریم داده بزاره چون ما کل StreamController رو واسش نفرستادیم فقط sink اون StreamController رو واسش فرستادیم.

پس هیچ تفاوتی بین عملکرد اون دو خط کد نیس فقط ما با استفاده از sink میتونیم دسترسی رو به استریم محدود کنیم و فقط بتونیم داده بزاریم روی استریم(sink در واقع یه جور input میشه واسه استریم).

واسه شنود کردن StreamController هم کافیه به Stream اون دسترسی داشته باشیم و روش listen کنیم :

streamController.stream.listen(
  (event) {
    print(event);
  },
);

پس به صورت کلی دوتا ویژگی خیلی مهم وجود داره توی Stream ها یکی sink که در واقع ما با استفاده از اون میتونیم داده روی Stream قرار بدیم و یکی stream که ما میتونیم روی اون شنود کنیم (listen) و داده خروجی از استریم رو بفهمیم.

خب بحث Stream ها همین جا تموم شد سعی کردم یه معرفی کلی و خلاصه دربارشون داشته باشم. استریم ها واقعا خودشون یه دنیان چون خیلی خیلی متد و ویژگی خاص دارن ولی از حوصله این مقاله خارجه واسه همین اگه میخواین توی بحث استریم ها پیشرفته تر بشین حتما دربارش تحقیق کنید.



بخش دوم معرفی خود الگوی BLoC :

کلمه BLoC مخفف Business Logic Components است. نکته اصلی درباره BLoC اینه که همه چیز در برنامه باید به عنوان جریانی(stream) از وقایع(event) نشان داده بشه: ویجت ها وقایع ها(events) را ارسال می کنند. سایر ویجت ها نسبت به اون وقایع پاسخ خواهند داد. کلا Bloc بر پایه Stream هاست واسه همین در بخش اول من Stream ها رو توضیح دادم.

خب اول کار مهمه که با دوتا کلمه جدید آشنا بشیم که خیلی خیلی مهمن: Events و States.

با چندتا مثال این دوتا کلمه رو کامل واستون توضیح میدم.

مثال اول ) فرض کنید یه برنامه دارم که وسط صفحه یه عدد نمایش میده و من میتونم اون عدد رو زیاد یا کم بکنم. پس این برنامه دوتا قابلیت داره یکی افزایش عدد و یکی کم کردن عدد. همونجور که میدونید معنی event ینی رخداد یا واقعه، الان توی این اپ دوتا رخداد میتونه اتفاق بیوفته(افزایش و کاهش عدد) پس این دوتا اتفاق میشه event های ما.

پس event شد یه سری وقایع و رخداد که توی اپ ما میتونه به وجود بیاد مثله همین افزایش و کاهش عدد.

خب حالا وقتی یکی از این event ها اتفاق افتاد ما انتظار داریم چه چیزی رو شاهدش باشیم ؟ مثلا اگه event افزایش عدد رخ داد ما باید شاهد چه چیزی باشیم ؟

درسته ما باید ببینیم که عدد روی صفحه یکی زیاد میشه. پس وقتی یه event ای اجرا میشه برنامه ما وارد یه حالت جدید میشه مثلا اگه من دارم عدد ۰ رو وسط صفحه میبینم در همین حین اگه event افزایش عدد اتفاق بیوفته برنامه وارد یک حالت جدید میشه که اون حالت نمایش عدد ۱ روی صفحه است. به این حالت میگن state.

به صورت کلی برنامه ما همیشه در یک حالت یا وضعیتی قرار داره ما نمیتونیم بگیم برنامه ما الان هیچ وضعیتی نداره در واقع همین که داره عدد ۰ رو روی صفحه نمایش میده این هم یک وضعیته واسه خودش حالا اتفاق افتادن event ها باعث میشه برنامه ما وارد یک حالت و وضعیت جدید بشه.

پس اتفاق افتادن event ها باعث تغییر state برنامه میشه.

حالا state توی این مثال ما چی میشه ؟ چون event های ما باعث میشه که اون عدد وسط صفحه تغییر کنه پس state ما
میشه اون عدد. که توی حالت اولیه(initial) برنامه اون عدد ۰ است و با اتفاق افتادن هر کدوم از اون event ها اون عدد تغییر میکنه و برنامه ما باید اون عدد جدید(حالت جدید) رو نمایش بده.

مثال دوم ) فرض کنید یه برنامه تشخیص آب و هوای ساده دارم که یک اسم شهر از من میگیره و آب و هوای اون شهر رو نشون میده.

خب وقایعی که توی این اپ میتونه رخ بده اینه که من اسم شهر رو وارد کنم و بعد روی دکمه جستجو کلیک کنم تا آب و هوای اون شهر رو نشونم بده. پس event ما میشه فشار دادن دکمه جستجو که همراه این event اسم شهر وارد شده هم میفرستیم.

حالا برنامه ما وقتی روی دکمه جستجو کلیک میکنیم چه حالت هایی میتونه داشته باشه ؟؟

۱) همیشه برنامه ما باید یه حالت ابتدایی (initial) داشته باشه ینی این حالت ابتدایی همیشه هست برای مثال قبل هم حالت ابتدایی نمایش عدد ۰ روی صفحه بود و خب منطقی هم هست چون برنامه ما از یه حالت باید شروع به کار کنه نمیشه ما حالت ابتدایی برای چیزی نداشته باشیم تو این برنامه هم حالت ابتداییه ما میشه اینکه مثلا به کاربر یه TextField نمایش بدیم تا اسم شهرش رو وارد کنه(حالت ابتدایی کاملا بستگی به خودتون داره که دوست دارین چی باشه اینجا من یه مثال ساده زدم)

۲) چون فرآیند گرفتن داده آب و هوا ممکنه زمان بر باشه پس ما توی این مدت میخوایم یه loading به کاربر نمایش بدیم پس یکی از حالت های ما میشه نمایش loading زمان گرفتن دیتا

۳) حالت بعدی این میتونه باشه که کاربر هیچ اسم شهری وارد نکرده باشه و روی دکمه جستجو کلیک کرده باشه که تو این حالت ما میخوایم به کاربر بگیم که اسم شهر نباید خالی باشه پس اینم یه حالت از برنامه میتونه باشه بعد از زدن دکمه جستجو.

۴) حالت بعدی اینه که داده آب و هوا برای اون شهر به صورت کامل گرفته شده و حالا میخوایم اون داده ها رو به کاربر نمایش بدیم

۵) و حالت نهایی هم این میتونه باشه که زمان گرفتن داده ها به یه مشکلی بخوره و نتونه داده ها رو بگیره تو این حالت ما میخوایم به کاربر یه پیغام خطا نمایش بدیم

پس این مثال ما یک event داشت و ۵ تا state

مثال سوم ) فرض کنید میخوایم صفحه سبد خرید رو شبیه سازی کنیم. ما میتونیم به این صفحه آیتم اضافه کنیم و همچنین میتونیم آیتمی از سبد خریدمون پاک کنیم.

پس وقایع ما (events) به صورت زیر میتونه باشه :

۱) اضافه کردن آیتم به سبد خرید

۲) حذف کردن آیتم از سبد خرید

اما حالت های(states) برنامه ما چی هست ؟

ما اول به یک حالت اولیه نیاز داریم.واسه سادگی فرض میکنیم که حالت اولیه ما یه لیست خالی از آیتم های سبد خرید است.

وقتی روی اضافه کردن آیتم به سبد خرید کلیک میکنیم انتظار داریم که توی صفحه سبد خرید اون آیتم رو ببینیم پس ما همیشه نیاز داریم یک لیست از آیتم ها رو به عنوان حالت (state) برنامه برگردونیم.

دیدیم که توی حالت اولیه ما یه لیست خالی برگردوندیم. توی event اضافه کردن آیتم به سبد خرید میتونیم دوباره یه لیست برگردونیم که اون آیتم توی اون لیست باشه. و همینجور اگه مثلا ما ۵ تا آیتم رو به سبد خرید اضافه کنیم یه لیست خواهیم داشت که ۵ تا آیتم توشه و این رو به عنوان حالت برنامه سبد خرید برمیگردونیم.

اگه event حذف آیتم رو زد ما باید اون آیتم رو از لیست فعلیمون پاک کنیم و لیست جدید رو به عنوان حالت برنامه برگردونیم.

پس این مثال هم ۲ تا event داشت که اضافه کردن و حذف کردن آیتم ها از سبد خرید بود و یک state داشت که اون هم یه لیستی از آیتم ها بود که در ابتدا خالی بود و با اتفاق افتادن هر event اون لیست تغییر میکرد.

خب سعی کردم توی ۳ تا مثال جدا event و state رو کامل بهتون بگم. حالا قبل از اینکه بریم یکم توی کد، میخوام یکم دیگه از مزیت های Bloc رو بهتون بگم.

الگوی Bloc علاوه بر اینکه یک استیت منیجر هست و ما میتونیم باهاش حالت های برناممون رو کنترل کنیم میتونیم ازش به عنوان یک الگوی معماری هم استفاده کنیم به این صورت که ما میتونیم کد های مربوط به بخش ظاهر رو از کدهای مربوط به بخش پردازش اطلاعات و گرفتن داده از سرور یا دیتابیس محلی یا ... جدا کنیم.

توی شکل بالا میتونیم به صورت کامل ببینیم نحوه جدا سازی بخش ظاهر از بخش دیتا رو.

بلاک در وسط برنامه قرار میگیره و ظاهر برنامه ما یا همون ویجت ها با event هایی که وجود داره با بلاک ارتباط برقرار میکنن. مثلا دکمه گرفتن آب و هوا میگه : "هی بلاک میخوام آب و هوای این شهری که کاربر وارد کرده رو بم بگی. بیا اسم شهر رو بگیر و دیتاش رو بم بده"

خب تو این حالت ما میاییم یه سری کلاس درست میکنیم واسه برقراری ارتباط با سرور یا دیتابیس محلی یا ... کلا کارهایی که مربوط به پردازش داده و ... میشه و اثری از ظاهر توشون نیس. حالا بلاک که تو قسمت قبل اسم شهر رو از ui گرفته میاد یه سری متد از اون کلاس هایی که نوشتیم صدا میزنه و میگه شما برین آب و هوای این شهر رو واسه من پیدا کنید. تا زمانی که شما چیزی پیدا نکردین من یه حالت loading فعلا به ui برمیگردونم هر وقت شما دیتایی پیدا کردید به من بگید تا من هم اون حالت جدید رو به ui بدم.

اینجوری میشه که کدهای بخش ظاهر ما از کدهای دیگمون جدا میشه و بلاک وسط این دوتا قرار میگیره تا ارتباط اون ها رو با یک سری event و state با هم برقرار کنه.

مزیت دیگه Bloc اینه که به راحتی میتونه توی معماری های دیگه هم استفاده بشه مثلا میتونیم توی معماری های Clean Architecture یا MVP یا MVVM و ... ازش استفاده کنیم مثلا توی معماری Clean توی لایه application میتونیم از Bloc استفاده کنیم یا توی MVVM میتونیم توی لایه ModelView از Bloc استفاده کنیم.

خب حالا میخوایم یکم کد بزنیم. میخوام اون مثال اول که یه عدد رو زیاد یا کم میکرد رو با Bloc بزنیم.(بدون استفاده از پکیج).

در قدم اول بیاین event ها رو بنویسیم. من چون دوتا event خیلی ساده بود از enum استفاده کردم ولی شما از کلاس هم میتونین استفاده کنید(من هر دو روش رو مینویسم ولی توی کد از enum استفاده میکنم)

روش اول با استفاده از enum :

enum CounterEvent {
  increment,
  decrement,
}

یه enum ساختم که دوتا حالت داره یا افزایش یا کاهش.

روش دوم با استفاده از کلاس ها :

abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}

یه کلاس پایه داریم به اسم CounterEvent و دوتا کلاس دیگه داریم که این دوتا کلاس باید زیرمجموعه CounterEvent باشن. چون همه event های ما آخرش باید از یه نوع باشن. الان چون IncrementEvent از CounterEvent ارث بری کرده در نهایت این کلاس نوعی از کلاس CounterEvent حساب میشه. پس به این سبک event هامون رو مینویسیم و باید دقت کنید که همگی از یه نوع باشن در نهایت.

خب الان میریم سراغ نوشتن کلاس Bloc. همون طور که اولش هم گفتم Bloc بر پایه Stream است ینی event ها و state اون باید به صورت stream باشه. توی این کلاس ما نیاز به یه فیلد counter داریم که از جنس int باشه چون قراره این state برنامه ما باشه و اون عدد رو برگردونیم. حالا میریم سراغ کد این کلاس:

class CounterBloc {
  int _counter = 0;

  final StreamController<int> _counterStateController = StreamController<int>();
  StreamSink<int> get _inCounter => _counterStateController.sink;
  Stream<int> get counter => _counterStateController.stream;

  final _counterEventController = StreamController<CounterEvent>();
  Sink<CounterEvent> get counterEventSink => _counterEventController.sink;
}

خب این کلاس ما یه فیلد داره به اسم counter که اولش هم ۰ گذاشتیمش. بعد باید بیام دوتا StreamController درست کنم یکی واسه event ها یکی واسه state ها. StreamController هم که گفته بودم واسه این خوبه که ما هر وقت خواستیم میتونیم روی stream داده بفرستیم.

⚠️⚠️ هشدار : لطفا متن زیر رو چند بار بخونید تا کامل متوجه بشید ⚠️⚠️

قبل اینکه بقیه کد رو توضیح بدم یه نکته بگم که چرا اصن event ها و State ها باید Stream باشن ؟ ببینید همونجور که قبلا گفتم Stream یه جریانه که همیشه برقراره(تا زمانی که اون رو نبستین) ما میخوایم تو برناممون هر زمان که یه واقعه ای رخ داد سریع یه حالتی نسبت به اون واقعه ببینیم تو اپمون، و این نیازمنده اینه که event های ما به صورت جریانی از event ها باشه که ما توی کلاس Bloc بتونیم به این جریان از event گوش کنیم و همچنین نیاز به یه جریان از state ها داریم که هر وقت یه event گرفتیم مختص به اون event ما یه state ای توی اون جریان بزاریم، و سمت ui به اون جریان state ها گوش کنیم و مختص به هر کدوم ظاهر خاص خودمون رو داشته باشیم. پس ما نیاز داریم توی کلاس Bloc به جریانی از event ها گوش کنیم و مختص به هر event یه state به سمت ui بفرستیم و چون سمت ui هم نیاز داره تا به این state ها گوش کنه تا به محض عوض شدن state ظاهر هم تغییر بده واسه همین نیازه که state های ما هم به صورت stream باشن. حالا بریم سراغ ادامه کد.

خب ما اول اوومدیم یه StreamController ساختیم به اسم counterStateController که این میشه استریمی از state های ما.بعد دوتا فیلد ساختیم که اولی از نوع StreamSink هست و دومی از نوع Stream. همونجور که قبلا گفتم sink واسه این هست که ما یه داده ای توی stream قرار بدیم. و ما چون میخوایم وقتی یه event ای اومد نسبت به اون event یه state توی جریان برگردونیم نیاز داریم که از sink استفاده کنیم.

فیلد بعدی که یه stream هست از state ما، واسه اینه که ui به این stream گوش کنه و نسبت با اون state ظاهر خودش رو بسازه.

حالا همینجور واسه event ها هم یک StreamController درست میکنیم. دقت کنید که ما فقط به sink نیاز داریم واسه event هامون چون قراره ما از طریق ویجت هامون یه event رو توی جریان Stream قرار بدیم واسه همین فقط sink میخواد.

حالا ادامه کد رو مینویسم :

CounterBloc() {  
    _counterEventController.stream.listen(_mapEventToState); 
}

در ادامه میام یه متد سازنده واسه کلاسم میسازم که توی این متد اومدم به اون StreamController ای که واسه event هام ساختم گوش میدم که هر وقت event ای از سمت ui وارد شد اینجا بفهمم. در نهایت توی متد listen من یه متد بهش پاس دادم به اسم mapEventToState که کد اون رو الان میزارم‌:

void _mapEventToState(CounterEvent event) {
  if (event == CounterEvent.increment)
    _counter++;
  else
    _counter--;

  _inCounter.add(_counter);
}

خب من اومدم روی StreamController ایونت هام شنود گذاشتم که هر وقت event ای وارد شد بفهمم و این متد mapEventToState رو صدا بزنم. کاری که این متد میکنه میاد event رو میگیره بعد چک میکنه اگه که event برابر با افزایش عدد بود یکی اون عدد رو زیاد میکنه و اگه کاهش بود یکی اون عدد رو کم میکنه و در نهایت با استفاده از متغیر inCounter که یه sink بود واسه state های ما میاد اون عدد رو توی جریان state ها قرار میده. حالا هر کی state ها رو شنود کنه این عدد که قرار گرفته تو جریان رو میفهمه.

در نهایت یه متد باید بنویسیم که Stream ها رو واسه ما ببنده چون بستن اون ها خیلی خیلی مهم هست.

void dispose() {
    _counterStateController.close();
    _counterEventController.close();
  }
}

این متد هم کار بستن Stream ها رو میکنه.

کد کامل کلاس CounterBloc
کد کامل کلاس CounterBloc


خب الان ما event ها و state ها و Bloc رو نوشتیم وقتشه که رابطه ی این کلاس رو با ui برقرار کنیم.

چون ما توی ui یه جریانی ازstate ها رو میگیریم میتونیم از ویجت StreamBuilder واسه گوش دادن به اون استریم state ها استفاده کنیم و هر وقت state عوض شد ui هم تغییر بدیم.

ما اول توی کلاس ui نیاز داریم تا یک نمونه از کلاس CounterBloc بسازیم:

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final _counterBloc = CounterBloc();
  .......
}

حالا توی متد build میاییم از StreamBuilder استفاده میکنیم. این ویجت چندتا فیلد مهم داره.

StreamBuilder<int>(
  initialData: 0,
  stream: _counterBloc.counter,
  builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '${snapshot.data}',
          style: Theme.of(context).textTheme.display1,
        ),
      ],
    );
  },
)

اولیش initialData هست که ما بهش گفتیم ۰ باشه ینی دیتا اولیه ما ۰ هست. فیلد بعدی stream هست که از ما میخواد یه چیزی بهش بدیم از نوع stream. چون ما میخوایم به جریان state های ما گوش بده باید یه stream از state هامون بهش بدیم که counterBloc.counter دقیقا همون stream ای هست که میخوایم.(برید توی کلاس CounterBloc نگاه کنید که counter در واقع یه متد getter هست از استریمی از state هامون). فیلد مهم بعدی builder هست که یه متد باید باشه که دوتا پارامتر داره یکی context و یکی دیگه snapshot و باید توی این متد یک ویجت حتما برگردونیم. این snapshot در واقع دیتای برگشتی از همون stream هست اگه دیتایی برگرده از stream این snapshot دارای مقدار میشه ولی اگه چیزی برنگرده این snapshot مقدار نداره. توی متد builder ما یه سری ویجت برگردوندیم که مهم ترین اونها Text آخری هست که اگه نگاه به دیتای اون Text بکنید نوشته شده snapshot.data این ینی مقداری که از stream برمیگرده رو بزار جای مقدار Text و چون ما یه stream ای از اعداد برمیگردوندیم در واقع دیتای برگشتیه ما از stream میشه یه سری عدد که ما توی Text به کاربر نمایشش میدیم.

خب کار نمایش state هامون رو هم درست کردیم با استفاده از StreamBuilder حالا وقتشه که یه سری دکمه بزاریم که وقتی روشون کلیک شد یه event واسه ما صدا بزنه.

Row(
  mainAxisAlignment: MainAxisAlignment.end,
  children: <Widget>[
    FloatingActionButton(
      onPressed: () => _counterBloc.counterEventSink.add(CounterEvent.increment),
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ),
    SizedBox(width: 10),
    FloatingActionButton(
      onPressed: () => _counterBloc.counterEventSink.add(CounterEvent.decrement),
      tooltip: 'Decrement',
      child: Icon(Icons.remove),
    ),
  ],
)

خب اینجا دوتا دکمه گزاشتم که واسه ما event هامون رو صدا میزنه توی onPressed اولی گفتم که evetn افزایش عدد رو صدا بزنه. اگه یادتون باشه توی کلاس CounterBloc من یه StreamController از event هام ساختم و بعدش یه متد getter ساختم به اسم counterEventSink که وظیفش این بود توی جریان event ها داده اضافه کنه. من هم اینجا از اون متد getter استفاده کردم و گفتم که CounterEvent.increment رو به جریان event هام اضافه کنه. و همین کارم برای دکمه دوم کردم ولی اونجا CounterEvent.decrement رو اضافه کردم به جریان event هام.

توضیح کلی : ما نیاز داریم که از طریق ظاهر برنامه یه سری event بفرستیم به کلاس Bloc و توی اون کلاس به اون جریان event ها گوش کنیم تا event ای که وارد شد، نوع اون event رو چک کنیم که چی هست بعد عملیات پردازش داده رو انجام بدیم و در نهایت یه state رو توی جریان state ها قرار بدیم تا ظاهر برنامه ما که داره به اون جریان از state ها گوش میده متوجه بشه که state برنامه تغییر کرده و ظاهر رو تغییر بده. به همین علت ما توی کلاس CounterBloc دوتا StreamController ساختیم یکی واسه event ها و یکی واسه state ها که واسه state ها هم stream درست کردیم که ظاهر برنامه بتونه بهش گوش بده و هم sink واسش درست کردیم که توی خود کلاس CounterBloc وقتی یه event ای اضافه میشه مختص به اون event یه state رو درون جریان state ها قرار بدیم و واسه StreamController ایونت هامون فقط اوومدیم sink درست کردیم که ظاهر برنامه بتونه با استفاده از اون یه event به جریان event ها اضافه کنه و توی متد سازنده کلاس هم به خود جریان event ها گوش دادیم تا هر وقت هر event ای از طرف ظاهر برنامه اضافه میشه رو بفهمیم و state مختص به اون رو برگردونیم.

اینم از خود الگوی Bloc توی بخش بعدی میریم سراغ استفاده از پکیج Bloc.



بخش سوم معرفی و نحوه استفاده از پکیج Bloc :

پکییج flutter_bloc در واقع اوومده یه سری از کار ها رو واسه ما ساده کرده. ما هنوز نیاز داریم که event ها و state هامون رو درست کنیم ولی این پکیج اوومده کار با خود Bloc رو ساده کرده دیگه نیاز نیس ما StreamController ها رو خودمون از پایه بنویسیم و یا خودمون به event ها گوش بدیم همه این کار ها رو این پکیج واسه ما ساده تر کرده.

بزارید من قبل از اینکه توضیح بدم این قسمت رو، یه دید کلی از چیزایی که قراراه گفته بشه تو این بخش رو بهتون بگم : اول درباره Cubit صحبت میکنم، بعد میام ویجتای مهمه این پکیج رو بهتون میگم، بعدش یه مثال با هم میزنیم از این پکیج و بعدش تغییرات مهمی که توی نسخه های آخر کرده رو بهتون میگم و در نهایت یه سری نکات خیلی مهم رو بهتون آموزش میدم.

اولین چیزی که قراره صحبت کنم راجع بهش Cubit هست.

اگه اشتباه نکنم توی نسخه ۵.۰.۰ این پکیج Cubit معرفی شد. Cubit در واقع یه نسخه ساده تر و سبک تر از Bloc هستش و تنها تفاوتش با Bloc از لحاظ ساختاری اینه که به جای گرفتن یه Stream از event ها ما با function(متد) باش ارتباط برقرار میکنیم.

cubit
cubit

اگه دقت کنید فلش پایینی زیرش نوشته شده functions توی شکل Bloc زیر فلش نوشته شده بود events. پس تنها تفاوت اینه که ما به جای جریانی از event ها از function ها استفاده میکنیم.

من تو این مقاله با جفتش براتون مثال هایی میزنم که خوب یادش بگیرید.

اول شروع میکنم با معرفی ویجتای مهم این پکیج.

BlocProvider :

BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

این ویجت به نظرم از مهم ترین ویجتای این پکیجه کاری که این ویجت واسه ما میکنه اینه که میاد یه نمونه از کلاس Bloc ما رو واسه ویجت هایی که توی widget tree زیر اون BlocProvider قرار دارن فراهم میکنه و اون کلاس ها میتونن به اون Bloc دسترسی داشته باشن. به این کار dependency injection هم میگن. شما کلاس CounterBloc رو تصور کنید. فرض کنید که توی یک صفحه من ویجتای مختلفی دارم و هر ویجت هم به صورت جداگانه اوومدم واسش یه کلاس درست کردم و حالا فرض کنید قراره من توی همه این کلاس ها به اون نمونه CounterBloc دسترسی داشته باشم به جای اینکه بیام اون نمونه کلاس CounterBloc رو واسه همه کلاسای دیگه پاس بدم میام از BlocProvider استفاده میکنم و اون نمونه کلاس CounterBloc رو توی اون صفحه به صورت کامل قابل دسترس میکنم برای همه.

widget tree
widget tree


فرض کنید اون نقطه ای که سبز رنگه ویجت BlocProvider ما هستش که واسه ما داره یه bloc رو فراهم میکنه. الان تو این شکل اون دوتا ویجت پایینی که BlocProvider والد اون دوتا هستش به اون bloc فراهم شده، توسط BlocProvider دسترسی دارن ولی بقیه ویجت ها این امکان رو ندارن.

اما چجوری دستری دارن ؟؟ این کار با استفاده از context انجام میشه(اگه مقاله من راجع به context رو نخوندین حتما اول اون رو بخونین). هر ویجت یه context داره که اون context نمایانگره مکان اون ویجت توی widget tree هستش.

فرض کنید من یه بلاک دارم به اسم TestBloc و میخوام اون رو با استفاده از BlocProvider (که توی نقطه سبز رنگ قرار داره) برای فرزندانش فراهم کنم.

الان ویجتی که زیر اون ویجت سبز رنگ که در واقع BlocProvider ما هست قرار داره میشه فرزند BlocProvider و میتونیم با استفاده از context اون ویجت فرزند بگیم :

context.bloc<TestBloc>();
یا 
‌BlocProvider.of<TestBloc>(context);

این دوتا خط دقیقا یه کار رو انجام میدن و اونم اینه که میرن واسه ما TestBloc رو پیدا میکنن و به ما برمیگردونن.اما چجوری ؟

ما وقتی context یه ویجت رو داشته باشیم میتونیم به ویجت های پدرش دسترسی داشته باشیم. این کد ها هم دقیقا همین کار رو میکنن با استفاده از context ویجت ها رو یکی یکی رو به بالا پیمایش میکنه تا برسه به BlocProvider ای که داره یه TestBloc رو فراهم میکنه واسه ما. اگه رسید بهش که اون TestBloc رو به ما برمیگردونه اگه هم پیدا نکرد که چیزی به ما نمیده.

مزیت دیگه ای که داره اینه که BlocProvider به صورت اتوماتیک اون کلاس Bloc رو واسه ما dispose میکنه و ما نیاز نیس نگران این باشیم که خودمون دستی dispose کنیم Bloc رو.

اما چند حالت خیلی مهم وجود داره. فرض کنید میخواید یه Bloc رو به صورت سراسری استفاده کنید ینی همه صفحات به اون Bloc دسترسی داشته باشن. تو این حالت ما باید BlocProvider رو در بالاترین نقطه ی widget tree قرار بدیم تا همه بتونن بهش دسترسی داشته باشن. اگه ما BlocProvider رو بالای MaterialApp قرار بدیم در واقع این حالت سراسری رو به وجود آوردیم و همه میتونن به اون BlocProvider دسترسی داشته باشن.

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create :(context) => TestBloc(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Home(),
      ),
    );
  }
}

الان توی کد بالا من یه BlocProvider دارم که داره TestBlocرو فراهم میکنه. در واقع فیلد create توی BlocProvider واسه همین هست که ما باید یه متد بنویسیم که یه context میگیره و توی بدنه اون متد حتما یه کلاس از نوع Bloc رو return کنیم. الان اینجا من دارم TestBloc رو ایجاد میکنم تا ویجت هایی که زیر این BlocProvider قرار دارن به TestBloc دسترسی داشته باشن.الان به صورت بالا اگه BlocProvider رو بزارین، به صورت سرتاسری بهش دسترسی دارین چون اگه بخوام widget tree کد بالا رو بگم اینجوری میشه :

MyApp -> BlocProvider -> MaterialApp -> Home

و اگه دقت کنید BlocProvider بعد از MyApp که در ریشه قرار داره اوومده و ینی اینجوری همه با context بهش دسترسی دارن(حتما مقاله BuildContext ای که نوشتم رو بخونید)

این ویجت BLocProvider یه فیلد اختیاری دیگه داره به اسم lazy که به صورت دیفالت برابر با false هست اما این فیلد چکار میکنه ؟ این فیلد به BLocProvider میگه هر وقت که من توی کدم برای اولین بار به اون کلاس Bloc فراهم شده توسط تو نیاز داشتم تو بیا اون کلاس Bloc رو واسم create کن ولی اگه بخوایم BLocProvider همون لحظه و در اولین فرصت اون کلاس Bloc رو واسه ما create کنه باید این فیلد رو برابر با true بزاریم.

تا اینجا واسه BlocProvider کافیه اما بعدا دوباره میاییم سر وقتش.


MultiBlocProvider :

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)

فرض کنید قراره ما چندتا کلاس Bloc رو فراهم کنیم واسه ویجت های زیرین. واسه این کار میتونیم از این ویجت استفاده کنیم. بقیه چیزهاشم مثه همون BlocProvider هست فقط مزیتش اینه که میشه چندتا Bloc رو فراهم کرد. توی فیلد providers ما میتونیم لیست BlocProvider هامون رو بهش بدیم.


BlocBuilder :

این ویجت یه جورایی مثه همون StreamBuilder است. یه فیلد اجباری داره به اسم builder که یه متد میگیره. اون متد پارامترهاش یه context هست و یه state که state فعلی هست که کلاس Bloc ما داره برمیگردونه.

توی این متد ما حتما باید یه ویجت رو return کنیم.این builder ممکنه چندین بار در طول زمان صدا زده بشه به خاطر دو علت. اولیش اینه که هر وقت state برنامه عوض شه این builder صدا زده میشه و علت دوم ممکنه خود موتور فلاتر دوباره باعث بشه که builder صدا زده بشه. واسه همین توی builder فقط و فقط باید ویجت برگردونیم. مثلا انجام یه سری کار ها مثله Navigate کردن یا نشون دادن SnackBar یا تغییر دادن مقدار یه سری داده اصلا خوب نیست و نباید انجام بشه.

یه فیلد دیگه داره که اختیاریه به اسم buildWhen که این فیلد هم یه متد میگیره که پارامتر هاش دوتا state هست اما یکی state فعلی برنامه هست و یکی state قبلیه اون state فعلی. توی این متد ما میتونیم یه سری شرط ها بزاریم که مثلا اگه فقط یه فیلد خاص از اون state ما تغییر کرده بود نسبت به state قبلی بیاد و دوباره builder رو صدا برنه و دوباره بیلد کنه ویجت های تو متد builder رو و اگه اون مقدار از state عوض نشده بود متد builder رو صدا نمیزنه. اینجوری میتونیم performance برنامه رو بهتر کنیم.

اگه کلاس Bloc ما همون جایی که داریم از BlocBuilder استفاده میکنم وجود داشته باشه میتونیم به این صورت از BlocBuilder استفاده کنیم.

BlocBuilder<BlocA, BlocAState>(
  cubit: blocA, // provide the local bloc instance
 buildWhen: (previousState, state) {     
    // return true/false to determine whether or not 
    // to rebuild the widget with state  
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

ینی فرض کنید من یه کلاس Bloc دارم به اسم CounterBloc اگه این کلاس رو از طریق BlocProvider فراهم نکرده باشم و اوومده باشم توی یک صفحه دستی از اون کلاس نمونه ساخته باشم باید توی ویجت BlocBuilder اون نمونه از کلاسم رو بهش پاس بدم که این کار توسط فیلد cubit توی BlocBuilder انجام میشه در واقع این فیلد cubit یه نمونه از کلاس Bloc رو میگیره. اما اگه با BlocProvider یه کلاس Bloc رو فراهم کرده باشم نیاز به این فیلد cubit نیست.

اگه که کلاس Bloc ما توسط BlocProvider فراهم شده باشه میتونیم اینجوری از BlocBuilder استفاده کنیم :

BlocBuilder<BlocA, BlocAState>(
  buildWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }
)

تنها فرقشون اینه که اگه ‌Bloc توسط BlocProvider فراهم شده باشه نیاز نیس ما از فیلد cubit توی BlocBuilder استفاده کنیم. ولی حتما باید نوع Bloc و state اون Bloc رو توی این علامتِ <> مشخص کنیم اگه از این روش BlocProvider استفاده میکنیم. چون با این روش خودش به صورت اتوماتیک میره میگرده واسه ما و اون کلاس Bloc رو پیدا میکنه. پس اگه Bloc شما توسط BLocProvider فراهم شده باشه نیاز نیست فیلد cubit رو مقدار دهی کنید ولی حتما باید نوع BLoc و state اون رو توی <> مشخص کنید.

BlocListener :

این ویجت یه فیلد داره به اسم listener که مثله همون builder در BlocBuilder یه متد میگیره که پارامترهاش context و state هست. متدی که اینجا وارد میکنیم واسه listener مقدار برگشتیش بر خلاف builder که Widget بود اینجا void هست ینی ما توی listener نیاز نیس ویجتی برگردونیم. این ویجت به ما تضمین میده که هر موقع که state برنامه عوض شد این listener صدا زده بشه بر خلاف builder که ممکن بود چندین بار صدا زده بشه. پس ما کارهایی مثله Navigate کردن یا نمایش پیغام یا SnackBar یا تغییر مقادیر داده ها رو میتونیم توی این listener انجام بدیم. نکته مهم اینه که متد listener زمانی که کلاس BLoc تازه ساخته میشه و InitialState رو به ما برمیگردونه صدا زده نمیشه ولی متد builder توی BlocBuilder برخلاف این عمل میکنه و حتی توی اولین state برگشتی که initialstate هست هم صدا زده میشه.

یه موقع اشتباه نکنیدا متد listener هر زمان که state عوض میشه صدا زده میشه تنها یک باز صدا زده نمیشه اونم زمانی هست که ما یه نمونه از کلاس Bloc ساختیم و اون به ما داره initialState رو برمیگردونه تو این تنها حالت صدا زده نمیشه.

یه فیلد دیگه داره به اسم listenWhen که دقیقا مثله همون buildWhen هست و میشه یه سری شرط گذاشت که بگیم کی listener صدا زده بشه کی صدا زده نشه.

این ویجت هم مثه BlocBuilder دو جور میشه صدا زد یکی بدین صورت که ما کلاس Bloc رو به فیلد cubit پاس بدیم :

BlocListener<BlocA, BlocAState>(
  cubit: blocA,
  listenWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to call listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  }
)

یکی بدین صورت که Bloc توسط BlocProvider فراهم شده باشه و ما دیگه نیاز نیس از فیلد cubit استفاده کنیم.و به جاش حتما باید نوع BLoc و state اون رو توی علامت <> مشخص کنیم.

BlocListener<BlocA, BlocAState>(
  listenWhen: (previousState, state) {
    // return true/false to determine whether or not
    // to call listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)


MultiBlocListener :

با این ویجت میشه هم زمان به چندتا Bloc گوش کرد فقط کافیه یه لیست از BlocListener رو بهش پاس بدیم.

MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocC, BlocCState>(
      listener: (context, state) {},
    ),
  ],
  child: ChildA(),
)

BlocConsumer :

فرض کنید به صورت هم زمان میخواید روی یه کلاس ‌Bloc هم از ‌BlocListener استفاده کنید هم از BlocBuilder. به جای اینکه بیایید جدا جدا از این ویجتا استفاده کنید میتونید از این ویجت استفاده کنید. که هم زمان هم listener داره هم builder. تنها کاری که این ویجت کرده اینه که listener و builder رو با هم ترکیب کرده وگرنه چیز پیچیده ای نیست.

BlocConsumer<BlocA, BlocAState>(
  listenWhen: (previous, current) {
    // return true/false to determine whether or not
    // to invoke listener with state
  },
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  buildWhen: (previous, current) {
    // return true/false to determine whether or not
    // to rebuild the widget with state
  },
  builder: (context, state) {
    // return widget here based on BlocA's state
  }


RepositoryProvider :

RepositoryProvider(
  create: (context) => RepositoryA(),
  child: ChildA(),
);

این ویجت دقیقا مثه همون BLocProvider هست ینی هر چی اونجا گفتیم واسه این هم صادقه ولی تنها فرقشون اینه که این ویجت کلاس Bloc رو فراهم نمیکنه واسه ما بلکه هر چیزی غیر از Bloc رو میشه باش فراهم کرد. مثلا فرض کنید یه کلاس دارید که توش کارهای ارتباط با سرور رو انجام میدید و میخواید از این کلاس تنها یک نمونه تو کل برنامه وجود داشته باشه میتونید با استفاده از این ویجت اون کلاس رو توی کل برنامه فراهم کنید و همه بتونن بهش دسترسی داشته باشن همون dependency injection میشه. پس با این ویجت ما میتونیم کلاس های دیگمون که از نوع Bloc نیستن رو توی برنامه تزریق کنیم و بقیه فرزندان بتونن ازش استفاده کنن.


MultiRepositoryProvider :

MultiRepositoryProvider(
  providers: [
    RepositoryProvider<RepositoryA>(
      create: (context) => RepositoryA(),
    ),
    RepositoryProvider<RepositoryB>(
      create: (context) => RepositoryB(),
    ),
    RepositoryProvider<RepositoryC>(
      create: (context) => RepositoryC(),
    ),
  ],
  child: ChildA(),
)

این ویجت هم واسه ما میتونه همزمان چند نمونه از کلاس ها رو فراهم کنه توی برنامه. تنها فرقش همینه که یه لیست از providers ها میگیره از ما.


خب این بود ویجتای مهم پکیج Bloc حالا میخوام یه مثال ساده بنویسم با استفاده از این پکیج.

مثالی که میزنم مثال معروف CounterApp هست.

خب همونجور که قبلا گفتم ما باید event ها و state هامون رو به صورت جدا بازم بنویسیم.

الان event های من میشه :

enum CounterEvent {
  increment,
  decrement,
}

و state این برنامه هم یک عدد int هست.(در بخش قبل صحبت شد که چرا int هست)

الان کلاس CounterBloc رو مینویسم :

class CounterBloc extends Bloc<CounterEvent, int>{
  CounterBloc() : super(0);

  @override
  Stream<int> mapEventToState(CounterEvent event) async*{
    if (event == CounterEvent.increment){
      yield state + 1 ;
    }else {
      yield state - 1;
    }
  }
}

خب نکته اول اینکه باید از Bloc ارث بری کنیم و حتما توی علامت <> باید نوع event و نوع state خودمون رو مشخص کنیم. که الان event ما میشه CounterEvent و state ما میشه int.

بعد باید متد سازنده کلاس رو بنویسیم و حتما اون بخش super هم بنویسیم. چیزی که به عنوان ورودی super میدم میشه initialState ما که ما چون state مون از نوع int هست واسه initial ما 0 رو در نظر گرفتیم. پس چیزی که توی super میزاریم میشه initialState ما.

متد بعدی که حتما باید override بشه متد mapEventToState هست. این متد یه event میگیره و یه جریان از state ها برمیگردونه و چون این متد داره Stream برمیگردونه باید از async* استفاده کنیم.

توی این متد چک کردم اگه event من افزایش عدد بود نوشتم yield state +1. با کلمه کلیدی yield که قبلا آشنا شدیم ولی state میشه همون state فعلیه ما. ما داریم میگم state فعلی رو یکی زیاد کن و برش گردون و واسه کم کردن هم همینجور. همیشه توی کلاس هایی که از Bloc یا Cubit ارث بری کردن ما میتونیم همه جای اون کلاس به state فعلی دسترسی داشته باشیم با نوشتن کلمه state.

نکته مهم این متد این بود که این متد از جنس Stream هست و state ها توی اون yield میشن نه return چون ما نیاز داریم همش روی event ها گوش کنیم که به محض اضافه شدن یه event یه state رو برگردونیم واسه همین نباید return کنیم توی این متد چون اگه return کنیم کلا این متد بسته میشه و دیگه به event ها گوش نمیده.

خب این شد کلاس Bloc. حالا میریم سراغ ظاهر.

class Home extends StatefulWidget {
  @override
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  final CounterBloc counterBloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child :BlocBuilder<CounterBloc,int>(
          cubit: counterBloc,
          builder: (context, int state){
            return Text(state.toString());
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            onPressed: () => counterBloc.add(CounterEvent.increment),
            tooltip: 'Increment',
            child: Icon(Icons.add),
          ),
          SizedBox(width: 10),
          FloatingActionButton(
            onPressed: () => counterBloc.add(CounterEvent.decrement),
            tooltip: 'Decrement',
            child: Icon(Icons.remove),
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    counterBloc.close();
    super.dispose();
  }
}

خب توی کلاس Home من اول یه نمونه از CounterBloc ساختم. بعد توی متد build توی بخش body اوومدم از BlocBuilder استفاده کردم و چون کلاس Bloc من توسط BlocProvider فراهم نشده و خودم دستی اون رو به صورت محلی ساختم میام توی BlocBuilder فیلد cubit رو مقدار دهی میکنم و اون نمونه کلاس Bloc ام رو بهش میدم. و توی builder هم یه Text اوومدم return کردم و مقدارش رو اون state برگشتی گذاشتم.(دقت کنید که نوع state ما int بود) .

واسه اضافه کردن event هم توی onPressed اوومدم گفتم‌:

counterBloc.add(CounterEvent.increment)

این متد add اجازه میده به من که یه event رو صدا بزنم. وقتی این event رو صدا میزنم اتفاقی که میوفته اینه که متد mapEventToState کلاس CounterBloc به صورت اتوماتیک صدا زده میشه و اونجا طبق event دریافتی یه state برمیگردونه و باعث میشه که state تغییر کنه و چون state تغییر کرده متد builder دوباره صدا زده میشه و اون ویجت Text دوباره ساخته میشه و مقدار جدید state رو نمایش میده.

همین مثال رو میشه با Cubit هم نوشت. همونجور که گفتم Cubit نیاز به جریانی از event ها نداره بلکه با function کار میکنه. پس ما دیگه نیازی به CounterEvent و mapEventToState نداریم.

class CounterCubit extends Cubit<int> {
  CounterBloc() : super(0);

  void increment() => emit(state + 1);

  void decrement() => emit(state - 1);
}

این میشه کلاس CounterCubit ما. اولین فرقش اینه که به جای ارث بری از Bloc از Cubit ارث بری کردیم و چون event هم نداریم دیگه نیاز نیس تو علامت <> event رو مشخص کنیم کافیه فقط نوع state رو مشخص کنیم.

فرق بعدی اینه که دیگه متد mapEventToState رو نداریم و باید event های خودمون رو به صورت یه سری متد بنویسیم.

مثلا ما event اضافه کردن رو به صورت یه متد نوشتیم و تفاوت دیگه هم اینه که به جای yield از emit استفاده میکنیم و نیازی به async* نیست چون متد ما دیگه Stream برنمیگردونه.

پس تفاوت مهمش این بود که ما باید event هامون رو به صورت یه سری متد بنویسیم و حتما واسه تغییر state هامون از emit استفاده کنیم.

تفاوت آخر هم توی نحوه صدا زدن event هاس. توی روش اول ما باید event رو توی متد add میفرستادیم ولی با این روش دیگه نیازی به add هم نیست و مستقیما متد های خودمون رو صدا میزنیم :

Row(
  mainAxisAlignment: MainAxisAlignment.end,
  children: <Widget>[
    FloatingActionButton(
      onPressed: () => counterBloc.increment(),
      tooltip: 'Increment',
      child: Icon(Icons.add),
    ),
    SizedBox(width: 10),
    FloatingActionButton(
      onPressed: () => counterBloc.decrement(),
      tooltip: 'Decrement',
      child: Icon(Icons.remove),
    ),
  ],
)

یادمون نره اگه کلاس ‌Bloc رو خودمون دستی داریم میسازیم(نه با استفاده از BlocProvider) حتما حتما اون کلاس Bloc رو close کنیم تا مشکلات memory leak واسمون پیش نیاد.

@override
void dispose() {
  counterBloc.close();
  super.dispose();
}

خب اینم از یه مثال ساده.

در آخر مقاله یه لینک از صفحه gitHub خودم میزارم که توش یه پرژه تستی با Bloc زدم واسه آموزش بیشتر به شما دوستان میتونین اون هم یه نگاهی بندازین(star هم یادتون نره ??)

زمانی که flutter_bloc نسخه ۶.۱.۰ معرفی شد یه سری تغییر خیلی مهم داشت که الان میخوام دربارش صحبت کنم.

تغییر مهمشون این بود که context.bloc که واسه پیدا کردن یه Bloc بود منسوخ شد و جاشو داد به یه سری چیز دیگه. الان شما میتونید به جای context.bloc از context.read یا context.watch یا context.select استفاده کنید.اما اینا چه فرقی با هم دارن؟؟

context.watch :

زمانی که شما یه Bloc رو با استفاده از BlocProvider فراهم میکنید میتونید توی ویجت های زیرین با استفاده از context.watch به اون ‌‌Bloc دسترسی داشته باشین اما یه نکته مهم است. در واقع context.watch میره اون Bloc رو پیدا میکنه(با استفاده از context اون کلاس Bloc رو پیدا میکنه) و حواسش هست که هر وقت state اون ‌Bloc عوض شد بیاد متد build رو از اول صدا بزنه. در واقع دیگه با این کار ما نیازی به BlocBuilder نداریم. چون ما با context.watch داریم state اون Bloc رو زیر نظر میگیریم و هر وقت state عوض شه متد build جایی که context.watch هست دوباره از اول صدا زده میشه. واسه همین نیاز به BlocBuilder نیست که بیاد state رو زیر نظر بگیره چون ما الان خودمون با context.watch داریم همین کار رو میکنیم.

فرض کنید من CounterCubit رو به صورت سرتاسری فراهم کنم توی برنامه.

void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterCubit(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: Home(),
      ),
    );
  }
}

حالا میام توی کلاس Home به جای BlocBuilder از ویجت Builder استفاده میکنم.

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Builder(
          builder: (context) {
            final counterState = context.watch<CounterCubit>().state;
            return Text(
              counterState.toString(),
              style: const TextStyle(fontSize: 40),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterCubit>().increment(),
      ),
    );
  }
}

علت اینکه از ویجت builder استفاده کردم اینه که اگه بیام context.watch رو توی خود متد build کلاس بزارم هر بار که state اون CounterCubit عوض شه چون اینجا من دارم watch میکنم باعث میشه کل متد build کلاس Home از اول صدا زده بشه چون اگه یادتون باشه گفتم context.watch هر جا باشه متد build اونجا از اول صدا زده میشه واسه همین از ویجت Builder استفاده میکنم و توی این ویجت میگم context.watch تا وقتی state اون CounterCubit عوض شد باعث بشه فقط متد builder ویجت Builder دوباره صدا زده بشه و فقط ویجت Text دوباره از اول ساخته بشه.


context.select :

دقیقا همون کار context.watch رو میکنه با این تفاوت که مثه buildWhen میتونیم بهش بگیم چه زمانی بیاد از اول rebuild کنه.

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Builder(
          builder: (context) {
            final counterState = context.select((CounterCubit state) => state.counter);
            return Text(
              counterState.toString(),
              style: const TextStyle(fontSize: 40),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterCubit>().increment(),
      ),
    );
  }
}

تنها تفاوت این کد با کد قبلی اینه که از context.select استفاده کردم. context.select یه متد به عنوان ورودی میگیره که پارامتر این متد state ما هست و باید انتخاب کنیم که روی چه بخشی از state گوش کنه که تا عوض شد بیاد و دوباره rebuild کنه الان اینجا من گفتم روی counter کلاس CounterCubit گوش کنه که هر وقت عوض شد بیاد متد builder ویجت Builder رو صدا بزنه و Text رو از اول بسازه.

پس در واقع این context.select مثه همون buildWhen هست توی BlocBuilder.

ما با استفاده از context.watch و context.select دیگه میتونیم از ‌BlocBuilder استفاده نکنیم.


context.read :

این context.read فقط میره واسه ما Bloc رو پیدا میکنه و میاره برعکس دوتای قبلی دیگه به تغییرات state گوش نمیده. در واقع همون context.bloc عه که الان منسوخ شده به جاش میتونین از context.read استفاده کنید.با این context.read شما حتما نیاز به BlocBuilder هم دارین.

توی متد هایی مثه onPressed, onTap, d و .... اگه بخواین به Bloc ای که توسط BlocProvider فراهم شده دسترسی داشته باشین حتما حتما باید از context.read استفاده کنید.



در پایان میخوام چندتا نکته خیلی مهم رو بهتون بگم که اگه رعایت نکنید قطعا به مشکل میخورید.

به کد زیر نگاه کنید :

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: Scaffold(
        body: Center(
          child: RaisedButton(
            onPressed: () => context.read<CounterBloc>().increment(),
          ),
        ),
      ),
    );
  }
} 

من اوومدم توی کلاس Test از BlocProvider استفاده کردم و CounterBloc رو توی این صفحه فراهم کردم و به عنوان child به BlocProvider یه scaffold دادم و یه RaisedBudtton ساختم که توی onPressed اوومدم با context.read اون CounterBloc رو گرفتم و عملیات افزایش عدد رو صدا زدم. خب به نظرتون این کد بدون مشکل کار میکنه ؟؟

این کد ارور میده و مشکلش هم اینه که میگه با این context که توی context.read ازش استفاده کردی من نتونستم یه BlocProvider که CounterBloc رو فراهم میکنه پیدا کنم. اما چرا ؟

ببینید widget tree این کد به این صورت است :

Test -> BlocProvider -> Scaffold -> Center -> RaisedButton

اون context ای که من ازش توی context.read دارم استفاده میکنم متعلق به Test هست حالا وقتی میگم context.read این شروع میکنه از مکان فعلیش توی widget tree میره بالا تا برسه به یه BlovProvider که CounterBloc داره فراهم میکنه اما اگه به این widget tree که من نوشتم دقت کنید BlocProvider به عنوان فرزند Test هست نه والد اون و چون context مال Test هست از ویجت Test شروع میکنه به بالا حرکت کردن و خب BlocProvider ای هم پیدا نمیکنه چون BlocProvider فرزند Test هست نه والد اون.

روش اول حل این مشکل استفاده از ویجت Builder هست :

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: Scaffold(
        body: Center(
          child: Builder(
            builder: (builderContext){
              return RaisedButton(
                onPressed: () => builderContext.read<CounterBloc>().increment(),
              );
            },
          ),
        ),
      ),
    );
  }
}

الان widget tree این جوری شده :

Test -> BlocProvider -> Scaffold -> Center -> Builder -> RaisedButton

و توی onPressed هم دارم از builderContext استفاده میکنم که context مربوط به ویجت Builder هست.

الان از Builder شروع میکنه به بالا حرکت کردن و چون الان Builder از فرزندان BlocProvidre هست پس میتونه به BlocProvider دسترسی داشته باشه و کد بدون مشکل اجرا میشه.

روش دوم :

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => CounterBloc(),
      child: Scaffold(
        body: Center(
          child: TestButton(),
        ),
      ),
    );
  }
}

class TestButton extends StatelessWidget {
  @override
  Widget build(BuildContext testButtonContext) {
    return RaisedButton(
      onPressed: () => testButtonContext.read<CounterBloc>().increment(),
    );
  }
}

اومدم ویجت RaisedButton رو توی یه کلاس Stateless جدا نوشتم.

الان widget tree این جوری شده :

Test -> BlocProvider -> Scaffold -> Center -> TestButton

و context ای که دارم استفاده میکنم ازش testButtonContext هست که متعلق به کلاس TestButton هست. الان از testButtonContext که context کلاس TestButton هست شروع میکنه میره به بالا حرکت کردن و چون الان TestButton از فرزندان BlocProvider هست این کد هم مشکلی نداره.

مشکل بعدی که واسه خیلی ها ممکنه پیش بیاد اینه که میگن چرا هر کاری میکنیم استیت برنامه عوض نمیشه ؟؟

فرض کنید کلاس زیر کلاس state ما هستش :

class LightState {
  bool isOn ;

  LightState(this.isOn);
}

این کلاس فقط یه فیلد داره که میگه چراغ روشن هست یا خاموش.

این هم کلاس Cubit من هست :

class LightCubit extends Cubit<LightState> {
  LightCubit() : super(LightState(false));

  void lightOn() {
    state.isOn = true;
    emit(state);
  }

  void lightOff() {
    state.isOn = false;
    emit(state);
  }
}

که initialState رو گزاشتم LightState(false) ینی اولش چراغ خاموش باشه و دوتا متد نوشتم یکی واسه روشن کردن که اومدم توش فیلد isOn اون state فعلیم رو true کردم و بعدش اون state رو emit کردم در lighoFF هم همین کار رو کردم ولی برعکس.

این هم کد ظاهر :

class Test extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<Test> {
  final LightCubit cubit = LightCubit();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child:BlocBuilder(
          cubit: cubit,
          builder: (context,LightState state){
            return  Text(state.isOn ? 'on' : 'off');
          },
        ),
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            onPressed: (){
              cubit.lightOn();
            },
          ),
          SizedBox(width: 10,),
          FloatingActionButton(
            backgroundColor: Colors.black,
            onPressed: (){
              cubit.lightOff();
            },
          )
        ],
      ),
    );
  }

  @override
  void dispose() {
    cubit.close();
    super.dispose();
  }
}

وقتی برنامه رو اجرا میکنیم اول مقدار initial رو به ما نشون میده که off هست بعد وقتی روی دکمه lightOn میزنیم نوشته وسط صفحه عوض میشه و مینویسه on ولی دیگه از این به بعد هر کاری کنیم استیت برنامه عوض نمیشه ولی چرا ؟

ببینید همونجور که قبل تر گفتم ویجت BlocBuilder به state حساسه ینی هر وقت state برنامه عوض بشه میاد دوباره builder رو صدا میزنه اما اینجا داره چه اتفاقی میوفته که state عوض نمیشه ؟

ما توی متد lightOn اوومدیم فقط فیلد همون state فعلی رو عوض کردیم و اون رو emit کردیم خب تا اینجاش اوکی و کار هم میکنه. اما وقتی بعد از اون میاییم lightOff رو صدا میزنیم این هم میاد فقط فیلد stateفعلی رو عوض میکنه و باز همون نمونه از state رو برمیگردونه(یادتون باشه که state ما از نوع کلاس LightState است). اگه بدونین توی دارت مقایسه دوتا آبجکت بر اساس مکان اونها توی حافظه انجام میشه حتی اگه مقدار فیلدهاش عوض شه ولی مکانش همون قبلی باشه از نظر دارت این فرقی نکرده با یه مثال ساده تر اینو توضیح میدم :

void main() {
  Test a = Test(20);
  Test b = Test(20);
  print(a == b);
  print(a == a);
  print(b == b);

}

class Test {
  int age;
  Test(age);
}

جواب ها :
false
true 
true

یه کلاس Test دارم که یه عدد میگیره و اوومدم دوتا نمونه a و b رو ازش ساختم که هر دو مقدار ۲۰ رو دارن ولی وقتی چک میکنم که a==b هست یا نه به من false برمیگردونه چون کاری به مقدارش نداره فقط چک میکنه که آیا این دوتا آدرس خونه حافظشون یکی هست یا نه و چون یکی نیس به من false میده. ولی وقتی میگم a==a به من true میده چون آدرس خونه حافظشون دقیقا یکیه. دارت به صورت دیفالت اینجوری دوتا آبجکت رو مقایسه میکنه و ما چون توی lightOff فقط اوومدیم مقدار فیلد isOn رو تغییر دادیم و باز همون نمونه از state رو برگردوندیم از نظر دارت این دوتا state کاملا شبیه به همن و چون شبیه هستن builder رو صدا نمیزنه اما چکار کنیم ؟

میتونیم از پکیج equatable استفاده کنیم که این قابلیت رو به دارت میده که آبجکت ها رو بر اساس مقادیر فیلد اونها مقایسه کنه. و همچنین توی این شرایط باید توی emit یک نمونه جدید از اون کلاس state مون برگردونیم. هم چنین میتونیم متد copyWith رو برای کلاس state مون بنویسیم و توی emit بگیم :

void lightOff() {
   emit(state.copyWith(false));
}

این copyWith کاری که میکنه اولا یه نمونه جدید از اون کلاس به ما برمیگردونه دوما میره فیلدهایی که ما تغییر دادیم رو تغییر میده و اونایی که تغییر ندادیم رو دست نخورده میزاره یه سری پکیج هستن که این متد copyWith رو برای کلاسای ما مینویسن.

پس یادتون باشه حتما یه نمونه جدید از کلاس state برگردونین.

نکته بعدی که میخوام دربارش حرف بزنم روش های دسترسی به Bloc هست. یه کمی دربارش گفتم ولی خب میخوام تو این قسمت کامل توضیحش بدم.
روش اول اینجوریه که ما مثلا تو یه صفحه خاص به یه ‌Bloc نیاز داریم کاری که میکنیم تو همون صفحه یه نمونه از اون کلاس Bloc میسازیم, ازش استفاده میکنیم و در نهایت حتما اون رو close میکنیم.به این روش میگن Local Access.

مثال این کد :

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  final CounterBloc counterBloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: BlocBuilder(
        cubit: counterBloc,
        builder: (context,int state){
          return Text(state.toString());
        },
      ),
    );
  }

  @override
  void dispose() {
    counterBloc.close();
    super.dispose();
  }
}

من توی کلاس TestPage اوومدم یه نمونه از CounterBloc ساختم ازش توی BlocBuilder استفاده کردم و درنهایت توی متد dispose اون رو close کردم.

حالا فرض کنید برنامه ما ۱۰ تا صفحه مختلف داره و میخوایم فقط توی دوتا صفحه اون، از CounterBloc استفاده کنیم. شاید بگین خب توی صفحه دوم هم یه نمونه از CounterBloc بساز دیگه.

آره این میشه ولی با این کار عملا من دوتا CounterBloc دارم و اگه توی صفحه اول عدد رو به ۵ افزایش بدم و بعد برم توی صفحه دوم چون اون یه نمونه جدا از ConterBloc هست از ۰ شروع میشه در صورتی که من میخوام عدد اون صفحه دوم هم مثله عدد همون صفحه اول باشه که واسه این کار کلا نیازه یه نمونه از CounterBloc وجود داشته باشه.

خب این مشکل ۲تا راه حل داره :

راه حل اول : استفاده از BlocProvider به صورت سرتاسری که به این روش میگن Global Access ینی ما از همه جا بهش دسترسی داریم.

راه حل دوم : استفاده از BlocProvider.value و فراهم کردن CounterBloc فقط واسه صفحه دوم. که به این روش میگن Route Access.

راه حل اول عملیه ولی زیاد مناسب نیس چون ما نمیخوایم این CounterBloc توی همه صفحات قابل دسترس باشه فقط میخوایم توی دوتا از صفحات قابل دسترس باشه.

اما راه حل دوم : ببینید BlocProvider یه فیلد داره به اسم create همون جور که قبلا گفتم ما توی این create میاییم یه نمونه از کلاس Bloc مون رو میسازیم و اون رو واسه فرزندای ‌BlocProvider توی widget tree فراهم میکنیم اما این BlocProvider یه متد سازنده دیگه داره به اسم BlocProvider.value که این دیگه فیلد create نداره بلکه یه فیلد داره به اسم value که یه کلاس Bloc که وجود داره رو میگیره و اون رو واسه فرزنداش فراهم میکنه. مثلا من یه نمونه ساختم از CounterBloc میتونم اون نمونه رو بدم به BlocProvider.value و اون نمونه از CountetBloc رو توی صفحه دوم فراهم کنم تا اون صفحه هم بتونه از همون نمونه CounterBloc استفاده کنه. به این صورت :

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  final CounterBloc counterBloc = CounterBloc();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BlocBuilder(
            cubit: counterBloc,
            builder: (context, int state) {
              return Text(state.toString());
            },
          ),
          SizedBox(
            height: 15,
          ),
          RaisedButton(
            child: Text('go to second page'),
            onPressed: () {
              Navigator.of(context).push(
                MaterialPageRoute(
                  builder: (pageContext) => BlocProvider.value(
                    value: counterBloc,
                    child: SecondPage(),
                  ),
                ),
              );
            },
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => counterBloc.increment(),
      ),
    );
  }

  @override
  void dispose() {
    counterBloc.close();
    super.dispose();
  }
}

این صفحه اول منه که توش یه CounterBloc ساختم و توی بدنه هم از BlocBuilder استفاده کردم و چون از BlocProvider برای فراهم کردم CounterBloc استفاده نکردم و خودم دستی اون رو ساختم توی همین صفحه، واسه همین نیازه که فیلد cubit موجود در BlocBuilder رو مقدار دهی کنیم. و بعد یه دکمه گذاشتم واسه افرایش عدد و توی متد dispose هم اوومدم CounterBloc رو close کردم و اما مهمترین بخش اون دکمه ای است که اوومدم باش به صفحه دومم Navigate کردم. تنها فرقی که داره زمانی که میخوام صفحه دومم رو توی MaterialPageRoute برگردونم قبلش از BlocProvider.value استفاده میکنم و اون صفحه دومم رو به عنوان child اون BlocProvider.value میدم با این کار CounterBloc توی صفحه دوم هم قابل دسترس است.


در واقع این بخش از کد توی صفحه اول مهم است :

RaisedButton(
  child: Text('go to second page'),
  onPressed: () {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (pageContext) => BlocProvider.value(
          value: counterBloc,
          child: SecondPage(),
        ),
      ),
    );
  },
)

لطفا حواستون باشه توی فیلد value در BlocProvider.value اصلا و ابدا نباید یه نمونه جدید از کلاس Bloc رو بسازید بلکه حتما باید همون نمونه ای که قبلا وجود داره و ساخته شده رو بدید بهش به طور مثال من توی کد بالا اول اوومدم یه نمونه از CounterBloc اول کلاسم ساختم و توی BlocProvider.value از اون نمونه آماده استفاده کردم نیومدم دوباره یه نمونه از کلاس CounterBloc بسازم و پاس بدم بهش. پس به این نکته حتما توجه کنید.

کد صفحه دوم :

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, int state){
            return Text(state.toString());
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        onPressed: () => context.read<CounterBloc>().increment(),
      ),
    );
  }
}

این هم صفحه دوم منه که توی FloatingActionButton اوومدم با context.read اون CounterBloc فراهم شده واسه این کلاس رو گرفتم و متد increment رو روش صدا زدم. الان هر دوتا پیج یه عدد مشترک رو نمایش میدن. فقط نکته مهم اینجاس که BlocProvider.value برخلاف BlocProvider عادی خودش به صورت اتوماتیک اون نمونه از کلاس Bloc رو واسمون close نمیکنه واسه همین خودمون دستی اون CounterBloc رو close کردیم.

و اما نکته آخر :

این نکته مربوط میشه به performance برنامتون و سعی کنید حتما این کار رو انجام بدید.

فرض کنید یه صفحه دارید که توش ۱۰ تا ویجت مختلف هست اما از بین این ۱۰ تا ویجت فقط قراراه یک ویجت state اون تغییر کنه و نسبت به event ها واکنش داشته باشه و ظاهرش عوض شه پس کاری که ما میکنیم به جای اینکه بیاییم BlocBuilder رو به عنوان اولین ویجت بزاریم و هر ۱۰ تا ویجت رو توی اون BlocBuilder بزاریم میاییم فقط اون یدونه ویجتی که قراره عوض شه رو توی BlocBuilder میزاریم. اینجوری وقتی state برنامه عوض میشه به جای اینکه دوباره ۱۰ تا ویجت رو از اول بسازه فقط یک ویجت رو از اول میسازه و performance برنامه ما بهتر میشه.

یه مثال هم میزنم براتون :

این کد پایین خووب نیست : ??

class Home extends StatelessWidget {
  final CounterCubit counterCubit = CounterCubit();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CounterCubit, int>(
      value : counterCubit,
      builder: (context, int state) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Push The Button For Change the Number'),
                const SizedBox(height: 10),
                Text(
                  state.toString(),
                  style: TextStyle(fontSize: 40),
                ),
                const SizedBox(height: 10),
                RaisedButton(onPressed: () => counterCubit.increment(), child: Text('Increment')),
                const SizedBox(height: 10),
                RaisedButton(onPressed: () => counterCubit.decrement(), child: Text('Decrement')),
              ],
            ),
          ),
        );
      },
    );
  }
}


میبینید که همه چیز توی BlocBuilder هست ولی فقط یه Text داره به state برنامه گوش میده و عوض میشه با عوض شدن state. پس این کار خوب نیست چون با عوض شدن state همه ویجتا از اول ساخته میشن.

و اما کد درست : ??

class Home extends StatelessWidget {
  final CounterCubit counterCubit = CounterCubit();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Push The Button For Change the Number'),
            const SizedBox(height: 10),
            BlocBuilder<CounterCubit, int>(
              cubit: counterCubit,
              builder: (context, int state) {
                return Text(
                  state.toString(),
                  style: TextStyle(fontSize: 40),
                );
              },
            ),
            const SizedBox(height: 10),
            RaisedButton(onPressed: () => counterCubit.increment(), child: Text('Increment')),
            const SizedBox(height: 10),
            RaisedButton(onPressed: () => counterCubit.decrement(), child: Text('Decrement')),
          ],
        ),
      ),
    );
  }
}

تو این کد من فقط اون Text ای که داره به state گوش میده رو توی BlocBuilder گذاشتم. که اینجوری هر وقت state برنامه عوض میشه فقط یه ویجت به اون تغییرات گوش میده و عوض میشه نه ۱۰ تا ویجت.


خب دوستان این آموزش هم تموم شد و امیدوارم که تونسته باشم به خوبی مفهوم Bloc رو بهتون گفته باشم. اگه سوالی داشتید زیر همین مقاله کامنت بزارین حتما در اولین فرصت سوال شما رو جواب میدم.??

این پایین هم یه لینک از یه پروژه آموزشی ساده با Bloc رو میزارم براتون که توی گیت هابم واسه شما دوستان عزیز نوشتم تا بهتر این مفهوم Bloc رو درک کنید.توی این پروژه در عین سادگی سعی کردم اکثر مفاهیم مهمی که گفتم رو توش بزارم براتون.

لینک کانال یوتوب من لطفا سر بزنید : https://www.youtube.com/c/FlutterStan


https://github.com/AliHoseinpoor/virgool_bloc_training


خدا نگه دار همتون باشه. ❤️❤️