رفرفة تحت غطاء المحرك

تحية للجميع! اسمي ميخائيل زوتييف ، أعمل كمطوّر Flutter في Surf. أنا ، على الأرجح مثل غالبية المطورين الآخرين الذين يعملون مع Flutter ، والأهم من ذلك كله هو مدى سهولة إنشاء تطبيقات جميلة ومريحة بمساعدتها. يستغرق الأمر القليل من الوقت للوصول إلى تطوير Flutter. لقد عملت مؤخرًا في تطوير اللعبة ، والآن تحولت تمامًا إلى تطوير الجوّال عبر الأنظمة الأساسية على Flutter.

ما هي البساطة؟ باستخدام عشرات الأدوات الأساسية ، يمكنك إنشاء واجهات مستخدم لائقة جدًا. وبمرور الوقت ، عندما تكون الأمتعة المستخدمة لائقة تمامًا ، فمن غير المحتمل أن يؤدي نوع من المهام إلى توقفك: سواء كان تصميمًا غير عادي أو رسوم متحركة متطورة. والأكثر إثارة للاهتمام - على الأرجح يمكنك استخدامه دون التفكير في السؤال: "كيف يعمل على الإطلاق؟"

نظرًا لأن Flutter مفتوح المصدر ، فقد قررت معرفة ما تحت غطاء المحرك (على جانب Dart من القوة) ومشاركته معك.



القطعة


لقد سمعنا جميعًا العبارة من فريق تطوير إطار العمل أكثر من مرة: "كل شيء في Flutter عبارة عن أدوات . " دعونا نرى ما إذا كان الأمر كذلك بالفعل. للقيام بذلك ، ننتقل إلى فئة القطعة (فيما يلي - القطعة) ونبدأ في التعرف تدريجياً على المحتويات.

أول شيء سنقرأه في وثائق الفصل:
يصف تكوين [عنصر].

اتضح أن الأداة نفسها هي مجرد وصف لبعض العناصر (فيما يلي - العنصر).
الحاجيات هي التسلسل الهرمي للفئة المركزية في إطار Flutter. عنصر واجهة المستخدم هو وصف ثابت لجزء من واجهة المستخدم. يمكن تضخم عناصر واجهة المستخدم في عناصر ، والتي تدير شجرة التجسيد الأساسية.
للتلخيص ، عبارة "كل شيء في Flutter هو عنصر واجهة مستخدم" هو الحد الأدنى لفهم كيفية ترتيب كل شيء لاستخدام Flutter. الأداة هي الطبقة المركزية في تسلسل Flutter. في الوقت نفسه ، هناك العديد من الآليات الإضافية حوله التي تساعد الإطار على التعامل مع مهمته.

لذا ، تعلمنا بعض الحقائق الإضافية:

  • القطعة - وصف ثابت لجزء من واجهة المستخدم ؛
  • القطعة مرتبطة ببعض العرض المتقدم يسمى عنصر ؛
  • عنصر يتحكم في بعض كيانات شجرة التجسيد.

لابد أنك لاحظت شيئًا غريبًا. واجهة المستخدم والغير قابلة للربط معًا بشكل سيئ جدًا ، بل أود أن أقول إن هذه مفاهيم غير متوافقة تمامًا. ولكن سنعود إلى ذلك عندما تظهر صورة أكثر اكتمالاً لجهاز Flutter العالمي ، ولكن في الوقت الحالي ، سنستمر في التعرف على وثائق الأداة.
الحاجيات نفسها ليس لها حالة قابلة للتغيير (يجب أن تكون جميع حقولها نهائية).
إذا كنت ترغب في ربط حالة قابلة للتغيير بعنصر واجهة مستخدم ، ففكر في استخدام [StatefulWidget] ، الذي ينشئ كائن [State] (عبر [StatefulWidget.createState]) كلما تم تضخيمه في عنصر ودمجه في الشجرة.
تكمل هذه الفقرة الفقرة الأولى قليلاً: إذا احتجنا إلى تكوين قابل للتغيير ، فإننا نستخدم كيان الدولة الخاص (المشار إليه فيما يلي باسم الحالة) ، والذي يصف الحالة الحالية لهذه الأداة. ومع ذلك ، لا ترتبط الدولة بالقطعة ، ولكن بتمثيلها الأولي.
يمكن تضمين عنصر واجهة مستخدم معين في الشجرة صفرًا أو أكثر. على وجه الخصوص يمكن وضع عنصر واجهة مستخدم معين في الشجرة عدة مرات. في كل مرة يتم وضع عنصر واجهة تعامل في الشجرة ، يتم نفخه في [عنصر] ، مما يعني أن عنصر واجهة تعامل مدمج في الشجرة عدة مرات سيتم تضخيمه عدة مرات.
يمكن تضمين نفس الأداة في شجرة الأدوات عدة مرات ، أو لا يتم تضمينها على الإطلاق. ولكن في كل مرة يتم تضمين عنصر واجهة مستخدم في شجرة عنصر واجهة المستخدم ، يتم تعيين عنصر لها.

