أساسيات RxVMS: RxCommand و GetIt

هذا هو الجزء الرابع من سلسلة هندسة Flutter الخاصة بي:



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


GetIt: خدمة سريعةالموقع


عندما تتذكر رسمًا بيانيًا يوضح عناصر RxVMS المختلفة في تطبيق ...


صورة


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


مع العديد من الطرق المختلفة (مثل الأدوات الوراثية ، حاويات IoC ، DI ...) ، أنا شخصياً أفضِّل تحديد موقع الخدمة. في هذا الصدد ، لدي مقال خاص حول GetIt - تنفيذي لهذا النهج ، لكنني سأتطرق هنا إلى هذا الموضوع قليلاً. بشكل عام ، تقوم بتسجيل الكائنات في هذه الخدمة مرة واحدة ، ومن ثم يمكنك الوصول إليها من خلال التطبيق. إنه نوع من المفردة ... ولكن بمزيد من المرونة.


استخدام


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


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


قليلا من الممارسة


عادة ما أقوم بتهيئة SeviceLocator في ملف يسمى service_locator.dart من خلال متغير عمومي. وبالتالي يتم الحصول على متغير عمومي واحد للمشروع بأكمله.


//    GetIt sl = new GetIt(); void setUpServiceLocator(ErrorReporter reporter) { //  // [registerSingleton]  -  . //   . // sl.get<ErrorReporter>.get()    . sl.registerSingleton<ErrorReporter>(reporter); // [registerLazySingleton]   ,       // sl.get<ImageService>.get()            . sl.registerLazySingleton<ImageService>(() => new ImageServiceImplementation()); sl.registerLazySingleton<MapService>(() => new MapServiceImplementation()); //  sl.registerSingleton<UserManager>(new UserManagerImplementation()); sl.registerLazySingleton<EventManager>(() => new EvenManagerImplementation()); sl.registerLazySingleton<ImageManager>(() => new ImageManagerImplementation()); sl.registerLazySingleton<AppManager>(() => new AppManagerImplementation()); 

كلما أردت الوصول ، ما عليك سوى الاتصال


 RegistrationType object = sl.get<RegistrationType>(); //  GetIt  `callable`,   : RegistrationType object2 = sl<RegistrationType>(); 

ملاحظة مهمة للغاية: عند استخدام GetIt ، استخدم دائمًا النمط نفسه عند استيراد الملفات - إما الحزم (الموصى بها) أو المسارات النسبية ، ولكن ليس كلا النهجين في وقت واحد. وذلك لأن دارت يتعامل مع هذه الملفات على أنها مختلفة ، على الرغم من هويتها.


إذا كان هذا صعبًا عليك ، فيرجى الانتقال إلى مدونتي للحصول على التفاصيل.


RxCommand


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


 class SearchManager { void lookUpZip(String zip); } 

ثم في واجهة المستخدم


 TextField(onChanged: sl.get<SearchManager>().lookUpZip,) 

هذا سوف lookUpZip على كل تغيير في TextField . لكن كيف ننقل النتيجة إلينا في المستقبل؟ نظرًا لأننا نريد أن نكون StreamController ، فإننا نضيف StreamController إلى StreamController لدينا:


 abstract class SearchManager { Stream<String> get nameOfCity; void lookUpZip(String zip); } class SearchManagerImplementation implements SearchManager{ @override Stream<String> get nameOfCity => cityController.stream; StreamController<String> cityController = new StreamController(); @override Future<void> lookUpZip(String zip) async { var cityName = await sl.get<ZipApiService>().lookUpZip(zip); cityController.add(cityName); } } 

وفي واجهة المستخدم:


 StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().nameOfCity, builder: (context, snapshot) => Text(snapShot.data); 

على الرغم من أن هذا النهج يعمل ، فهو ليس الأمثل. هذه هي المشاكل:


  • شفرة زائدة عن الحاجة - يجب علينا دائمًا إنشاء طريقة ، StreamController ، و getter لدفقها ، إذا لم نرغب في عرضها بشكل صريح في المجال العام
  • الحالة المزدحمة - ماذا لو كنا نرغب في عرض Spinner أثناء قيام الوظيفة بعملها؟
  • معالجة الأخطاء - ماذا يحدث إذا ألقت الدالة استثناءً؟

بالطبع ، يمكننا إضافة المزيد من StreamControllers للتعامل مع الحالات والأخطاء ... ولكن سرعان ما تصبح مملة ، وتأتي حزمة rx_command في متناول اليد هنا .


RxCommand يحل جميع المشاكل المذكورة أعلاه وأكثر من ذلك بكثير. RxCommand بتغليف وظيفة (متزامن أو غير متزامن) RxCommand تلقائيًا في الدفق.


