BuildContext in Flutter

Monday 10 August 2020 ~7 min read


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

كل مافي Flutter هي عناصر (Everything in Flutter is a Widget)

نتعامل بشكل أساسيّ أثناء تطوير تطبيقات فلتر مع العناصر، أو widgets، ولهذه العناصر أنواع، سنشرح في هذا القسم النوعين الرئيسيين منهما.

دورة حياة StatelessWidget

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

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

تبدأ دورة حياتها حين أقوم باستدعائها عن طريق الـconstructur، ثم يتم استدعاء دالة ()buil مباشرة، وتظهر في مكانها على الشاشة.

//Stateless Widget Syntax

class MyWidget extends StatelessWidget {

  //constructor
  const MyWidget({    Key key,  }) : super(key: key);  
  //build method called immediately after the constructor
  
  Widget build(BuildContext context) {    return Container();
  }
}

دورة حياة StatefulWidget

Stateful Widget

تبدأ دورة حياتها بعد ما ننادي الـconstructor مباشرة بدالة ()createState. تقوم هذه الدالة بمناداة الجزء الآخر من عنصرنا، وهو الـState. كل ما بعد ذلك سيحصل بداخل هذا الكائن، وهو السبب الذي يمكّن الـStatefulWidget من إعادة بناء نفسها من الداخل ويظهر التغيير على الشاشة. نلاحظ أنّ العنصر نفسه مازال immutable، كما في StatelessWidget تماماً، ولكِن الفرق أنه يمتلك الآن State مرتبطة به!

بعد ذلك ستقوم Flutter بتغيير قيمة متغير اسمه mounted من false إلى true، وفي هذه اللحظة سيتم إنشاء context وربطه بهذه الـState، ثم تنادى دالة ()initState، والتي نستطيع إعادة الكتابة فوقها ووضع أي كود نريده أن يعمل لمرة واحدة قبل ظهور هذا العنصر على الشاشة.

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

ثم تأتي ()build وتبدأ عملية البناء الفعلية وظهور مابداخلها على الشاشة، وفي حالة أردت أن أعرف متى تتم إعادة بناء عنصري، فبعد ()build تأتي ()didUpdateWidget وكما اسمها يوحي فهي تقوم بتنبيهي متى ما حصل أي تحديث لهذا العنصر.

أخيراً،

//Stateful Widget Syntax

class MyWidget extends StatefulWidget {
  const MyWidget({
    Key key,
  }) : super(key: key);

  
  _MyWidgetState createState() => _MyWidgetState();}

class _MyWidgetState extends State<MyWidget> {

  //called after mounted is true
  initState(){
    super.initState();
    print("initial state");
  }

  
  Widget build(BuildContext context) {
    return Container();
  }
}

ماهو BuildContext

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

علينا أوّلاً أن نعرف أنّ في Flutter لا يوجد شجرة واحدة فقط، إنما يوجد 3:

  1. Widget Tree (ما نتعامل معه بشكل دائم كمطورين داخل أكوادنا)
  2. Element Tree (تربط الـWidget Tree بـRenderObject Tree وتقوم بالعمل الحقيقي بناءً على الخواص التي تم تعريفها في الـWidgets المرتبطة بها)
  3. RenderObject Tree (ما نراه من بكسلات حقيقية على الشاشة)

ملاحظة: هذه المحاضرة تشرح بشكل رائع سبب وجود 3 أشجار بدلاً من واحدة وكيف يقوم ذلك بجعل Flutter ذات أداء سريع.

وظيفة BuildContext تتضح هنا، فهو يعمل كواجهة للـElement، حيث يخبرني أين تمّ وضع عنصري في الشجرة، ومن هم آباءه parents أو ancestors.

من موقع Flutter الرسمي، BuildContext هو:

A handle to the location of a widget in the widget tree

كيف ولماذا قد أستعمله؟

حينَ أريد استدعاء معلوماتٍ معيّنة من عنصر في مكانٍ ما، فيجب أن أعرف أوّلاً أين مكاني، وهل العنصر الذي أريد مناداته يقع ضمن نطاقي (أي هل هو أحد الـancestors لعنصري) أم لا. لعمل ذلك هناك دالة داخل BuildContext تساعدني على الصعود لأعلى الشجرة والبحث عن Widget من النوع الذي أريده وجلب بضع معلومات منها.

 context.findAncestorWidgetOfExactType(MyWidget)