لذا ، في هذه المرحلة ، تم تقريب الأدوات ، دعنا نلخص:

  • القطعة - الطبقة المركزية من التسلسل الهرمي ؛
  • القطعة بعض التكوين.
  • القطعة - وصف ثابت لجزء من واجهة المستخدم ؛
  • يرتبط عنصر واجهة المستخدم بعنصر يتحكم في العرض بطريقة ما ؛
  • يمكن لبعض الكيانات وصف الحالة المتغيرة للأداة ، ولكنها ليست مرتبطة بالأداة ، ولكن بالعنصر الذي يمثل هذه الأداة.

جزء


من ما تعلمناه ، يطرح السؤال: "ما هذه العناصر التي تحكم كل شيء؟" افعل نفس الشيء - افتح الوثائق لفئة العنصر.
إنشاء مثيل لـ [Widget] في موقع معين في الشجرة.
العنصر هو بعض تمثيل القطعة في مكان معين في شجرة.
تصف التطبيقات المصغرة كيفية تكوين شجرة فرعية ولكن يمكن استخدام نفس الأداة لتكوين أشجار فرعية متعددة في نفس الوقت لأن الأدوات غير قابلة للتغيير. يمثل [عنصر] استخدام عنصر واجهة المستخدم لتكوين موقع محدد في الشجرة. بمرور الوقت ، يمكن أن تتغير الأداة المصاحبة لعنصر معين ، على سبيل المثال ، إذا قامت الأداة الرئيسية بإعادة إنشاء وإنشاء أداة جديدة لهذا الموقع.
يصف عنصر واجهة المستخدم تكوين جزء من واجهة المستخدم ، ولكن كما نعلم بالفعل ، يمكن استخدام نفس الأداة في أماكن مختلفة من الشجرة. سيتم تمثيل كل مكان من هذا القبيل من قبل عنصر المقابلة. ولكن بمرور الوقت ، قد تتغير الأداة المصاحبة للعنصر. هذا يعني أن العناصر أكثر عنادا وتستمر في استخدامها ، فقط تحديث اتصالاتها.

هذا قرار عقلاني إلى حد ما. كما سبق أن حددنا أعلاه ، تعد الأدوات عبارة عن تكوين غير قابل للتغيير يصف ببساطة جزءًا محددًا من الواجهة ، مما يعني أنها يجب أن تكون خفيفة للغاية. والعناصر في المنطقة التي يكون التحكم فيها أثقل بكثير ، ولكن لا يتم إعادة إنشائها دون داع.

لفهم كيفية القيام بذلك ، ضع في اعتبارك دورة حياة العنصر:

  • Widget.createElement , .
  • mount . .
  • .
  • , (, ), . runtimeType key, . , , .
  • , , , , ( deactivate).
  • , . , , (unmount), .
  • عندما تقوم بإعادة تضمين عناصر في الشجرة ، على سبيل المثال ، إذا كان العنصر أو أسلافه يحتوي على مفتاح عام ، فستتم إزالته من قائمة العناصر غير النشطة ، وسيتم استدعاء طريقة التنشيط ، وسيتم دمج الكائن المرتبط بهذا العنصر مرة أخرى في شجرة التجسيد. هذا يعني أن العنصر يجب أن يظهر على الشاشة مرة أخرى.

