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

... ربما كنت تتساءل كيف تعرف مختلف المخططات والمديرين والخدمات عن بعضهم البعض. الأهم من ذلك ، قد تتساءل كيف يمكن لأحد العناصر الوصول إلى وظائف عنصر آخر.
مع العديد من الطرق المختلفة (مثل الأدوات الوراثية ، حاويات IoC ، DI ...) ، أنا شخصياً أفضِّل تحديد موقع الخدمة. في هذا الصدد ، لدي مقال خاص حول GetIt - تنفيذي لهذا النهج ، لكنني سأتطرق هنا إلى هذا الموضوع قليلاً. بشكل عام ، تقوم بتسجيل الكائنات في هذه الخدمة مرة واحدة ، ومن ثم يمكنك الوصول إليها من خلال التطبيق. إنه نوع من المفردة ... ولكن بمزيد من المرونة.
استخدام
باستخدام GetIt هو واضح جدا. في بداية التطبيق ، تقوم بتسجيل الخدمات و / أو المديرين الذين تخطط لاستخدامهم لاحقًا. في المستقبل ، ما عليك سوى استدعاء أساليب GetIt للوصول إلى مثيلات الفئات المسجلة.
ميزة لطيفة هي أنه يمكنك تسجيل واجهات أو فئات مجردة تماما مثل تطبيقات ملموسة. عند الوصول إلى مثيل ، ما عليك سوى استخدام واجهات / تجريدات ، واستبدال التطبيقات الضرورية بسهولة أثناء التسجيل. هذا يتيح لك التبديل بسهولة الخدمة الحقيقية إلى MockService.
قليلا من الممارسة
عادة ما أقوم بتهيئة SeviceLocator في ملف يسمى service_locator.dart من خلال متغير عمومي. وبالتالي يتم الحصول على متغير عمومي واحد للمشروع بأكمله.
كلما أردت الوصول ، ما عليك سوى الاتصال
RegistrationType object = sl.get<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
على أنه
.results
مفيدًا بشكل خاص إذا كنت ترغب في تمرير نتيجة أمر مباشرة إلى StreamBuilder. سيعرض هذا محتوى مختلفًا استنادًا إلى حالة الأمر ، RxLoader
بشكل جيد للغاية مع RxLoader
من حزمة rx_widgets . فيما يلي مثال لعنصر واجهة مستخدم .results
يستخدم .results
يمكن .results
:
Expanded(
إنشاء 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() {
يمكنك الجمع بين 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, ),
أنماط الاستخدام النموذجي
لقد رأينا بالفعل طريقة واحدة للرد على حالات مختلفة من الأوامر باستخدام CommandResults . في الحالات التي نريد فيها عرض ما إذا كان الأمر ناجحًا (ولكن لا يتم عرض النتيجة) ، هناك نمط شائع يتمثل في الاستماع إلى أمر initState
وظيفة initState
StatefulWidget. هنا مثال على مشروع حقيقي.
تعريف createEventCommand
:
RxCommand<Event, void> createEventCommand;
سيؤدي هذا إلى إنشاء كائن حدث في قاعدة البيانات ولن يُرجع أي قيمة حقيقية. ولكن ، كما تعلمنا سابقًا ، حتى RxCommand مع نوع إرجاع من void
سيعود عنصر بيانات واحد عند الاكتمال. وبالتالي ، يمكننا استخدام هذا السلوك لتحريك إجراء في طلبنا بمجرد اكتمال الأمر:
@override void initState() {
مهم : لا تنس إكمال الاشتراكات عندما لا نحتاج إليها:
@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;
لا تحتاج إلى تمرير جميع وظائف المعالج. جميعها اختيارية ، لذلك يمكنك ببساطة نقل تلك التي تحتاج إليها. تحتاج فقط إلى استدعاء dispose
أجل RxCommandListener
في وظيفة dispose
الخاصة بك ، وسوف يلغي جميع المستخدمة داخل الاشتراك.
دعونا نقارن نفس الرمز مع وبدون RxCommandListener
في مثال حقيقي آخر. يتم استخدام الأمر selectAndUploadImageCommand
هنا على شاشة الدردشة حيث يمكن للمستخدم تحميل الصور. عندما يسمى الأمر:
- يتم عرض مربع حوار ImagePicker.
- بعد اختيار يتم تحميل الصورة
- بعد اكتمال التنزيل ، يقوم الأمر بإرجاع عنوان تخزين الصور بحيث يمكنك إنشاء إدخال دردشة جديد.
بدون RxCommandListener :
_selectImageCommandSubscription = sl .get<ImageManager>() .selectAndUploadImageCommand .listen((imageLocation) async { if (imageLocation == null) return;
باستخدام 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 .