كيفية الاستفادة من BLoC لمشاركة التعليمات البرمجية في Flutter و AngularDart

نشرت: 2022-03-11

في منتصف العام الماضي ، كنت أرغب في نقل تطبيق Android إلى iOS والويب. كان Flutter خيارًا لمنصات الأجهزة المحمولة ، وكنت أفكر فيما سأختاره على جانب الويب.

بينما وقعت في حب Flutter للوهلة الأولى ، كان لا يزال لدي بعض التحفظات: أثناء نشر الحالة أسفل شجرة عنصر واجهة المستخدم ، ستقوم Flutter's InheritedWidget أو Redux - بكل أشكالها - بتنفيذ المهمة ، ولكن مع إطار عمل جديد مثل Flutter ، توقع أن تكون طبقة العرض أكثر تفاعلاً قليلاً ، أي أن الأدوات المصغّرة ستكون عديمة الحالة نفسها ، وتتغير وفقًا للحالة التي يتم تغذيتها من الخارج ، لكنها ليست كذلك. Aslo ، يدعم Flutter فقط Android و iOS ، لكنني أردت النشر على الويب. لدي بالفعل الكثير من منطق الأعمال في تطبيقي وأردت إعادة استخدامه قدر الإمكان ، وكانت فكرة تغيير الكود في مكانين على الأقل لتغيير منطق الأعمال أمرًا غير مقبول.

بدأت أبحث حول كيفية تجاوز هذا ووجدت BLoC. للحصول على مقدمة سريعة ، أوصي بمشاهدة Flutter / AngularDart - مشاركة التعليمات البرمجية ، معًا بشكل أفضل (DartConf 2018) عندما يكون لديك الوقت.

نمط BLoC

رسم تخطيطي لتدفق الاتصالات في العرض و BLoC والمستودع وطبقات البيانات

BLoC هي كلمة خيالية اخترعتها Google وتعني " b usiness lo gic c omponents." تتمثل فكرة نمط BLoC في تخزين أكبر قدر ممكن من منطق عملك في رمز Dart النقي بحيث يمكن إعادة استخدامه بواسطة الأنظمة الأساسية الأخرى. لتحقيق ذلك ، هناك قواعد يجب اتباعها:

  • تواصل في طبقات. تتواصل طرق العرض مع طبقة BLoC ، التي تتصل بالمستودعات ، وتتحدث المستودعات إلى طبقة البيانات. لا تتخطى الطبقات أثناء التواصل.
  • التواصل عبر الواجهات. يجب كتابة الواجهات برمز Dart النقي والمستقل عن النظام الأساسي. لمزيد من المعلومات ، راجع الوثائق الخاصة بالواجهات الضمنية.
  • تعرض BLoCs فقط التدفقات والمصارف. ستتم مناقشة الإدخال / الإخراج الخاص بـ BLoC لاحقًا.
  • حافظ على بساطة الآراء. ابق منطق الأعمال خارج وجهات النظر. يجب عليهم فقط عرض البيانات والاستجابة لتفاعل المستخدم.
  • اجعل منصة BLoCs محايدة. تعد BLoCs عبارة عن كود Dart خالص ، ولذا يجب ألا تحتوي على منطق أو تبعيات خاصة بالمنصة. لا تتفرع إلى كود شرطي للنظام الأساسي. يتم تنفيذ BLoCs منطقيًا في Pure Dart وهي أعلى من التعامل مع النظام الأساسي الأساسي.
  • قم بإدخال التبعيات الخاصة بالمنصة. قد يبدو هذا متناقضًا مع القاعدة أعلاه ، لكن اسمعني. تعتبر BLoCs نفسها حيادية للنظام الأساسي ، ولكن ماذا لو احتاجوا إلى التواصل مع مستودع خاص بمنصة؟ احقن. من خلال ضمان الاتصال عبر الواجهات وحقن هذه المستودعات ، يمكننا التأكد من أنه بغض النظر عما إذا كان المستودع الخاص بك مكتوبًا لـ Flutter أو AngularDart ، فإن BLoC لن يهتم.

آخر شيء يجب مراعاته هو أن إدخال BLoC يجب أن يكون بالوعة ، بينما يكون الإخراج من خلال دفق. كلاهما جزء من StreamController .

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