في إعلان الفئة ، نرى أن العنصر يطبق واجهة BuildContext. BuildContext هو شيء يتحكم في موضع عنصر واجهة المستخدم في شجرة عنصر واجهة مستخدم ، كما يلي من وثائقه. يتطابق تقريبًا مع وصف العنصر. يتم استخدام هذه الواجهة لتجنب التلاعب المباشر للعنصر ، ولكن في نفس الوقت منح الوصول إلى أساليب السياق الضرورية. على سبيل المثال ، findRenderObject ، الذي يسمح لك بالعثور على كائن شجرة التجسيد المطابق لهذا العنصر.

Renderderbject


يبقى للتعامل مع الرابط الأخير لهذا الثالوث - RenderObject . كما يوحي الاسم ، هذا هو كائن شجرة التصور. يحتوي على كائن أصل ، بالإضافة إلى حقل بيانات يستخدمه الكائن الأصل لتخزين معلومات محددة تتعلق بهذا الكائن نفسه ، على سبيل المثال ، موقعه. هذا الكائن مسؤول عن تنفيذ بروتوكولات العرض والتخطيط الأساسية.

لا يحد RenderObject من نموذج استخدام الكائنات الفرعية: قد لا يكون هناك شيء واحد أو أكثر. أيضا ، لا يقتصر نظام تحديد المواقع على: النظام الديكارتي ، الإحداثيات القطبية ، كل هذا وأكثر متاحة للاستخدام. لا توجد قيود على استخدام بروتوكولات الموقع: ضبط العرض أو الارتفاع ، تحديد الحجم ، تحديد حجم وموقع الأصل أو ، إذا لزم الأمر ، استخدام بيانات الكائن الأصل.

Flutter World Picture


دعونا نحاول بناء صورة كبيرة لكيفية عمل كل شيء معًا.

لقد أشرنا من قبل إلى أن عنصر واجهة المستخدم عبارة عن وصف ثابت ، ولكن واجهة المستخدم ليست ثابتة على الإطلاق. تتم إزالة هذا التناقض عن طريق التقسيم إلى 3 مستويات من الأشياء وتقسيم مناطق المسؤولية.

  • , .
  • , .
  • , — , .

صورة

دعونا نلقي نظرة على كيف تبدو هذه الأشجار بمثال بسيط:

صورة

في هذه الحالة ، لدينا بعض StatelessWidget ملفوفة في عنصر واجهة مستخدم Padding وتحتوي على نص بداخلها.

دعونا نضع أنفسنا مكان Flutter - لقد تم إعطاؤنا شجرة الأدوات هذه.

رفرفة: "مهلا، الحشو، ولست بحاجة عنصر بك"
حشوة: "بطبيعة الحال، عقد SingleChildRenderObjectElement"

صورة

رفرفة: "العنصر، وهنا مكانك، يستقر"
SingleChildRenderObjectElement: "يا شباب، طيب كل شيء، ولكني في حاجة RenderObject"
رفرفة: "الحشو، مثل لجذبك على الإطلاق؟ "
الحشو: "Hold it، RenderPadding"
SingleChildRenderObjectElement: "Great، get to work"

صورة

Flutter:"إذن من التالي؟" StatelessWidget، والآن يمكنك السماح للعنصر »
StatelessWidget: « هنا StatelessElement »
الرفرفة: « StatelessElement، سوف تكون في الخضوع لSingleChildRenderObjectElement، وهنا المكان، الشروع »
StatelessElement: « موافق »

صورة

الرفرفة: « لRichText والحاضر elementik، الرجاء »
لRichText يعطي MultiChildRenderObjectElement
الرفرفة: "MultiChildRenderObjectElement ، ها أنت ذا ، ابدأ"
MultiChildRenderObjectElement: "أحتاج إلى تقديم للعمل"
Flutter: "RichText ، نحن بحاجة إلى كائن تقديم"
RichText: "Here is a RenderParagraph" Flutter
:"RenderParagraph ستتلقى تعليمات RenderPadding وسيتحكم في MultiChildRenderObjectElement"
MultiChildRenderObjectElement: "الآن كل شيء على ما يرام ، أنا جاهز"

صورة

بالتأكيد ستطرح سؤالًا منطقيًا: "أين هو كائن التقديم لـ StatelessWidget ، لماذا لم يكن هناك ، قررنا أعلاه أن العناصر تربط بين التكوينات مع العرض؟ " دعونا ننتبه إلى التنفيذ الأساسي لطريقة التحميل ، التي تمت مناقشتها في هذا القسم من وصف دورة الحياة.

