مرحبا يا هابر!تتضمن خططنا متوسطة المدى إصدار كتاب 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';
موفر القاعدةإنشاء الأساس Provide
r في جذر تطبيقنا ؛ سيحتوي هذا على مثال لنموذجنا: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
أعلاه.ValueListenableProviderValueListenable هو واجهة دارت تنفذها ValueNotifier الطبقة التي تأخذ قيمة وتخطر المستمعين عندما يتحول إلى قيمة أخرى. من الممكن ، على سبيل المثال ، لف عداد صحيح في فئة نموذج بسيطة:class MyModel {
final ValueNotifier<int> counter = ValueNotifier(0);
}
عند العمل مع أنواع معقدة ، فإنه ValueNotifier
يستخدم عامل ==
الكائن المخزن فيه لتحديد ما إذا كانت القيمة قد تغيرت.لنقم بإنشاء أبسط واحد Provider
، والذي سيحتوي على نموذجنا الرئيسي ، وسيتبعه خاصية استماع Consumer
متداخلة :ValueListenableProvider
counter
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(...),
)
ProxyProviderProxyProvider
هي فئة مثيرة للاهتمام تمت إضافتها في إصدار الحزمة الثالثة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) {
},
);
استنتاج
عند استخدامه ، InheritedWidget
Provider
فإنه يسمح لك بإدارة الحالة كما هو معتاد في Flutter. يسمح لعناصر واجهة المستخدم بالوصول إلى كائنات الحالة والاستماع إليها بطريقة يتم فيها تلخيص آلية الإعلام الأساسية. من السهل إدارة عمر كائنات الحالة من خلال إنشاء نقاط ربط لإنشاء هذه الكائنات حسب الحاجة والتخلص منها عند الحاجة. يمكن استخدام هذه الآلية لتنفيذ التبعيات بسهولة وحتى كأساس لخيارات إدارة الحالة الأكثر تقدمًا. بفضل نعمة Google والدعم المتزايد في مجتمع Flutter ، Provider
أصبحت حزمة تستحق المحاولة دون تأخير!