بناء تطبيق AngularDart و Flutter BLoC Todo

لقد أنشأت تطبيقًا بسيطًا للمهام في Flutter و AngularDart. يستخدم التطبيق Firecloud كنهاية خلفية ونهج تفاعلي لعرض الإنشاء. يتكون التطبيق من ثلاثة أجزاء:

  • bloc
  • todo_app_flutter
  • todoapp_dart_angular

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

كود BLoC

في bloc/ الدليل:

  • lib/src/bloc bloc: يتم تخزين وحدات BloC النمطية هنا كمكتبات Dart خالصة تحتوي على منطق الأعمال.
  • lib/src/repository : يتم تخزين واجهات البيانات في الدليل.
  • lib/src/repository/firestore : يحتوي المستودع على واجهة FireCloud للبيانات جنبًا إلى جنب مع نموذجها ، وبما أن هذا تطبيق نموذجي ، فلدينا نموذج بيانات واحد فقط todo.dart وواجهة واحدة للبيانات todo_repository.dart ؛ ومع ذلك ، في تطبيق العالم الحقيقي ، سيكون هناك المزيد من النماذج وواجهات المستودعات.
  • يحتوي lib/src/repository/preferences على preferences_interface.dart ، وهي واجهة بسيطة تخزن أسماء المستخدمين الذين تم تسجيل دخولهم بنجاح في التخزين المحلي على الويب أو التفضيلات المشتركة على الأجهزة المحمولة.
 //BLOC abstract class PreferencesInterface{ //Preferences final DEFAULT_USERNAME = "DEFAULT_USERNAME"; Future initPreferences(); String get defaultUsername; void setDefaultUsername(String username); }

يجب أن تقوم تطبيقات الويب والجوال بتنفيذ ذلك في المتجر والحصول على اسم المستخدم الافتراضي من التخزين المحلي / التفضيلات. يبدو تنفيذ AngularDart لهذا الأمر كما يلي:

 // ANGULAR DART class PreferencesInterfaceImpl extends PreferencesInterface { SharedPreferences _prefs; @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance(); @override void setDefaultUsername(String username) => _prefs.setString(DEFAULT_USERNAME, username); @override String get defaultUsername => _prefs.getString(DEFAULT_USERNAME); }

لا يوجد شيء مذهل هنا - إنها تنفذ ما تحتاجه. قد تلاحظ طريقة initPreferences() المتزامنة التي ترجع قيمة null . يجب تنفيذ هذه الطريقة على جانب Flutter نظرًا لأن الحصول على مثيل SharedPreferences على الهاتف المحمول غير متزامن.

 //FLUTTER @override Future initPreferences() async => _prefs = await SharedPreferences.getInstance();

دعنا نتمسك قليلاً بدير lib / src / bloc. يجب أن تحتوي أي طريقة عرض تتعامل مع منطق الأعمال على مكون BLoC الخاص بها. في هذا الدليل ، سترى طريقة العرض BLoCs base_bloc.dart و endpoints.dart و session.dart . آخر واحد مسؤول عن تسجيل دخول المستخدم وخروجه وتوفير نقاط النهاية لواجهات المستودع. سبب وجود واجهة الجلسة هو أن حزم firebase و firecloud ليست متطابقة للويب والجوال ويجب تنفيذها على أساس النظام الأساسي.

 // BLOC abstract class Session implements Endpoints { //Collections. @protected final String userCollectionName = "users"; @protected final String todoCollectionName = "todos"; String userId; Session(){ _isSignedIn.stream.listen((signedIn) { if(!signedIn) _logout(); }); } final BehaviorSubject<bool> _isSignedIn = BehaviorSubject<bool>(); Stream<bool> get isSignedIn => _isSignedIn.stream; Sink<bool> get signedIn => _isSignedIn.sink; Future<String> signIn(String username, String password); @protected void logout(); void _logout() { logout(); userId = null; } }

الفكرة هي جعل الفصل عالميًا (فردي). استنادًا إلى برنامج getter _isSignedIn.stream ، فإنه يتعامل مع تبديل التطبيق بين عرض تسجيل الدخول / عرض قائمة المهام ويوفر نقاط نهاية لتطبيقات المستودع إذا كان معرف المستخدم موجودًا (على سبيل المثال ، تم تسجيل دخول المستخدم).

