这是我的Flutter体系结构系列的第四部分:
尽管前两部分显然与RxVMS模式无关,但是对于清楚地了解此方法而言,它们是必需的。 现在,我们转到在应用程序中使用RxVMS所需的最重要的程序包。
GetIt:快速ServiceLocator
当您回顾显示应用程序中各种RxVMS元素的图表时...

...也许您想知道各种映射,管理器和服务如何相互了解。 更重要的是,您可能想知道一个元素如何访问另一个元素的功能。
通过许多不同的方法(例如继承的小部件,IoC容器,DI ...),我个人更喜欢服务定位器。 在这方面,我有一篇关于GetIt的特别文章 -这种方法的实现,但是在这里我将略微谈谈该主题。 通常,您只需在该服务中注册对象一次,然后就可以在整个应用程序中访问它们。 有点单身……但具有更大的灵活性。
使用方法
使用GetIt很明显。 在应用程序的最开始,您注册了您计划随后使用的服务和/或管理器。 将来,只需调用GetIt方法即可访问已注册类的实例。
一个不错的功能是,您可以像具体实现一样注册接口或抽象类。 访问实例时,只需使用接口/抽象,即可在注册期间轻松替换必要的实现。 这使您可以轻松地将实际服务切换到MockService。
一点练习
我通常通过全局变量在名为service_locator.dart的文件中初始化SeviceLocator。 这样就为整个项目获得了一个全局变量。
每当您要访问时,只需致电
RegistrationType object = sl.get<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
定义为
如果要将命令的结果直接传递到StreamBuilder,则.results
Observable尤其有用。 这将根据命令的状态显示不同的内容,并且与rx_widgets包中的RxLoader一起使用时效果很好。 这是一个使用.results
Observable的RxLoader小部件示例:
Expanded(
创建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() {
您可以将不同的RxCommands组合在一起。 请注意, switchedChangedCommand
实际上是updateWeatherCommand
的Observable 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, ),
典型使用模式
我们已经看到了一种使用CommandResults响应命令的不同状态的方法。 在我们要显示命令是否成功(但不显示结果)的情况下,一种常见的模式是在initState
StatefulWidget函数中侦听Observables命令。 这是一个真实项目的示例。
createEventCommand
定义:
RxCommand<Event, void> createEventCommand;
这将在数据库中创建一个Event对象,并且不会返回任何实际值。 但是,正如我们之前所了解的,即使返回类型为void
的RxCommand在完成时也将返回单个数据项。 因此,命令完成后,我们可以使用此行为来触发应用程序中的操作:
@override void initState() {
重要提示 :当我们不再需要订阅时,请不要忘记完成订阅:
@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;
您不需要传递所有处理程序函数。 所有这些都是可选的,因此您可以简单地转移所需的那些。 您只需要在dispose
函数中为RxCommandListener
调用dispose
,它将取消订阅中使用的所有内容。
在另一个真实示例中,让我们比较带有和不RxCommandListener
的相同代码。 在聊天屏幕上,这里可以使用selectAndUploadImageCommand
命令,用户可以在其中上传图像。 调用命令时:
- 显示“图像选择器”对话框。
- 选择图像后加载
- 下载完成后,该命令将返回图像存储地址,以便您可以创建新的聊天条目。
没有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"));
通常,如果有多个Observable,我将始终使用RxCommandListener。
尝试使用RxCommands ,看看它如何使您的生活更轻松。
顺便说一下,您不需要使用RxVMS来利用RxCommands 。
有关RxCommand
更多信息RxCommand
阅读RxCommand
自述包。