تفاصيل حول حزمة الموفر لـ Flutter

مرحبا يا هابر!

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

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

1. StatefulWidget


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

class _MyWidgetState extends State<MyWidget> {
  DateTime _time = DateTime.now();  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text(_time.toString()),
      onPressed: () {
        setState(() => _time = DateTime.now());
      },
    );
  }
}

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

2. InheritedWidget


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

3. ScopedModel


ScopedModel هي حزمة تم إنشاؤها في عام 2017 بواسطة Brian Egan ، مما يجعلها سهلة الاستخدام InheritedWidgetلتخزين حالة التطبيق. تحتاج أولاً إلى إنشاء كائن حالة يرث من الطراز ، ثم notifyListeners()تسميته عندما تتغير خصائصه. الوضع يذكرنا بتنفيذ واجهة PropertyChangeListener في جافا.

class MyModel extends Model {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

لتقديم كائن الحالة الخاص بنا ، نلف هذا الكائن في عنصر واجهة مستخدم ScopedModelفي جذر تطبيقنا:

ScopedModel<MyModel>(
  model: MyModel(),
  child: MyApp(...)
)

الآن سيتمكن أي MyModelعنصر واجهة مستخدم سليل من الوصول باستخدام عنصر واجهة مستخدم ScopedModelDescendant . يتم تمرير نسخة النموذج إلى المعلمة builder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModelDescendant<MyModel>(
      builder: (context, child, model) => Text(model.foo),
    );
  }
}

سيتمكن أي عنصر واجهة مستخدم سليل من تحديث النموذج ، مما سيؤدي تلقائيًا إلى إعادة إنشاء أي ScopedModelDescendants(بشرط أن يستدعي نموذجنا بشكل صحيح notifyListeners()):

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = ScopedModel.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

ScopedModelاكتسبت شعبية في Flutter كأداة لإدارة الدولة ، ولكن استخدامها يقتصر على توفير الكائنات التي ترث الصف Modelواستخدام هذا النمط من الإخطار بالتغييرات.

4. BLoC


في مؤتمر Google I / O '18 ، تم تقديم نمط مكون منطق الأعمال (BLoC) ، والذي يعمل كأداة أخرى لسحب الحالة من الأدوات. فئات BLoC هي مكونات طويلة العمر غير واجهة المستخدم التي تحافظ على الحالة وتكشفها كتيارات ومستقبلات. باستخدام منطق الدولة والأعمال خارج واجهة المستخدم ، يمكنك تنفيذ الأداة على أنها بسيطة StatelessWidgetواستخدام StreamBuilder لإعادة البناء التلقائي. ونتيجة لذلك ، تصبح الأداة "غبية" ويصبح اختبارها أسهل.

مثال لفئة BLoC:

class MyBloc {
  final _controller = StreamController<MyType>();  Stream<MyType> get stream => _controller.stream;
  StreamSink<MyType> get sink => _controller.sink;
  
  myMethod() {
    //  
    sink.add(foo);
  }  dispose() {
    _controller.close();
  }
}
 ,   BLoC:
@override
Widget build(BuildContext context) {
 return StreamBuilder<MyType>(
  stream: myBloc.stream,
  builder: (context, asyncSnapshot) {
    //  
 });
}

المشكلة في نمط BLoC هي أنه ليس من الواضح كيفية إنشاء وتدمير كائنات BLoC. كيف تم إنشاء المثيل myBlocفي المثال أعلاه؟ كيف نتصل dispose()للتخلص منه؟ تتطلب التدفقات الاستخدام StreamController، والذي يجب أن يكون closedبمجرد أن يصبح غير ضروري - يتم ذلك لمنع تسرب الذاكرة. (لا يوجد شيء مثل المدمر الطبقي في Dart ؛ فقط الطبقة Stateفي StatefulWidgetلديها طريقة dispose()). بالإضافة إلى ذلك ، ليس من الواضح كيفية مشاركة BLoC بين أدوات متعددة. غالبًا ما يكون من الصعب على المطورين إتقان BLoC. هناك العديد من الحزم التي تحاول تبسيط ذلك.

5. المزود