base_bloc.dart هو الأساس لكل BLoCs. في هذا المثال ، يتعامل مع مؤشر التحميل وعرض مربع حوار الخطأ حسب الحاجة.

بالنسبة لمثال منطق العمل ، سنلقي نظرة على todo_add_edit_bloc.dart . يشرح الاسم الطويل للملف الغرض منه. لها طريقة باطلة خاصة _addUpdateTodo(bool addUpdate) .

 // BLOC void _addUpdateTodo(bool addUpdate) { if(!addUpdate) return; //Check required. if(_title.value.isEmpty) _todoError.sink.add(0); else if(_description.value.isEmpty) _todoError.sink.add(1); else _todoError.sink.add(-1); if(_todoError.value >= 0) return; final TodoBloc todoBloc = _todo.value == null ? TodoBloc("", false, DateTime.now(), null, null, null) : _todo.value; todoBloc.title = _title.value; todoBloc.description = _description.value; showProgress.add(true); _toDoRepository.addUpdateToDo(todoBloc) .doOnDone( () => showProgress.add(false) ) .listen((_) => _closeDetail.add(true) , onError: (err) => error.add( err.toString()) ); }

الإدخال لهذه الطريقة هو bool addUpdate وهو مستمع final BehaviorSubject<bool> _addUpdate = BehaviorSubject<bool>() . عندما ينقر المستخدم على زر الحفظ في التطبيق ، يرسل الحدث القيمة الحقيقية لمخزون الموضوع هذا ويطلق وظيفة BLoC هذه. هذه القطعة من الشفرة المرفرفة تفعل السحر على جانب العرض.

 // FLUTTER IconButton(icon: Icon(Icons.done), onPressed: () => _todoAddEditBloc.addUpdateSink.add(true),),

يتحقق _addUpdateTodo من أن العنوان والوصف ليسا فارغين ويغير قيمة _todoError BehaviorSubject بناءً على هذا الشرط. خطأ _todoError هو المسؤول عن تشغيل عرض خطأ العرض في حقول الإدخال إذا لم يتم توفير قيمة. إذا كان كل شيء على ما يرام ، فإنه يتحقق مما إذا كان سيتم إنشاء TodoBloc أو تحديثه ، وفي النهاية يقوم _toDoRepository بالكتابة إلى FireCloud.

منطق العمل هنا ولكن لاحظ:

  • فقط التدفقات والمصارف هي عامة في BLoC. _addUpdateTodo خاص ولا يمكن الوصول إليه من العرض.
  • يتم ملء _title.value و _description.value بواسطة المستخدم بإدخال القيمة في إدخال النص. يرسل إدخال النص في حدث تغيير النص قيمته إلى الأحواض المعنية. بهذه الطريقة ، لدينا تغيير تفاعلي للقيم في BLoC وعرضها في طريقة العرض.
  • _toDoRepository يعتمد على النظام الأساسي ويتم توفيره عن طريق الحقن.

تحقق من كود todo_list.dart BLoC _getTodos() . يستمع للحصول على لقطة من مجموعة المهام ويقوم بتدفق بيانات المجموعة لإدراجها في عرضها. يتم إعادة رسم قائمة العرض بناءً على تغيير دفق المجموعة.

 // BLOC void _getTodos(){ showProgress.add(true); _toDoRepository.getToDos() .listen((todosList) { todosSink.add(todosList); showProgress.add(false); }, onError: (err) { showProgress.add(false); error.add(err.toString()); }); }

الشيء المهم الذي يجب الانتباه إليه عند استخدام التدفقات أو مكافئ rx هو أنه يجب إغلاق التدفقات. نقوم بذلك بطريقة dispose() لكل BLoC. تخلص من BLoC لكل عرض في طريقة التخلص / التدمير.

 // FLUTTER @override void dispose() { widget.baseBloc.dispose(); super.dispose(); }

أو في مشروع AngularDart:

 // ANGULAR DART @override void ngOnDestroy() { todoListBloc.dispose(); }

حقن المستودعات الخاصة بالمنصة

رسم تخطيطي للعلاقات بين أنماط BLoC ومستودع todo والمزيد