void mount(Element parent, dynamic newSlot) {
    assert(_debugLifecycleState == _ElementLifecycle.initial);
    assert(widget != null);
    assert(_parent == null);
    assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
    assert(slot == null);
    assert(depth == null);
    assert(!_active);
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null)
        _owner = parent.owner;
    if (widget.key is GlobalKey) {
        final GlobalKey key = widget.key;
        key._register(this);
    }
    _updateInheritance();
    assert(() {
        _debugLifecycleState = _ElementLifecycle.active;
        return true;
    }());
}

لن نرى فيه إنشاء كائن عرض. لكن العنصر يقوم بتطبيق BuildContext ، والذي يحتوي على طريقة بحث عن كائن تصور findRenderObject ، والتي ستقودنا إلى المُشغل التالي:

RenderObject get renderObject {
    RenderObject result;
    void visit(Element element) {
        assert(result == null); 
        if (element is RenderObjectElement)
            result = element.renderObject;
        else
            element.visitChildren(visit);
    }
    visit(this);
    return result;
}

في الحالة الأساسية ، لا يجوز للعنصر إنشاء كائن عرض ؛ فقط RenderObjectElement ونسله مطلوبون للقيام بذلك ، ومع ذلك ، في هذه الحالة ، يجب أن يحتوي العنصر في بعض مستويات التداخل على عنصر فرعي يحتوي على كائن عرض.

يبدو لماذا كل هذه الصعوبات. ما يصل إلى 3 أشجار ، مناطق مسؤولية مختلفة ، إلخ. الجواب بسيط للغاية - هذا هو المكان الذي يتم فيه بناء أداء Flutter. الأدوات عبارة عن تكوينات غير قابلة للتغيير ، لذلك ، غالبًا ما يتم إعادة إنشائها ، ولكن في نفس الوقت فهي خفيفة الوزن تمامًا ، والتي لا تؤثر على الأداء. لكن Flutter يحاول إعادة استخدام العناصر الثقيلة قدر الإمكان.

تأمل في مثال.

نص في منتصف الشاشة. سيبدو الرمز في هذه الحالة كالتالي:

body: Center(
    child: Text(“Hello world!”)
),

في هذه الحالة ، ستبدو شجرة الأدوات

صورة

على النحو التالي : بعد أن يقوم Flutter ببناء الأشجار الثلاثة ، نحصل على الصورة التالية:

صورة

ماذا يحدث إذا قمنا بتغيير النص الذي سنعرضه؟

صورة

لدينا الآن شجرة القطعة الجديدة. أعلاه تحدثنا عن أقصى قدر ممكن من إعادة استخدام العناصر. نلقي نظرة على طريقة فئة القطعة ، تحت اسم الحديث يمكن التحديث .

static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}

نتحقق من نوع الأداة السابقة والأخرى ، بالإضافة إلى مفاتيحها. إذا كانت هي نفسها ، فلا حاجة لتغيير العنصر.

لذا ، قبل الترقية ، العنصر الأول هو Center ، بعد الترقية ، Center أيضًا. كلاهما ليس لديهم مفاتيح ، صدفة كاملة. يمكننا تحديث ارتباط العنصر إلى عنصر واجهة مستخدم جديد.

صورة

ولكن بالإضافة إلى النوع والمفتاح ، فإن عنصر واجهة المستخدم عبارة عن وصف وتكوين ، ويمكن أن تتغير قيم المعلمات الضرورية للعرض. هذا هو السبب في أن العنصر ، بعد تحديث الرابط إلى عنصر واجهة المستخدم ، يجب أن يبدأ التحديثات على كائن العرض. في حالة المركز ، لم يتغير شيء ، ونستمر في المقارنة أكثر.

مرة أخرى ، يخبرنا النوع والمفتاح أنه من غير المنطقي إعادة إنشاء العنصر. النص من سلالة StatelessWidget ؛ ليس له كائن عرض مباشر.

صورة

انتقل إلى RichText. لم يغير عنصر واجهة المستخدم نوعه أيضًا ، ولا توجد اختلافات في المفاتيح. يقوم العنصر بتحديث ارتباطه بعنصر واجهة المستخدم الجديد.