باستخدام RxCommand ، يمكننا إعادة كتابة مديرنا مثل هذا:


 abstract class SearchManager { RxCommand<String,String> lookUpZipCommand; } class SearchManagerImplementation implements SearchManager{ @override RxCommand<String,String> lookUpZipCommand; SearchManagerImplementation() { lookUpZipCommand = RxCommand.createAsync((zip) => sl.get<ZipApiService>().lookUpZip(zip)); } } 

وفي واجهة المستخدم:


 TextField(onChanged: sl.get<SearchManager>().lookUpZipCommand,) ... StreamBuilder<String>( initialData:'', stream: sl.get<SearchManager>().lookUpZipCommand, builder: (context, snapshot) => Text(snapShot.data); 

وهو أكثر إيجازا وقراءة.


RxCommand بالتفصيل



يحتوي RxCommand على مدخل واحد وخمسة عناصر مراقبة:


  • canExecuteInput هو Observable<bool> يمكن ملاحظته اختياريًا يمكنك تمريره إلى وظيفة المصنع عند إنشاء RxCommand. يشير إلى RxCommand ما إذا كان يمكن تنفيذه ، وهذا يتوقف على القيمة الأخيرة التي تلقاها.


  • isExecuting هو Observable<bool> ملاحظته يشير إلى ما إذا كان الأمر يؤدي وظيفته حاليًا. عندما يكون الفريق مشغولاً ، لا يمكن إعادة تشغيله. إذا كنت ترغب في عرض Spinner في وقت التشغيل ، فاستمع إلى isExecuting


  • canExecute هو Observable<bool> ملاحظته يشير إلى القدرة على تنفيذ أمر. هذا ، على سبيل المثال ، سارت الامور بشكل جيد مع StreamBuilder لتغيير مظهر زر بين الدولة على / قبالة.
    معناها هو كما يلي:


     Observable<bool> canExecute = Observable.combineLatest2<bool,bool>(canExecuteInput,isExecuting) => canExecuteInput && !isExecuting).distinct. 

    وهو ما يعني


    • ستكون false إذا كانت isExecuting ترجع إلى true
    • سيتم إرجاع true فقط إذا كان isExecuting بإرجاع false و canExecuteInput لا يُرجع false .

  • thrownExceptions هو Observable<Exception> ملاحظته . سيتم اكتشاف جميع الاستثناءات التي يمكن أن تقوم بها الوظيفة المعبأة وإرسالها إلى هذه الملاحظة. من الملائم الاستماع إليه وعرض مربع حوار في حالة حدوث خطأ.


  • (الفريق نفسه) هو في الواقع أيضا ملحوظ. سيتم إرسال القيم التي يتم إرجاعها بواسطة وظيفة العمل على هذه القناة ، بحيث يمكنك تمرير RxCommand مباشرة إلى StreamBuilder كمعلمة دفق


  • تحتوي النتائج على جميع حالات الأوامر في Observable<CommandResult> واحدة Observable<CommandResult> ، حيث CommandResult تعريف CommandResult على أنه



 /// Combined execution state of an `RxCommand` /// Will be issued for any state change of any of the fields /// During normal command execution, you will get this item's listening at the command's [.results] observable. /// 1. If the command was just newly created, you will get `null, false, false` (data, error, isExecuting) /// 2. When calling execute: `null, false, true` /// 3. When exceution finishes: `result, false, false` class CommandResult<T> { final T data; final Exception error; final bool isExecuting; const CommandResult(this.data, this.error, this.isExecuting); bool get hasData => data != null; bool get hasError => error != null; @override bool operator ==(Object other) => other is CommandResult<T> && other.data == data && other.error == error && other.isExecuting == isExecuting; @override int get hashCode => hash3(data.hashCode, error.hashCode, isExecuting.hashCode); @override String toString() { return 'Data: $data - HasError: $hasError - IsExecuting: $isExecuting'; } } 