ومتى ما حصل أيُّ تغيير في MyWidget، فإنها ستقوم بتنبيه كل العناصر التي استعملت هذه الدالة بأنّ تغييراً قد حصل.

النوع الثالث من العناصر: InheritedWidget

ما رأيناه قَبل قليل ربّما قد لا يبدو مألوفاً للوهلة الأولى، لكنّك بالتأكيد قد استعملته من قبل، وسأترك بضع سطور برمجية يكثر استعمالها داخل Flutter لأذكرك بما أقصده:

Theme.of(context).primaryColor;
MediaQuery.of(context).size.height;
Scaffold.of(context).showSnackBar(SnackBar());

هل لاحظت أننا في كل مرة ننادي على دالة اسمها of وهذه الدالة تأخذ context كـargument لها؟ لنأخذ نظرة سريعة على مافي داخل هذه الدالة.

class MyInheritedWidget extends InheritedWidget {
  MyInheritedWidget({
    Key key,
     this.name,
     Widget child,
  }) : super(key: key, child: child);

  final String name;
  final Widget child;

  static MyInheritedWidget of(BuildContext context) {      return context.inheritFromWidgetOfExactType(MyInheritedWidget);  }  // This is a built in method which you can use to check if
  // any state has changed. If not, no reason to rebuild all the widgets
  // that rely on your state.
  
  bool updateShouldNotify(_InheritedStateContainer old) => true;
}

هكذا أقوم بكتابة InheritedWidget، وكونها ليست موضوعي هنا لن أسهب في شرحها أكثر، لكن ذكرها كان لابدّ منه حتى نشرح أين نستعمل BuildContext بكثرة. نلاحظ في السطور التي عليها علامة، أنّ هذه الدالة تقوم ببساطة باستعمال الـcontext الذي أمرره لها حتى تنادى على الدالة التي تحدثنا عنها من قبل، أي inheritFromWidgetOfExactType.

أين أحصل على الـcontext للعنصر الذي أريده؟

كثيرٌ منّا ستصيبه الحيرة حينّ يتعامل مع BuildContext للمرة الأولى، خصوصاً حينما لا تظهر النتيجة التي نريدها، وسبب ذلك أن الـWidgets التي تأتي بعد return في دالة build ليس لها نفس الـcontext الذي نلاحظ وجوده كـargument، ولتوضيح هذه النقطة جيداً، سنستعين بمثال بسيط.

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.red,        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Home(),
    );
  }
}

class Home extends StatelessWidget {
  const Home({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Theme(
        data: ThemeData(primaryColor: Colors.blue),        child: Center(
          child: Text(
            "Hello",
            style: TextStyle(color: Theme.of(context).primaryColor),
          ),
        ),
      ),
    );
  }
}

ما نريده هو أن يكون النص باللون الأزرق، لكن ما سيحصل أنه سيظهر باللون الأحمر، لماذا؟ الجواب هو أني أستعمل context يعود لـHome، ولا يوجد فوقه أي لون أزرق! الرسم التالي يوضح الفكرة ببساطة.

red text

نستنتج من هنا أنّ الـcontext الذي يكون متاحاً داخل build يعود على العنصر الأب فقط، فإن أردت أن أحصل على context يكون مكانه فوق Text مباشرة، أستطيع استعمال Widget موجودة خصيصاً لهذا الغرض، وهي ()Builder.

blue text

مشكلة في إظهار SnackBar

هذه المشكلة شائعة جداً، وحلها مماثل للحل السابق، فحين أريد استدعاء SnackBar، أستخدم دالة باسم showSnackBar موجودة داخل Scaffold، إذاً وحتى أستعملها من أي مكان أحتاج أن يكون Scaffold موجوداً في الـwidget tree! الكود التالي لن يعمل، وأظن أنك الآن تعرف السبب.

class Home extends StatelessWidget {
  const Home({Key key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: FlatButton(
          child: Text(
            "Click to show snackbar",
            style: TextStyle(color: Theme.of(context).primaryColor),
          ),
          onPressed: () {
            Scaffold.of(context).showSnackBar(
              SnackBar(
                content: Text("This is a snackbar!"),
              ),
            );
          },
        ),
      ),
    );
  }
}

الحل

الحل الأول هو استعمال Builder. اضغط على زر Flutter حتى يظهر لك الكود.

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

مصادر تعلّم إضافية