مفاهیم ویجت، RenderObjects و عنصر در فلاتر — راهنمای پیشرفته
آیا تاکنون کنجکاو بودهاید که فلاتر چگونه با دریافت ویجتها آنها را به پیکسلهای واقعی روی صفحه تبدیل میکند؟ درک طرز کار داخلی یک فناوری باعث میشود که به عنوان یک توسعهدهنده خبره تغییر زیادی در سطح حرفهای شما حاصل شود. زمانی که بدانید چه مواردی پاسخگو هستند و کدام ها نیستند به روشی راحتتر میتوانید طرحبندیهای سفارشی و جلوههای خاص را ایجاد کنید. دانستن این موضوعات همچنین باعث صرفهجویی در وقت و تلاش شما برای کدنویسی میشود. در این نوشته به بررسی مفاهیم ویجت، شیئ رندر و عنصر در فلاتر (Flutter) میپردازیم.
هدف این نوشته معرفی دنیای ماورای سطح ظاهری فلاتر است. در این مقاله جنبههای مختلف فلاتر را بررسی کرده و با طرز کار عملی آن آشنا میشویم.
سرآغاز
شما احتمالاً با طرز استفاده از StatelessWidget و StatefulWidget آشنا هستید. اما این ویجتها خود متشکل از ویجتهای دیگر هستند. طرحبندی ویجتها و رندر کردن آنها همه جا رخ میدهد.
پیشنهاد میکنیم که IDE محبوب خود را باز کنید و همراه با مطالعه این مقاله، ساختار کد واقعی را نیز عملاً بررسی کنید تا در این مسیر تفهیم بیشتری حاصل شود. در نرمافزار Intellij میتوانید با زدن دوباره دکمه Shift و وارد کردن نام یک کلاس آن را پیدا کنید.
شفافیت
برای این که با مفاهیم پایهای طرز کار فلاتر آشنا شویم، نگاهی به ویجت Opacity (+) خواهیم داشت و آن را مورد بررسی قرار میدهیم. از آنجا که این ویجت کاملاً ساده است، نمونه خوبی برای بررسی مفاهیم مورد نظر ما محسوب میشود.
این ویجت تنها یک فرزند میپذیرد. از این رو میتوانید هر ویجتی را درون یک Opacity قرار دهید و روش نمایش آن را تغییر دهید. علاوه بر فرزند، تنها یک پارامتر به نام opacity وجود دارد که مقداری بین 0.0 تا 1.0 میگیرد و میزان شفافیت ویجت حامل را مشخص میسازد.
ویجت
ویجت Opacity یک SingleChildRenderObjectWidget است. سلسله مراتب بسط کلاس به صورت زیر است:
Opacity → SingleChildRenderObjectWidget → RenderObjectWidget → Widget
به طور عکس ویجت StatelessWidget و StatefulWidget به صورت زیر هستند:
StatelessWidget/StatefulWidget → Widget
تفاوت در این واقعیت است که Stateless/StatefulWidget تنها متشکل از ویجتهای دیگر هستند در حالی که ویجت Opacity در عمل شیوه ترسیم ویجت را تغییر میدهد. اما در صورتی که به هر کدام از این کلاسها نگاه کنید، هیچ کد مرتبط با روش نمایش شفافیت را نخواهید یافت.
دلیل این امر آن است که یک ویجت تنها اطلاعات پیکربندی را نگهداری میکند. در این حالت ویجت شفافیت تنها مقدار شفافیت را نگهداری میکند. به همین دلیل است که میتوانیم هر بار که تابع build فراخوانی میشود، ویجت جدیدی بسازیم. چون ساخت ویجتها پرهزینه نیست. آنها صرفاً کانتینرهایی برای اطلاعات محسوب میشوند.
رندر کردن
سؤالی که اینک پیش میآید این است که پس رندر کردن در کجا اتفاق میافتد؟ پاسخ این است که رندر کردن درون RenderObjects رخ میدهد. همان طور که احتمالاً از نامش حدس میزنید، RenderObject مسئول چند کار است که شامل رندر کردن نیز میشود.
ویجت شفافیت RenderObjects را با این متدها ایجاد و بهروزرسانی میکند:
1@override
2RenderOpacity createRenderObject(BuildContext context) => new RenderOpacity(opacity: opacity);
3
4@override
5void updateRenderObject(BuildContext context, RenderOpacity renderObject) {
6 renderObject.opacity = opacity;
7}
RenderOpacity
ویجت Opacity یا شفافیت اندازه خود را طوری تغییر میدهد که برابر با اندازه فرزندش باشد. در واقع این ویجت همه جنبههای فرزند را به جز نمایش تقلید میکند. این ویجت پیش از ترسیم فرزندش یک مقدار شفافیت به آن اضافه میکند. در این حالت، Opacity باید همه متدها را پیادهسازی کند و از فرزندش بخواهید که کار واقعیاش را انجام دهد.
RenderOpacity بسطی از RenderProxyBox است که با چند کلاس دیگر ترکیب شده است. اینها در واقع پیادهسازی آن متدها هستند و محاسبه واقعی را تا زمان عملکرد فرزند به تأخیر میاندازند.
1double get opacity => _opacity;
2double _opacity;
3set opacity(double value) {
4 _opacity = value;
5 markNeedsPaint();
6}
در کد فوق بخش زیادی از تأییدها/بهینهسازیها را حذف کردهایم. در واقع کد اصلی به صورت زیر است:
1 RenderOpacity({
2 double opacity = 1.0,
3 bool alwaysIncludeSemantics = false,
4 RenderBox child,
5 }) : assert(opacity != null),
6 assert(opacity >= 0.0 && opacity <= 1.0),
7 assert(alwaysIncludeSemantics != null),
8 _opacity = opacity,
9 _alwaysIncludeSemantics = alwaysIncludeSemantics,
10 _alpha = _getAlphaFromOpacity(opacity),
11 super(child);
12
13 @override
14 bool get alwaysNeedsCompositing => child != null && (_alpha != 0 && _alpha != 255);
15
16 int _alpha;
فیلدها عموماً یک getter را برای دریافت متغیرهای خصوصی افشا میکنند. همچنین یک setter که علاوه بر تعیین فیلد، ()markNeedsPaint یا ()markNeedsLayout را نیز فراخوانی میکند. همان طور که از نام آن برمیآید، به سیستم اعلام میکند که «من تغییر یافتهام، لطفاً من را مجدد ترسیم کن.»
درون RenderOpacity متد زیر را نیز مشاهده میکنیم:
1@override
2void paint(PaintingContext context, Offset offset) {
3 context.pushOpacity(offset, _alpha, super.paint);
4}
در این مورد نیز مقدار زیادی از تأییدها و بهینهسازی را حذف کردهایم و کد اصلی به صورت زیر است:
1 double get opacity => _opacity;
2 double _opacity;
3 set opacity(double value) {
4 assert(value != null);
5 assert(value >= 0.0 && value <= 1.0);
6 if (_opacity == value)
7 return;
8 final bool didNeedCompositing = alwaysNeedsCompositing;
9 final bool wasVisible = _alpha != 0;
10 _opacity = value;
11 _alpha = _getAlphaFromOpacity(_opacity);
12 if (didNeedCompositing != alwaysNeedsCompositing)
13 markNeedsCompositingBitsUpdate();
14 markNeedsPaint();
15 if (wasVisible != (_alpha != 0))
16 markNeedsSemanticsUpdate();
17 }
در واقع PaintingContext یک بوم زیبا محسوب میشود. روی این بوم زیبا متدی به نام pushOpacity وجود دارد. این همان خطی است که عملاً شفافیت را پیادهسازی میکند.
جمعبندی ویجت
- ویجت Opacity یک StatelessWidget یا یک StatefulWidget نیست؛ بلکه یک SingleChildRenderObjectWidget است.
- Widget تنها اطلاعاتی در مورد این که از کدام رندر کننده باید استفاده کند را نگهداری میکند.
- در حالتی که Opacity حفظ شود، میزان شفافیت دوباره نمایش مییابد.
- RenderOpacity که بسطی از RenderProxyBox است عمل واقعی طرحبندی/رندر کردن را انجام میدهد.
- از آنجا که ویجت Opacity تا حدودی زیادی شبیه به فرزندش عمل میکند، هر متدی که به فرزند فراخوان بدهد را تقلید میکند.
- این ویجت متد paint را override کرده و pushOpacity را فراخوانی میکند که شفافیت مطلوب را به ویجت میافزاید.
نکات پایانی مربوط به ویجتهای فلاتر
به یاد داشته باشید که ویجت تنها یک پیکربندی است و RenderObject تنها به مدیریت طرحبندی/ رندر کردن و مواردی از این دست میپردازد.
در فلاتر ما همواره ویجتها را بازسازی میکنیم. زمانی که متدهای ()build فراخوانی شوند میتوانید یک دسته از ویجتها را ایجاد کنید. این متد build هر بار که چیزی تغییر یابد فراخوانی میشود. برای نمونه زمانی که انیمیشنی اتفاق میافتد، متد build به طور مکرر فراخوانی میشود. این بدان معنی است که شما نمیتوانید کل زیردرخت را هر بار کلاً بازسازی کنید. به جای آن میتوانید آن را بهروزرسانی کنید.
امکان دریافت اندازه یا موقعیت یک صفحه از یک ویجت وجود دارد زیرا ویجت مانند یک طرح اولیه است و عملاً روی صفحه قرار ندارد. ویجت صرفاً توصیفی از متغیرهایی است که شیئ رندر زیربنایی از آنها باید استفاده کند.
مقدمهای بر «عنصر» (Element) در فلاتر
عنصر به یک ویجت محکم در یک درخت بزرگ گفته میشود. در واقع در عنصر اتفاقاتی به شرح زیر روی میدهند:
اولین باری که یک ویجت ایجاد میشود، به صورت یک Element تولید میشود. سپس این عنصر درون درخت قرار میگیرد. اگر متعاقباً ویجت تغییر پیدا کند با ویجت قبلی مقایسه میشود و عنصر بر همین اساس بهروزرسانی میشوند. نکته مهم این است که عنصر بازسازی نمیشود و تنها بهروزرسانی میشود.
عناصر بخشی اصلی از فریمورک مرکزی محسوب میشوند و بدیهی است که نکات زیادی در مورد آنها وجود دارد که باید اشاره کرد؛ اما در این بخش تا این حد از اطلاعات کفایت میکند.
عنصر در مثال قبلی Opacity در کجا ایجاد میشود؟
در ادامه برای افراد کنجکاو توضیح کوتاهی ارائه کردهایم. برای پاسخ به سؤال فوق باید اشاره کنیم که در واقع آن را ساخته است.
1@override
2SingleChildRenderObjectElement createElement() => new SingleChildRenderObjectElement(this);
و SingleChildRenderObjectElement تنها یک Element است که یک فرزند منفرد دارد.
عنصر اقدام به ساخت RenderObject میکند؛ اما آیا در مورد مثال ما ویجت Opacity اقدام به ساخت RenderObject خود کرده است؟
این کار صرفاً به منظور راحتی API صوت گرفته است؛ اما در پارهای موارد ویجت به یک RenderObject و نه یک Element سفارشی نیاز دارد. RenderObject در واقع از سوی Element ایجاد میشود. در ادامه میتوانید آن را ملاحظه کنید:
1SingleChildRenderObjectElement(SingleChildRenderObjectWidget widget) : super(widget);
عنصر SingleChildRenderObjectElement یک ارجاع به RenderObjectWidget میگیرد که خود متدهایی برای ساخت یک RenderObject دارد.
متد mount جایی است که عنصر در درخت عناصر درج میشود و همه کارها همین جا در کلاس RenderObjectElement صورت میپذیرد:
1@override
2void mount(Element parent, dynamic newSlot) {
3 super.mount(parent, newSlot);
4 _renderObject = widget.createRenderObject(this);
5 attachRenderObject(newSlot);
6 _dirty = false;
7}
این عنصر تنها یک بار و زمانی که mount میشود از ویجت میخواهد که renderobject-ی را که میخواهد استفاده کنید در اختیار وی قرار دهد تا آن را ذخیره کند.
سخن پایانی
بدین ترتیب به پایان این مقاله میرسیم. در این نوشته به توضیح طرز کار درونی ویجت شفافیت پرداختیم. هدف ما این بوده است که این نوشته مقدمهای برای ورود به دنیای عمیقتر ویجتهای فلاتر برای شما باشد. موضوعات زیاد دیگری وجود دارند که باید مورد بررسی قرار گیرند؛ اما امیدواریم این مقدمه کوتاه انگیزه خوبی برای پیمودن این مسیر در اختیار شما قرار داده باشد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- گوگل فلاتر (Flutter) از صفر تا صد — ساخت اپلیکیشن به کمک ویجت
- مجموعه آموزشهای طراحی و توسعه پروژه های وب
- مفاهیم مقدماتی فلاتر (Flutter) — به زبان ساده
- استفاده از SQLite در فلاتر (Flutter) — به زبان ساده
==