.results مفيدًا بشكل خاص إذا كنت ترغب في تمرير نتيجة أمر مباشرة إلى StreamBuilder. سيعرض هذا محتوى مختلفًا استنادًا إلى حالة الأمر ، RxLoader بشكل جيد للغاية مع RxLoader من حزمة rx_widgets . فيما يلي مثال لعنصر واجهة مستخدم .results يستخدم .results يمكن .results :


 Expanded( /// RxLoader      ///    Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, /// ,  .hasData == true dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), /// ,  .isExceuting == true placeHolderBuilder: (context) => Center( key: AppKeys.loaderPlaceHolder, child: Text("No Data")), /// ,  .hasError == true errorBuilder: (context, ex) => Center( key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), 

إنشاء RxCommands


يمكن أن تستخدم RxCommands وظائف متزامنة وغير متزامنة:


  • ليس لديهم معلمة ولا يُرجعون نتيجة ؛
  • لديهم معلمة ولا تُرجع نتيجة ؛
  • ليس لديهم معلمة وإرجاع نتيجة ؛
  • لديهم معلمة وإرجاع نتيجة.

بالنسبة لجميع الخيارات ، توفر RxCommand عدة طرق للمصنع ، مع مراعاة المعالجات المتزامنة وغير المتزامنة:


 static RxCommand<TParam, TResult> createSync<TParam, TResult>(Func1<TParam, TResult> func,... static RxCommand<void, TResult> createSyncNoParam<TResult>(Func<TResult> func,... static RxCommand<TParam, void> createSyncNoResult<TParam>(Action1<TParam> action,... static RxCommand<void, void> createSyncNoParamNoResult(Action action,... static RxCommand<TParam, TResult> createAsync<TParam, TResult>(AsyncFunc1<TParam, TResult> func,... static RxCommand<void, TResult> createAsyncNoParam<TResult>(AsyncFunc<TResult> func,... static RxCommand<TParam, void> createAsyncNoResult<TParam>(AsyncAction1<TParam> action,... static RxCommand<void, void> createAsyncNoParamNoResult(AsyncAction action,... 

حتى إذا لم تُرجع الدالة المحزومة قيمة ، فسوف تُرجع RxCommand قيمة فارغة بعد تنفيذ الوظيفة. وبالتالي ، يمكنك ضبط المستمع على مثل هذا الأمر بحيث يستجيب لإكمال الوظيفة.


الوصول إلى أحدث النتائج


يمنحك RxCommand.lastResult الوصول إلى آخر قيمة ناجحة لنتيجة تنفيذ الأمر ، والتي يمكن استخدامها كـ initialData في StreamBuilder.


إذا كنت ترغب في الحصول على النتيجة الأخيرة المضمنة في أحداث CommandResult في وقت التشغيل أو في حالة حدوث خطأ ، يمكنك تمرير emitInitialCommandResult = true عند إنشاء الأمر.


إذا كنت تريد تعيين قيمة أولية إلى .lastResult ، على سبيل المثال ، إذا كنت تستخدمها كـ initialData في StreamBuilder ، فيمكنك تمريرها باستخدام معلمة initialLastResult عند إنشاء الأمر.


مثال - جعل رفرفة رد الفعل


تمت إعادة تنظيم أحدث إصدار من المثال لـ RxVMS ، لذا يجب أن يتوفر لديك الآن خيار جيد حول كيفية استخدامه.


نظرًا لأن هذا التطبيق بسيط للغاية ، فنحن بحاجة إلى مدير واحد فقط:


 class AppManager { RxCommand<String, List<WeatherEntry>> updateWeatherCommand; RxCommand<bool, bool> switchChangedCommand; RxCommand<String, String> textChangedCommand; AppManager() { //    bool         //   Observable switchChangedCommand = RxCommand.createSync<bool, bool>((b) => b); //    switchChangedCommand  canExecute Observable  // updateWeatherCommand updateWeatherCommand = RxCommand.createAsync<String, List<WeatherEntry>>( sl.get<WeatherService>().getWeatherEntriesForCity, canExecute: switchChangedCommand, ); //         textChangedCommand = RxCommand.createSync<String, String>((s) => s); //    ... textChangedCommand //     500ms... .debounce(new Duration(milliseconds: 500)) // ...   updateWeatherCommand .listen(updateWeatherCommand); //     updateWeatherCommand(''); } } 

يمكنك الجمع بين RxCommands مختلفة معًا. لاحظ أن switchedChangedCommand الواقع يمكن ملاحظتهاالتنفيذ للتحديث updateWeatherCommand .


الآن دعونا نرى كيف يتم استخدام مدير في واجهة المستخدم:


  return Scaffold( appBar: AppBar(title: Text("WeatherDemo")), resizeToAvoidBottomPadding: false, body: Column( children: <Widget>[ Padding( padding: const EdgeInsets.all(16.0), child: TextField( key: AppKeys.textField, autocorrect: false, controller: _controller, decoration: InputDecoration( hintText: "Filter cities", ), style: TextStyle( fontSize: 20.0, ), //    textChangedCommand! onChanged: sl.get<AppManager>().textChangedCommand, ), ), Expanded( /// RxLoader   builders   ///   Stream<CommandResult> child: RxLoader<List<WeatherEntry>>( spinnerKey: AppKeys.loadingSpinner, radius: 25.0, commandResults: sl.get<AppManager>().updateWeatherCommand.results, dataBuilder: (context, data) => WeatherListView(data, key: AppKeys.weatherList), placeHolderBuilder: (context) => Center(key: AppKeys.loaderPlaceHolder, child: Text("No Data")), errorBuilder: (context, ex) => Center(key: AppKeys.loaderError, child: Text("Error: ${ex.toString()}")), ), ), Padding( padding: const EdgeInsets.all(8.0), child: Row( children: <Widget>[ ///  Updatebutton    updateWeatherCommand.canExecute Expanded( //        Streambuilder, //      WidgetSelector child: WidgetSelector( buildEvents: sl .get<AppManager>() .updateWeatherCommand .canExecute, onTrue: RaisedButton( key: AppKeys.updateButtonEnabled, child: Text("Update"), onPressed: () { _controller.clear(); sl.get<AppManager>().updateWeatherCommand(); }, ), onFalse: RaisedButton( key: AppKeys.updateButtonDisabled, child: Text("Please Wait"), onPressed: null, ), ), ), //    canExecuteInput StateFullSwitch( state: true, onChanged: sl.get<AppManager>().switchChangedCommand, ), ], ), ), ], ), ); 

أنماط الاستخدام النموذجي


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


تعريف createEventCommand :


  RxCommand<Event, void> createEventCommand; 

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


 @override void initState() { //      ,        _eventCommandSubscription = _createCommand.listen((_) async { Navigator.pop(context); await showToast('Event saved'); }); //        _errorSubscription = _createEventCommand.thrownExceptions.listen((ex) async { await sl.get<ErrorReporter>().logException(ex); await showMessageDialog(context, 'There was a problem saving event', ex.toString()); }); } 

مهم : لا تنس إكمال الاشتراكات عندما لا نحتاج إليها:


 @override void dispose() { _eventCommandSubscription?.cancel(); _errorSubscription?.cancel(); super.dispose(); } 

بالإضافة إلى ذلك ، إذا كنت تريد استخدام عرض العداد المشغول ، فيمكنك:


  • الاستماع إلى أوامر isExecuting يمكن ملاحظتها في وظيفة initState ؛
  • إظهار / إخفاء العداد في الاشتراك ؛ كذلك
  • استخدم الأمر نفسه كمصدر بيانات لبرنامج StreamBuilder

جعل الحياة أسهل مع RxCommandListeners


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


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


 class RxCommandListener<TParam, TResult> { final RxCommand<TParam, TResult> command; //       final void Function(TResult value) onValue; //    isExecuting final void Function(bool isBusy) onIsBusyChange; //       final void Function(Exception ex) onError; //    canExecute final void Function(bool state) onCanExecuteChange; //    .results Observable  final void Function(CommandResult<TResult> result) onResult; //     /  final void Function() onIsBusy; final void Function() onNotBusy; //     final Duration debounceDuration; RxCommandListener(this.command,{ this.onValue, this.onIsBusyChange, this.onIsBusy, this.onNotBusy, this.onError, this.onCanExecuteChange, this.onResult, this.debounceDuration,} ) void dispose(); 

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


دعونا نقارن نفس الرمز مع وبدون RxCommandListener في مثال حقيقي آخر. يتم استخدام الأمر selectAndUploadImageCommand هنا على شاشة الدردشة حيث يمكن للمستخدم تحميل الصور. عندما يسمى الأمر:


  • يتم عرض مربع حوار ImagePicker.
  • بعد اختيار يتم تحميل الصورة
  • بعد اكتمال التنزيل ، يقوم الأمر بإرجاع عنوان تخزين الصور بحيث يمكنك إنشاء إدخال دردشة جديد.

بدون RxCommandListener :


 _selectImageCommandSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .listen((imageLocation) async { if (imageLocation == null) return; //    sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }); _selectImageIsExecutingSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .isExecuting .listen((busy) { if (busy) { MySpinner.show(context); } else { MySpinner.hide(); } }); _selectImageErrorSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .thrownExceptions .listen((ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection")); 

باستخدام RxCommandListener :


 selectImageListener = RxCommandListener( command: sl.get<ImageManager>().selectAndUploadImageCommand, onValue: (imageLocation) async { if (imageLocation == null) return; sl.get<EventManager>().createChatEntryCommand(new ChatEntry( event: widget.event, isImage: true, content: imageLocation.downloadUrl, )); }, onIsBusy: () => MySpinner.show(context), onNotBusy: MySpinner.hide, onError: (ex) => showMessageDialog(context, 'Upload problem', "We cannot upload your selected image at the moment. Please check your internet connection")); 

بشكل عام ، أود دائمًا استخدام RxCommandListener إذا كان هناك أكثر من ملاحظة.


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


لمزيد من المعلومات حول RxCommand اقرأ حزمة RxCommand readme .

Source: https://habr.com/ru/post/ar449872/


All Articles