صورة

تم تحديث الاتصال ، يبقى فقط لتحديث الخصائص. نتيجة لذلك ، سيعرض RenderParagraph قيمة النص الجديدة.

صورة

وبمجرد أن يحين الوقت لإطار الرسم التالي ، سنرى النتيجة التي نتوقعها.

بفضل هذا النوع من العمل ، يحقق Flutter مثل هذا الأداء العالي.

يصف المثال أعلاه الحالة عندما لا تتغير بنية الأداة نفسها. ولكن ماذا يحدث إذا تغير الهيكل؟ سيواصل Flutter ، بالطبع ، محاولة زيادة استخدام الكائنات الموجودة إلى أقصى حد ، كما فهمنا من وصف دورة الحياة ، ولكن سيتم إنشاء عناصر جديدة لجميع الأدوات الجديدة ، وسيتم حذف العناصر القديمة وغير الضرورية في نهاية الإطار.

دعونا نلقي نظرة على بضعة أمثلة. وللتأكد مما سبق ، نستخدم أداة Android Studio - Flutter Inspector.

@override
Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
            child: _isFirst ? first() : second(),
        ),
        floatingActionButton: FloatingActionButton(
            child: Text("Switch"),
            onPressed: () {
                setState(() {
                    _isFirst = !_isFirst;
                });
            },
        ),
    );
}

Widget first() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "test",
            style: TextStyle(fontSize: 25),
        ),
        SizedBox(
            width: 5,
        ),
        Icon(
            Icons.error,
        ),
    ],
);

Widget second() => Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
        Text(
            "one more test",
            style: TextStyle(fontSize: 25),
        ),
        Padding(
            padding: EdgeInsets.only(left: 5),
        ),
        Icon(
            Icons.error,
        ),
    ],
);

في هذه الحالة ، بالضغط على الزر ، ستتغير إحدى الأدوات. دعونا نرى ما يظهره لنا المفتش.

صورة

صورة

كما نرى ، قام Flutter بإعادة إنشاء العرض للحشو فقط ، والباقي أعيد استخدامه للتو.

ضع في اعتبارك خيارًا واحدًا آخر يتغير فيه الهيكل بطريقة أكثر عالمية - نغير مستويات التداخل.

Widget second() => Container(child: first(),);

صورة

صورة

على الرغم من حقيقة أن الشجرة لم تتغير بصريًا على الإطلاق ، فقد تم إعادة إنشاء عناصر وكائنات شجرة التقديم. حدث هذا لأن Flutter يقارن حسب المستوى (في هذه الحالة ، لا يهم أن معظم الشجرة لم تتغير) ، تم غربلة هذا الجزء في وقت مقارنة الحاوية والصف. ومع ذلك ، يمكن للمرء الخروج من هذا الوضع. هذا سوف يساعدنا GlobalKey. أضف مثل هذا المفتاح للصف.

var _key = GlobalKey(debugLabel: "testLabel");

Widget first() => Row(
    key: _key,
    …
);

صورة

صورة

بمجرد أن أخبرنا Flutter أنه يمكن إعادة استخدام الجزء ، استغل الفرصة.

استنتاج


لقد اقتربنا أكثر قليلاً من سحر Flutter ونعرف الآن أنه ليس فقط في الحاجيات.

Flutter هي آلية منسقة جيدًا ومدروسة جيدًا مع التسلسل الهرمي الخاص بها ، ومناطق المسؤولية ، والتي لا يمكنك من خلالها إنشاء تطبيقات جميلة فحسب ، بل أيضًا إنتاجية. بالطبع ، قمنا بفحص جزء صغير فقط ، وإن كان مهمًا إلى حد ما من جهازه ، لذلك سنستمر في تحليل الجوانب المختلفة للعمل الداخلي للإطار في المقالات المستقبلية.

آمل أن تكون المعلومات الواردة في هذه المقالة مفيدة في فهم كيفية عمل Flutter داخليًا ومساعدتك في العثور على حلول أنيقة ومنتجة أثناء التطوير.

شكرا للانتباه!

مصادر


Flutter
"كيف يصنع Flutter Widgets" بقلم أندرو فيتز جيبون ، مات سوليفان

All Articles