مفاهیم پایه فلاتر : Widget -> BuildContext -> State

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

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

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

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

Scaffold.of() called with a context that does not contain a Scaffold.

خب ارور که خیلی واضحه اگه بخوایم ترجمش کنیم میگه که شما Scaffold.of رو با context ای فراخوانی کردید که اون context اصن scaffold نداره !!

جالب شد نه ؟؟ خب حالا این چیزی که گفته ینی چی ؟؟

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

Widget :

ویدجت به زبون خیلی ساده میشه اون چیزی که با توجه به پیکربندی(configuration) و ویژگی هاش توصیف میکنه، ui ما چه شکلی باشه(این تعریف ویدجت توی داکیومنت خود فلاتر هست که خیلی ساده و راحته)

توی فلاتر همه چیز ویدجته. حالا ما یه چیز توی فلاتر داریم به اسم widget tree که میشه درختی از ویدجت ها که به صورت والد و فرزند هستن. به ویدجتی که ویدجت های دیگه رو شامل بشه میگن پدر و به اون ویدجت های پایین اون میگن ویدجت فرزند.

widget tree
widget tree

عکس بالا به خوبی widget tree رو به ما نشون میده. که اگه بخوام یه مثال بزنم MyApp پدر MaterialApp هست و بالعکس MaterialApp میشه فرزند MyApp.

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


BuildContext :

کانتکست میشه اشاره گری به مکان یه ویدجت توی widget tree.

خب نکته مهم اینکه هر ویدجت context مخصوص خودش رو داره که این context مکان اون ویدجت توی درخت رو مشخص میکنه.

اگه ویدجت A یه سری فرزند داشته باشه، context این ویدجت A میشه والد context فرزندای اون ویدجت A. در واقع مثه همون widget tree هست که اینجا به جای ویدجت context داریم.

توی عکس بالا اگه هر رنگ رو نماد یه context در نظر بگیریم به ما نشون میده که مثلا MyHomePage که با رنگ قرمز نشون داده شده والد context ویدجت های زیریش هست در واقع ما میتونیم از یه فرزند با context، اجداد اون رو پیدا کنیم مثلا توی ویدجت Column ما میتونیم به اجداد اون دسترسی داشته باشیم که اجدادش میشن :
MyApp ->MaterialApp -> MyHomePage ->Scaffold -> Center

اگه توی ویدجت Text که توی این عکس در پایین ترین سطح قرار داره بنویسیم :

context.ancestorWidgetOfExactType(Scaffold);

توی این کد context در واقع context اون ویدجت Text ما هستش که با پیمایش اجداد این context، به ما نزدیک ترین Scaffold به Text رو برمیگردونه .

این نکته خیلی مهمه که ما توی یه ویدجت با استفاده از context اون میتونیم به اجدادش دسترسی داشته باشیم. چون همونجور که گفتم در واقع context یه اشاره گری هست به اینکه، یه ویدجت کجای درخت قرار داره و چون این context مکان رو مشخص میکنه ما میتوینم باهاش درخت رو پیمایش کنیم و مکان اجداد اون ویدجت رو هم بفهمیم و بتونیم از اجداد اون استفاده کنیم. بالفرض من یه Text دارم که والدش یه Scaffold هست حالا اگه من context این Text رو داشته باشم باهاش به راحتی میتونم به Scaffold هم دسترسی داشته باشم.

اگه دقت کرده باشین توی کدهاتون خیلی جا ها از متد of() استفاده کردین مثلا Scaffold.of یا Navigator.of یا BlocProvider.of و ... این of دقیقا کاری که میکنه میاد context رو میگیره و از اونجا شروع میکنه به پیمایش تا اولین Scaffold یا Navigator یا BlocProvider یا ... رو پیدا کنه و به شما برگردونه اگه هم پیدا نکرد که null برمیگردونه به شما.

در واقع وقتی من توی یه ویدجت میگم Scaffold.of و بهش context اون ویدجت جاریم هم میدم این میاد از اون context شروع میکنه پیمایش کردن درخت رو به بالا تا اولین Scaffold که والد این ویدجت جاریم هست رو پیدا کنه و به من برش گردونه تا من بتونم از خصوصیات Scaffold استفاده کنم.


state :

به مجموعه دیتا هایی که توی ویدجت نگه داری میشن و بیانگر ویژگی های رفتاری اون ویدجت هستن و ممکنه توی طول عمر اون ویدجت تغییر کنه رو میگن State.

زمانی که یه ویدجت ساخته میشه state اون ویدجت مرتبط میشه با context اون ویدجت ینی یه جوری به هم وصل میشن حتی اگه context جاش توی tree عوض شه باز هم state به اون مرتبط هست و هیچ جوره از هم جدا نمیشن. زمانی که این دوتا به هم مرتبط میشن در واقع ویدجت mounted میشه و تا قبل اینکه mounted نشه، ما نمیتونیم از setState استفاده کنیم.