قلنا من قبل أن كل ما يأتي في BLoC يجب أن يكون Dart بسيطًا ولا يعتمد على النظام الأساسي. يحتاج TodoAddEditBloc إلى ToDoRepository للكتابة إلى Firestore. يحتوي Firebase على حزم تعتمد على النظام الأساسي ، ويجب أن يكون لدينا تطبيقات منفصلة لواجهة ToDoRepository . يتم حقن هذه التطبيقات في التطبيقات. بالنسبة إلى Flutter ، استخدمت حزمة flutter_simple_dependency_injection وهي تبدو كالتالي:

 // FLUTTER class Injection { static Firestore _firestore = Firestore.instance; static FirebaseAuth _auth = FirebaseAuth.instance; static PreferencesInterface _preferencesInterface = PreferencesInterfaceImpl(); static Injector injector; static Future initInjection() async { await _preferencesInterface.initPreferences(); injector = Injector.getInjector(); //Session injector.map<Session>((i) => SessionImpl(_auth, _firestore), isSingleton: true); //Repository injector.map<ToDoRepository>((i) => ToDoRepositoryImpl(injector.get<Session>()), isSingleton: false); //Bloc injector.map<LoginBloc>((i) => LoginBloc(_preferencesInterface, injector.get<Session>()), isSingleton: false); injector.map<TodoListBloc>((i) => TodoListBloc(injector.get<ToDoRepository>(), injector.get<Session>()), isSingleton: false); injector.map<TodoAddEditBloc>((i) => TodoAddEditBloc(injector.get<ToDoRepository>()), isSingleton: false); } }

استخدم هذا في عنصر واجهة مستخدم مثل هذا:

 // FLUTTER TodoAddEditBloc _todoAddEditBloc = Injection.injector.get<TodoAddEditBloc>();

AngularDart مزود بحقن مدمج عبر مقدمي الخدمة.

 // ANGULAR DART @GenerateInjector([ ClassProvider(PreferencesInterface, useClass: PreferencesInterfaceImpl), ClassProvider(Session, useClass: SessionImpl), ExistingProvider(Endpoints, Session) ])

وفي أحد المكونات:

 // ANGULAR DART providers: [ overlayBindings, ClassProvider(ToDoRepository, useClass: ToDoRepositoryImpl), ClassProvider(TodoAddEditBloc), ExistingProvider(BaseBloc, TodoAddEditBloc) ],

يمكننا أن نرى أن Session عالمية. يوفر وظيفة تسجيل الدخول / الخروج ونقاط النهاية المستخدمة في ToDoRepository و BLoCs. يحتاج ToDoRepository إلى واجهة نقاط نهاية يتم تنفيذها في SessionImpl وما إلى ذلك. يجب أن يرى العرض فقط BLoC الخاص به وليس أكثر.

الآراء

رسم تخطيطي للمصارف والجداول المتفاعلة بين BLoC والعرض

يجب أن تكون المشاهدات بسيطة قدر الإمكان. إنهم يعرضون فقط ما يأتي من BLoC ويرسلون مدخلات المستخدم إلى BLoC. سننتقل إليه باستخدام عنصر واجهة مستخدم TodoAddEdit من Flutter وما يعادله على الويب TodoDetailComponent . يعرضون عنوان المهام المحددة والوصف ويمكن للمستخدم إضافة أو تحديث المهام.

رفرفة:

 // FLUTTER _todoAddEditBloc.todoStream.first.then((todo) { _titleController.text = todo.title; _descriptionController.text = todo.description; });

ولاحقًا في الكود ...

 // FLUTTER StreamBuilder<int>( stream: _todoAddEditBloc.todoErrorStream, builder: (BuildContext context, AsyncSnapshot errorSnapshot) { return TextField( onChanged: (text) => _todoAddEditBloc.titleSink.add(text), decoration: InputDecoration(hintText: Localization.of(context).title, labelText: Localization.of(context).title, errorText: errorSnapshot.data == 0 ? Localization.of(context).titleEmpty : null), controller: _titleController, ); }, ),

يعيد عنصر واجهة StreamBuilder نفسه إذا كان هناك خطأ (لم يتم إدراج أي شيء). يحدث هذا من خلال الاستماع إلى _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink _todoAddEditBloc.todoErrorStream . _todoAddEditBloc.titleSink ، وهو حوض في BLoC يحمل العنوان ويتم تحديثه عند إدخال المستخدم للنص في حقل النص.

