RxVMS基础:RxCommand和GetIt

这是我的Flutter体系结构系列的第四部分:



尽管前两部分显然与RxVMS模式无关,但是对于清楚地了解此方法而言,它们是必需的。 现在,我们转到在应用程序中使用RxVMS所需的最重要的程序包。


GetIt:快速ServiceLocator


当您回顾显示应用程序中各种RxVMS元素的图表时...


图片


...也许您想知道各种映射,管理器和服务如何相互了解。 更重要的是,您可能想知道一个元素如何访问另一个元素的功能。


通过许多不同的方法(例如继承的小部件,IoC容器,DI ...),我个人更喜欢服务定位器。 在这方面,我有一篇关于GetIt特别文章 -这种方法的实现,但是在这里我将略微谈谈该主题。 通常,您只需在该服务中注册对象一次,然后就可以在整个应用程序中访问它们。 有点单身……但具有更大的灵活性。


使用方法


使用GetIt很明显。 在应用程序的最开始,您注册了您计划随后使用的服务和/或管理器。 将来,只需调用GetIt方法即可访问已注册类的实例。


一个不错的功能是,您可以像具体实现一样注册接口或抽象类。 访问实例时,只需使用接口/抽象,即可在注册期间轻松替换必要的实现。 这使您可以轻松地将实际服务切换到MockService。


一点练习


我通常通过全局变量在名为service_locator.dart的文件中初始化SeviceLocator。 这样就为整个项目获得了一个全局变量。


//    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时,导入文件时始终使用相同的样式-包(推荐)或相对路径,但不能同时使用两种方法。 这是因为Dart不管这些文件的身份如何,都将它们视为不同的文件。


如果您对此感到困难,请访问我的博客以获取详细信息。


接收命令


现在,我们使用GetIt来访问任何地方的对象(包括用户界面),我想描述如何实现UI事件的处理函数。 最简单的方法是将功能添加到管理器并在小部件中调用它们:


 class SearchManager { void lookUpZip(String zip); } 

然后在用户界面中


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

这将在TextField每次更改时lookUpZip 。 但是我们如何将结果传递给我们? 由于我们希望是反应式的,因此可以在SearchManager添加一个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怎么办?
  • 错误处理-如果一个函数抛出异常怎么办?

当然,我们可以添加更多StreamController来处理状态和错误...但是很快它变得很乏味,并且rx_command包在这里派上用场。


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具有一个输入和五个输出Observables:


  • 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. 

    这意味着


    • 如果isExecuting返回true则为true
    • 仅当isExecuting返回false并且canExecuteInput不返回false ,它将返回false

  • thrownExceptions是一个Observable<Exception> 。 打包函数可以抛出的所有异常都将被捕获并发送到此Observable。 如果发生错误,可以方便地收听并显示对话框。


  • (团队本身)实际上也是可观察的。 工作函数返回的值将在此通道上传输,因此您可以将RxCommand作为流参数直接传递给StreamBuilder


  • 结果在一个Observable<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'; } } 

如果要将命令的结果直接传递到StreamBuilder,则.results Observable尤其有用。 这将根据命令的状态显示不同的内容,并且与rx_widgets包中的RxLoader一起使用时效果很好。 这是一个使用.results Observable的RxLoader小部件示例:


 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()}")), ), ), 

创建RxCommand


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使您可以访问命令执行结果的最后成功值,该值可以在StreamBuilder中用作initialData


如果要在运行时或发生错误时获取CommandResult事件中包含的最后结果,则可以在创建命令时传递emitInitialCommandResult = true


例如,如果要将初始值分配给.lastResult ,则在StreamBuilder中将其用作initialData时,可以在创建命令时将它与initialLastResult参数一起传递。


示例-使Flutter具有反应性


示例的最新版本已为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实际上是updateWeatherCommandObservable canExecute


现在,让我们看看如何在用户界面中使用Manager:


  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 StatefulWidget函数中侦听Observables命令。 这是一个真实项目的示例。


createEventCommand定义:


  RxCommand<Event, void> createEventCommand; 

这将在数​​据库中创建一个Event对象,并且不会返回任何实际值。 但是,正如我们之前所了解的,即使返回类型为void的RxCommand在完成时也将返回单个数据项。 因此,命令完成后,我们可以使用此行为来触发应用程序中的操作:


 @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(); } 

此外,如果要使用忙碌计数器显示,可以:


  • 监听initState函数中的initState Observable命令;
  • 显示/隐藏订阅中的计数器; 以及
  • 使用命令本身作为StreamBuilder的数据源

使用RxCommandListeners使生活更轻松


如果要使用多个Observable,则可能必须管理多个订阅。 直接控制侦听和释放一组订阅可能很困难,这会使代码的可读性降低,并使您有出错的风险(例如,在完成过程中忘记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命令,用户可以在其中上传图像。 调用命令时:


  • 显示“图像选择器”对话框
  • 选择图像后加载
  • 下载完成后,该命令将返回图像存储地址,以便您可以创建新的聊天条目。

没有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")); 

通常,如果有多个Observable,我将始终使用RxCommandListener。


尝试使用RxCommands ,看看它如何使您的生活更轻松。
顺便说一下,您不需要使用RxVMS来利用RxCommands


有关RxCommand更多信息RxCommand阅读RxCommand 自述包。

Source: https://habr.com/ru/post/zh-CN449872/


All Articles