Providerحزمة مكتوبة في عام 2018 من قبل Remy Rusle ، تشبه ScopedModel، ولكن لا تقتصر وظائفها على ، توفر فئة فرعية من النموذج. هذا أيضًا عبارة عن غلاف يختتم InheritedWidget، ولكن يمكن للمزود توفير أي كائنات دولة ، بما في ذلك BLoC ، والجداول ، والعقود الآجلة وغيرها. نظرًا لأن المزود بسيط ومرن للغاية ، أعلنت Google في مؤتمر Google I / O '19 أنه سيكون في المستقبل Providerالحزمة المفضلة لإدارة الحالة. بالطبع ، يُسمح أيضًا بالحزم الأخرى ، ولكن إذا كانت لديك أي شكوك ، توصي Google بالتوقف عند Provider.

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

  • الموفر الأساسي
  • ChangeNotifierProvider
  • StreamProvider
  • فوتوريبروفيدر
  • ValueListenableProvider
  • MultiProvider
  • Proxyprovider

التركيب


لاستخدامه Provider، قم أولاً بإضافة التبعية إلى ملفنا pubspec.yaml:

provider: ^3.0.0

ثم نقوم باستيراد الحزمة Providerحيث تكون هناك حاجة إليها:

import 'package:provider/provider.dart';

موفر القاعدة

إنشاء الأساس Provider في جذر تطبيقنا ؛ سيحتوي هذا على مثال لنموذجنا:

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

تقوم المعلمة builderبإنشاء مثيل MyModel. إذا كنت ترغب في تمرير نسخة موجودة إليه ، استخدم المُنشئ هنا Provider.value.

ثم يمكنك استهلاك هذا النموذج من النموذج في أي مكان MyApp، باستخدام الأداة Consumer:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<MyModel>(
      builder: (context, value, child) => Text(value.foo),
    );
  }
}

في المثال أعلاه ، MyWidgetيحصل الفصل على مثيل MyModelباستخدام أداة المستهلك . تعطينا هذه الأداة builderاحتواء كائننا في المعلمة value.

الآن ، ماذا نفعل إذا أردنا تحديث البيانات في نموذجنا؟ لنفترض أن لدينا أداة أخرى حيث ، عند النقر على زر ، يجب تحديث الخاصية foo:

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.foo = 'bar';
      },
    );
  }
}

لاحظ الصيغة المحددة المستخدمة للوصول إلى المثيل MyModel. من الناحية الوظيفية ، هذا يعادل الوصول إلى القطعة Consumer. القطعة Consumerمفيدة في الحالات التي يتعذر فيها على التعليمات البرمجية الحصول على الرابط بسهولة BuildContext.

ما رأيك سيحدث للأداة الأصلية MyWidgetالتي أنشأناها في وقت سابق؟ هل سيتم عرض معنى جديد فيه bar؟ لسوء الحظ لا . لا يمكن الاستماع إلى التغييرات في كائنات Dart التقليدية القديمة (على الأقل بدون انعكاس ، وهو غير متوفر في Flutter). وبالتالي ، Providerلن تتمكن من "معرفة" أننا قمنا بتحديث الخاصية بشكل صحيح fooوأمرنا MyWidgetبتحديث القطعة استجابة.

ChangeNotifierProvider

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

class MyModel with ChangeNotifier {
  String _foo;  String get foo => _foo;
  
  void set foo(String value) {
    _foo = value;
    notifyListeners();  
  }
}

كما ترون، نحن استبدال ممتلكاتنا fooمع getterو setterمدعومة المتغير الخاص _foo. بهذه الطريقة يمكننا "اعتراض" أي تغييرات تم إجراؤها على خاصية foo وإخبار مستمعينا بأن جسمنا قد تغير.

الآن ، من الخارج Provider، يمكننا تغيير تطبيقنا بحيث يستخدم فئة مختلفة تسمى ChangeNotifierProvider:

ChangeNotifierProvider<MyModel>(
  builder: (context) => MyModel(),
  child: MyApp(...),
)

مثله! الآن ، عندما نقوم OtherWidgetبتحديث الخاصية fooفي المثال MyModel، MyWidgetسيتم تحديثها تلقائيًا لتعكس هذا التغيير. رائع ، أليس كذلك؟

بالمناسبة. ربما لاحظت معالج زر OtherWidgetاستخدمنا به بناء الجملة التالي:

final model = Provider.of<MyModel>(context);

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

final model = Provider.of<MyModel>(context, listen: false);

هذا هو سحر آخر المقدمة في الحزمة Providerمثل هذا تماما.

StreamProvider

للوهلة الأولى ، ليس من الواضح سبب الحاجة إليها StreamProvider. في النهاية ، يمكنك فقط استخدام المعتاد StreamBuilderإذا كنت بحاجة إلى استهلاك دفق في Flutter. على سبيل المثال ، هنا نستمع إلى الدفق onAuthStateChangedالمقدم من FirebaseAuth:

@override
Widget build(BuildContext context {
  return StreamBuilder(
   stream: FirebaseAuth.instance.onAuthStateChanged, 
   builder: (BuildContext context, AsyncSnapshot snapshot){ 
     ...
   });
}

للقيام بالشيء نفسه بالمساعدة Provider، يمكننا توفير تدفقنا من خلال StreamProviderجذر تطبيقنا:

StreamProvider<FirebaseUser>.value(
  stream: FirebaseAuth.instance.onAuthStateChanged,
  child: MyApp(...),
}

ثم استهلك الأداة المصغرة التابعة ، كما هو الحال عادةً مع Provider:

@override
Widget build(BuildContext context) {
  return Consumer<FirebaseUser>(
    builder: (context, value, child) => Text(value.displayName),
  );
}

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

FutureProvider

مشابه للمثال أعلاه ، فهو FutureProviderبديل للمعيار FutureBuilderعند العمل مع الحاجيات. هنا مثال:

FutureProvider<FirebaseUser>.value(
  value: FirebaseAuth.instance.currentUser(),
  child: MyApp(...),
);

لاستهلاك هذه القيمة في الأداة الفرعية التابعة ، نستخدم نفس التنفيذ Consumerكما في المثال StreamProviderأعلاه.

ValueListenableProvider

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

class MyModel {
  final ValueNotifier<int> counter = ValueNotifier(0);  
}

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

Provider<MyModel>(
  builder: (context) => MyModel(),
  child: Consumer<MyModel>(builder: (context, value, child) {
    return ValueListenableProvider<int>.value(
      value: value.counter,
      child: MyApp(...)
    }
  }
}

يرجى ملاحظة أن هذا الموفر المتداخل من النوع int. قد يكون هناك آخرون. إذا كان لديك العديد من مقدمي الخدمة من نفس النوع المسجل ، فسيرجع المزود "الأقرب" (أقرب سلف).

إليك كيفية الاستماع إلى خاصية counterمن أي أداة طفل:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Consumer<int>(
      builder: (context, value, child) {
        return Text(value.toString());
      },
    );
  }
}

ولكن إليك كيفية تحديث خاصية counterمن أداة أخرى. يرجى ملاحظة: نحتاج إلى الوصول إلى النسخة الأصلية MyModel.

class OtherWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text('Update'),
      onPressed: () {
        final model = Provider.of<MyModel>(context);
        model.counter.value++;
      },
    );
  }
}

MultiProvider

إذا كنت تستخدم العديد من عناصر واجهة المستخدمProvider، فعندئذٍ في جذر التطبيق تحصل على بنية قبيحة من العديد من المرفقات:

Provider<Foo>.value( 
  value: foo, 
  child: Provider<Bar>.value( 
    value: bar, 
    child: Provider<Baz>.value( 
      value: baz , 
      child: MyApp(...)
    ) 
  ) 
)

MultiProviderيسمح لك بإعلانها جميعًا على نفس المستوى. إنه مجرد السكر النحوي: على مستوى النظام الداخلي ، تظل جميعها متداخلة على أي حال.

MultiProvider( 
  providers: [ 
    Provider<Foo>.value(value: foo), 
    Provider<Bar>.value(value: bar), 
    Provider<Baz>.value(value: baz), 
  ], 
  child: MyApp(...), 
)

ProxyProvider

ProxyProvider هي فئة مثيرة للاهتمام تمت إضافتها في إصدار الحزمة الثالثةProvider. يسمح لك بالإعلان عن مقدمي الخدمات الذين قد يعتمدون هم أنفسهم على مقدمي خدمات آخرين ، حتى ستة على واحد. في هذا المثال ، تكون فئة Bar خاصة بالمثيلFoo. هذا مفيد عند تجميع مجموعة جذرية من الخدمات التي تعتمد على بعضها البعض.

MultiProvider ( 
  providers: [ 
    Provider<Foo> ( 
      builder: (context) => Foo(),
    ), 
    ProxyProvider<Foo, Bar>(
      builder: (context, value, previous) => Bar(value),
    ), 
  ], 
  child: MyApp(...),
)

وسيطة النوع العام الأول هي النوع الذي تعتمد عليه ProxyProvider، والثاني هو النوع الذي يرجعه.

كيفية الاستماع إلى العديد من مقدمي الخدمات في نفس الوقت


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

Consumer2<MyModel, int>(
  builder: (context, value, value2, child) {
    //value  MyModel
    //value2  int
  },
);

استنتاج


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

All Articles