يتم ملء القيمة الأولية لحقل الإدخال هذا (إذا تم تحديد أحد المهام) من خلال الاستماع إلى _todoAddEditBloc.todoStream الذي يحتفظ بالمهام المحددة أو الفارغة إذا أضفنا أمرًا جديدًا.

يتم تعيين قيمة إلى حقل نصي بواسطة وحدة التحكم _titleController.text = todo.title; .

عندما يقرر المستخدم حفظ المهام ، فإنه يضغط على أيقونة الاختيار في شريط التطبيق ويؤدي إلى تشغيل _todoAddEditBloc.addUpdateSink.add(true) . يستدعي ذلك _addUpdateTodo(bool addUpdate) الذي تحدثنا عنه في قسم BLoC السابق ويفعل كل منطق الأعمال الخاص بإضافة أو تحديث أو عرض الخطأ مرة أخرى للمستخدم.

كل شيء تفاعلي وليس هناك حاجة للتعامل مع حالة عنصر واجهة المستخدم.

رمز AngularDart أكثر بساطة. بعد توفير المكون BLoC الخاص به ، باستخدام الموفرين ، يقوم كود ملف todo_detail.html بالجزء الخاص بعرض البيانات وإرسال تفاعل المستخدم مرة أخرى إلى BLoC.

 // AngularDart <material-input #title label="{{titleStr}}" ngModel="{{(todoAddEditBloc.titleStream | async) == null ? '' : (todoAddEditBloc.titleStream | async)}}" (inputKeyPress)="todoAddEditBloc.titleSink.add($event)" [error]="(todoAddEditBloc.todoErrorStream | async) == 0 ? titleErrString : ''" autoFocus floatingLabel type="text" useNativeValidation="false" autocomplete="off"> </material-input> <material-input #description label="{{descriptionStr}}" ngModel="{{(todoAddEditBloc.descriptionStream | async) == null ? '' : (todoAddEditBloc.descriptionStream | async)}}" (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)" [error]="(todoAddEditBloc.todoErrorStream | async) == 1 ? descriptionErrString : ''" autoFocus floatingLabel type="text" useNativeValidation="false" autocomplete="off"> </material-input> <material-button animated raised role="button" class="blue" (trigger)="todoAddEditBloc.addUpdateSink.add(true)"> {{saveStr}} </material-button> <base-bloc></base-bloc>

على غرار Flutter ، نقوم بتعيين ngModel= القيمة من تدفق العنوان ، وهي القيمة الأولية.

 // AngularDart (inputKeyPress)="todoAddEditBloc.descriptionSink.add($event)"

يرسل حدث الإخراج inputKeyPress التي يكتبها المستخدم في إدخال النص إلى وصف BLoC. يرسل زر المواد (trigger)="todoAddEditBloc.addUpdateSink.add(true)" حدث إضافة / تحديث BLoC الذي يقوم مرة أخرى بتشغيل نفس _addUpdateTodo(bool addUpdate) في BLoC. إذا ألقيت نظرة على كود todo_detail.dart للمكون ، فسترى أنه لا يوجد شيء تقريبًا باستثناء السلاسل التي يتم عرضها في العرض. لقد وضعتها هناك وليس في HTML بسبب الترجمة المحتملة التي يمكن إجراؤها هنا.

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

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

تغليف

تتناول هذه المقالة الموجزة المفاهيم الرئيسية لتطبيق نمط BLoC. إنه دليل عملي على أن مشاركة الكود بين Flutter و AngularDart ممكنة ، مما يسمح بالتطوير الأصلي وعبر الأنظمة الأساسية.

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

آمل أن تعطيك هذه المقالة لمحة عن المتعة والحماس لدي في إنشاء تطبيقات الويب / الهاتف المحمول باستخدام Flutter / AngularDart ونمط BLoC. إذا كنت تتطلع إلى إنشاء تطبيقات سطح مكتب عبر الأنظمة الأساسية في JavaScript ، فاقرأ Electron: تطبيقات سطح المكتب عبر الأنظمة الأساسية أصبحت سهلة بواسطة زميلك Toptaler Stephane P. Pericat.

الموضوعات ذات الصلة: لغة Dart: عندما لا تكون Java و C # شاربين بدرجة كافية