خب مفاهیم پایه مون تموم شد حالا وقتشه بریم سر کار اصلی خودمون ینی جواب دادن به اون سوال اصلیمون(همون ارور context) :

نکته اولی که باید بدونین اینه که وقتی از Stateless یا Stateful ویدجت استفاده میکنین، اون context که توی متد build پاس داده میشه context همون ویدجت جاری هست که به صورت اتوماتیک توسط خود فلاتر به این ویدجت فعلی پاس داده میشه. با یه مثال توضیح میدم :

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Test',
      home: Test(),
    );
  }
}

class Test extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    // به این بخش دقت کنید
    print(context.widget);
    print(context.findAncestorWidgetOfExactType<Test>());
    //
    return const Scaffold();
  }
}

اولین print توی کد بالا میاد اسم ویدجتی که این context به اون ویدجت تعلق داره رو به ما برمیگردونه. که خروجیش الان Test هست ینی این context به ویدجت Test تعلق داره.

توی کد بالا ما یه MaterialApp داریم که توی home بهش ویدجت Test رو دادیم. توی متد build اون ویدجت Test اوومدیم توی print دوم گفتیم که با این context که داری(که مال همین ویدجت Test هست) برو واسه من اولین ویدجت Scaffold که توی اجدادت هست رو پیدا کن و اون رو برگردون و اون رو print کن. به نظرتون جوابش چیه ؟؟؟

بله null برمیگردونه چون ویدجت Scaffold ای پیدا نمیکنه. در واقع ویدجت Test ویدجت جاریه ما هست و ویدجت های والد اون میشن MaterialApp و MyApp که همینجور که میبینیم ویدجت Scaffold جزو والد ها نیست بلکه جزو فرزندهای Test هست. متد findAncestorWidgetOfExactType فقط والد ها رو پیدا میکنه و خود ویدجت جاری هم حساب نمیکنه توی والد ها واسه همین وقتی به context ویدجت Test میگیم برو توی والد هات Scaffold رو پیدا کن به ما null برمیگردونه چون ویدجت والدی به اسم Test نداره.

حالا اگه اون بخش print کد رو اینجوری تغییرش بدم :

print(context.findAncestorWidgetOfExactType<MyApp>());

این دیگه الان به من null برنمیگردونه بلکه به من یه آبجکت از MyApp برمیگردونه چون دارم به context که مال Test هست میگم برو واسه من نزدیک ترین MyApp رو پیدا کن و برگردون. حالا میاد پیمایش میکنه توی اجداد Test که میبینه Test فرزند MaterialApp هست و MaterialApp فرزند MyApp هست و چون الان MyApp رو پیدا کرده اینو واسه ما برمیگردونه.

در واقع پیمایشش اینجور شد : MaterialApp -> MyApp ینی از پایین به بالا شروع به پیمایش کرده تا اون ویدجت رو پیدا کنه.

حالا یه مثال دیگه میزنیم : (برای خوانایی بهتر عکس کد رو میزارم و دقت کنید که فقط کلاس Test رو عوض میکنیم بقیه کد دست نخورده هست)

توی این مثال اوومدم توی FloatingActionButton یه SnackBar ساختم که واسه ساختنش نیاز داره که یه Scaffold داشته باشه که این Scaffold رو با همون متد معروف of (که یه context میگرف و اجداد رو پیمایش میکرد تا یه آبجکتی که ما خواستیم رو برگردونه) پیدا میکنه.

به نظرتون با چیزایی که الان بهتون گفتم جواب این چی میشه ؟؟ آیا نشون میده ؟؟ ( جواب رو نخونین و روش با توجه به نکاتی که گفتم فک کنید)

جواب : باز هم به ارور میخوریم و این دفعه ارور معروف رو به ما میده که میگه با این context من scaffold ای پیدا نکردم که بهت برگردونم که باش Snackbar نشون بدی.

ولی چرا ارور داد ؟ جوابش باز هم همون نکتس چون این context داره به ویدجت Test اشاره میکنه و توی اجداد Test هم هیچ ویدجت Scaffold ای نیس پس نمیتونه به من یه Scaffold برگردونه پس ارور میده که من هیچ Scaffold ای پیدا نکردم.

ولی چجور میشه این رو برطرف کرد و به context اون Scaffold دسترسی داشت ؟

واسه حلش دو راه وجود داره :

راه حل ۱) استفاده از ویدجت Builder : ویدجت Builder یه پروپرتی داره به اسم builder که این پروپرتی یه متد میگیره که ورودیش یه context هست و یه ویدجت رو برمیگردونه:

‌Builder(
  builder: (context) {
    return Container();
  },
)

همونجور که بالا تو کد میبینین این متد یه context میگیره و یه ویدجت رو return میکنه. اما نکته مهم اینجاس که این context از کجا میاد ؟؟ این context میشه context همین ویدجت Builder. در واقع ما با این ویدجت به context خود ویدجت Builder دسترسی پیدا میکنیم و میتونیم حالا با این context به اجداد Builder هم دسترسی داشته باشیم. حالا اگه ما Builder رو بزاریم جزو فرزندهای Scaffold در واقع میتونیم با استفاده از context اون ‌ویدجت Builder به Scaffold دسترسی داشته باشیم.(طبق همون نکاتی که گفتم که میشه context رو پیمایش کرد و اجدادش رو به دست آورد)

در واقع اگه ما FloatingActionButton رو توی ‌Builder بزاریم چون الان والد Builder اون Scaffold هس در واقع ما میتونیم الان با context این Builder به جدش که Scaffold هست دسترسی داشته باشیم و مشکل رو حل کنیم. چون همه مشکل ما الان اینه که یه context داشته باشیم که بتونیم باش به Scaffold دسترسی داشته باشیم تا بتونیم یه SnackBar نشون بدیم:

من الان FloatingActionButton رو گزاشتم توی Builder و Builder الان فرزند Scaffold هست. حالا من توی متد ‌builder اومدم FloatingActionButton رو return کردم و توش دوباره Scaffold.of رو صدا زدم که این دفعه به درستی کار میکنه چون این context همونجور که گفتم context ویدجت Builder هست و به راحتی با پیمایش اجداد این context میتونیم به Scaffold برسیم.

حالا یکم پیچیده ترش میکنم :

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

Test -> Scaffold -> Center -> Container -> Builder -> FlatButton -> Text

من اوومدم FlatButton رو گزاشتم توی ویدجت Builder و اجداد Builder هم که توی خط بالا میتونید ببینید.

حالا توی onPressed ویدجت FlatButton اوومدم باز گفتم Scaffold.of به نظرتون این کار میکنه ؟؟؟

معلومه که کار میکنه چون همونجور که دیگه الان میدونید این ctx(برای خوانایی بهتر ورودیه متد builder که یه context میگیره رو اسمش رو کردم ctx که با اون context که توی متد build خود ویدجت هست قاطی نشه. پس الان ctx میشه context ویدجت Builder ما و context میشه برای ویدجت Test) میشه مال Builder که وقتی این ctx رو پیمایش کنیم رو به بالا(اجدادش رو پیمایش کنیم) Scaffold رو میتونیم توی اجداد Builder پیدا کنیم و ازش استفاده کنیم.

به همین راحتی مسئله حل شد.

راه حل ۲) راه حل بعدی اینه که ویدجتامون رو از هم جدا کنیم که با کد نشونش میدم که راحت تر قابل فهم باشه :


الان من اوومدم FloatingActionButton رو کلا بردم توی یه کلاس Stateless دیگه. نکته مهم اینجاس همونجور که قبلا گفتم این context که واسه متد build کلاس FloatingActionTest فرستاده میشه در واقع context خود ویدجت جاری ینی FloatingActionTest هست. اگه بخوام widget tree رو بگم اینجور میشه الان :

Test -> Scaffold -> FloatingActionTest -> FloatingActionButton

که همینجور که مشخصه Scaffold الان والد FloatingActionTest هست و ما میتونیم با پیمایش context اون ویدجت FloatingActionTest به Scaffold برسیم و SnackBar خودمون رو بدون مشکل نشون بدیم.

برای اون مثال پیچیده تر هم میتونیم به این روش عمل کنیم :

الان ترتیب ویدجت ها به این صورته که :

Test -> Scaffold -> Center -> Container -> FlatButtonTest -> FlatButton -> Text

که الان context ای که به کلاس FlatButtonTest فرستاده میشه مال خود FlatButtonTest هست و چون FlatButtonTest فرزند Container هست و Container هم فرزند Scaffold هست ما میتونیم با پیمایش اجداد context این FlatButtonTest به Scaffold برسیم و باز SnackBar رو نمایش بدیم.

خب اینم از راه حلی برای رفع این مشکل.(لازم به ذکره که یه راه حل دیگه هم موجوده که استفاده از GloblKey هست اون راه حل چون به این بحث context مربوط نبود اونو اینجا بازش نکردم ولی بدونین که با کلید هم میشه این کار رو انجام داد و به state یه ویدجت با کلید خیلی راحت میشه دسترسی داشت)

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

تا آموزش های بعد بدرود ...

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