本文由 简悦SimpRead 转码,原文地址 www.burkharts.net
当和一些开发者聊到Flutter的反应性时,他说了一句伟大的话:Flutter是RE......。
当与一些开发人员聊到Flutter的反应性时,他说Flutter是REACTive,而不是我们所知道的Reactive Extensions(Rx)意义上的反应性。
来自Xamarin和ReactiveUI,这让我有些失望,因为如果你一旦开始用Rx的方式思考,你就再也不想回去了。
从MVVM到RxVAMS
对我来说,来自Xamarin Forms的MVVM是应用程序的自然架构模式。MVVM解决了很多问题,同时还很容易理解,并且不会引入不必要的组件。
请和我呆一会,我们回到Flutter,我保证
我不会详细描述MVVM,这是这里做得很好
它基本上由三层组成,其中_View_由你在屏幕上看到的所有UI元素组成。它也可以包含只涉及UI的逻辑。_Model_包含你的业务逻辑和后端连接。视图模型(ViewModel)是两者之间的粘合剂,它处理和转换来自模型的数据,使之成为视图可以轻松显示的形式。它提供了视图可以调用的函数(通常称为命令),以触发对_Model_的操作,并提供事件,以向视图发出数据变化的信号。
重要的是:ViewModel_对_View_一无所知,这使得它可以进行测试,而且多个View可以使用同一个_ViewModel。一个ViewModel只是向View提供服务(事件、命令)。视图决定它使用哪一个。
一些MVVM框架封装了事件的订阅和调用函数来更新_ViewModel_中的数据,从_View_使用DataBinding。
除了来自ViewModel的数据更新外,要在_View_中触发任何其他的动作都会变得很繁琐,因为如果_View_应该能够检索到事件的结果,你就必须发布事件和函数。
另一个问题是,我们总是在不同的层之间移动状态,必须保持同步。
我们真正想要的是一个应用程序,只需对来自外部的任何事件作出反应,而不必一直处理状态管理。
Rx来拯救我们
如果你以前没有听说过Rx,可以把它想象成事件流,你可以用类似Linq的方式对其进行转换。尽管Rx首先是由微软开发的,但它几乎可以用于任何语言。你可以在reativex.io上找到一个介绍,从那里。
ReactiveX的Observable模型允许你用简单的、可组合的操作来处理异步事件流,就像你对数组等数据项的集合一样。它将你从回调的纠结网络中解放出来,从而使你的代码更可读,更不容易出现错误。
当使用Rx工作时,你以声明的方式定义如何对事件作出反应的规则。这是以一种功能性的方式完成的,以减少任何副作用。应用于MVVM,这意味着层与层之间的所有通信都是通过偶数流(在Rx中称为 "可观察")和反应命令进行的。最重要的是,可观察流在本质上是异步的。
Rx的Dart实现是RxDart。
引入RxVAMS
我不知道是否有其他人会使用这个名字,但我喜欢这个名字 🙂
还有一个观察结果,让人怀疑MVVM是否是移动App的正确模式。MVVM的目的是在不同的应用程序之间共享_Model_甚至是_ViewModel_的代码,但这种情况真的很少。 看看现实世界中的应用程序,你经常会意识到模型层退化为服务层,将应用程序与世界的其他部分连接起来,而且_VieModels_有越来越多的逻辑,而不是仅仅作为一个适配器。其原因是典型的应用程序并不处理复杂的业务逻辑。所以我提出了以下的应用程序模式。
这也是将App分成三层,但用一个包含整个App逻辑的_AppModel_代替了_ViewModel_,而_View_的接口则由反应式命令(下文中为RxCommands)和为视图提供状态变化的Observables组成。 为了对事件做出反应,_View_会订阅这个Observables。
模型被指定的服务层所取代,服务层提供所有连接应用程序和外部的服务(REST APIs, device APIs, databases)。来自_服务层_的数据以 "可观察 "的形式返回,或者以异步函数调用的形式使访问异步化。
如果你不想遵循这种模式,只要在你的脑海中把AppModel替换成ViewModel就可以了。
调用到行动
我做了一个小的演示应用程序来展示下面的实践。它查询REST API的天气数据,并在ListView中显示。我为这篇文章的其余部分的每一步都做了分支,这样你就可以轻松地自己尝试。
非常非常感谢Brian Egan在最后一步中对App的打磨,甚至包括单元和UI测试!。
将AppModel注入到视图中
为了从widget树的任何地方访问一个对象,Flutter提供了InheritedWidget的概念。当添加到widget树中时,"InheritedWidget "可以通过调用其静态的of()方法来访问树中的任何位置。
在 main.dart中。
class TheViewModel extends InheritedWidget
{
final HomePageAppModel theModel;
const TheViewModel({Key key,
@required
this.theModel,
@required
Widget child}) : assert(theModel != null),assert(child != null),
super(key: key, child: child);
static HomePageAppModel of(BuildContext context) => (context.inheritFromWidgetOfExactType(TheViewModel)as TheViewModel).theModel;
@override
bool updateShouldNotify(TheViewModel oldWidget) => theModel != oldWidget.theModel;
}
在这种情况下,.of()并不返回继承的widget,而是直接返回包含的HomePageAppModel实例,因为它是唯一的数据域。
因为我们想让我们的_AppModel_在HomePage的任何地方都能使用,所以我们把它插入到widget树的最顶端。
在 main.dart:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new TheViewModel(
theModel: new HomePageAppModel(),
child:
new MaterialApp(
title: 'Flutter Demo',
home: new HomePage()
),
);
}
}
第1步:使视图具有反应性
用Flutter对来自ViewModel的事件做出反应的典型方法是创建一个StatefulWidget并在State的initState方法中订阅一个事件,然后在每个事件上调用setState。
使用StreamBuilder和Streams代替事件订阅是使之更容易的第一步StreamBuilder将一个流作为输入,并在每次收到新的输入时调用其构建函数。
在 listview.dart中。
Widget build(BuildContext context) {
return new StreamBuilder<List<WeatherEntry>>( // Streambuilder rebuilds its subtree on every item the stream issues
stream: TheViewModel.of(context).newWeatherEvents, //We access our AppwModel through the inherited Widget
builder: (BuildContext context, AsyncSnapshot<List<WeatherEntry>> snapshot) // in Dart Lambdas with body don't use =>
{
// only if we get data
if (snapshot.hasData && snapshot.data.length > 0)
{
return new ListView.builder(
itemCount: snapshot.data.length,
itemBuilder : (BuildContext context, int index) =>
buildRow(context,index,snapshot.data)
);
}
else
{
return new Text("No items");
}
}
);
任何时候有人在这个流中排一个新的List of WeatherEntry,这个ListView就会用最新的数据重新创建。
这发生在homepage_appmodel.dart。
// Subjects are like StreamSinks. You can queue new events that are then published on the .observable property of th subject.
final _newWeatherSubject = new BehaviorSubject<List<WeatherEntry>>() ;
// Only the observable of the Subject gets published
Observable<List<WeatherEntry>> get newWeatherEvents => _newWeatherSubject.observable;
update({String filtertext = ""})
{
_newWeatherSubject
.addStream( WeatherService.getWeatherEntriesForCity(filtertext)
.handleError((error)=> print) // if there is an error while accessing the REST API we just make a debug output
);
}
查询数据和更新是完全解耦的,这使得测试性很容易。
``更新`是通过按下页面上的更新按钮来触发的。
在 homepage.dart:
new MaterialButton(
child:
new Text("Update"), // Watch the Button is again a composition
color: new Color.fromARGB(255, 33, 150, 243),
textColor: new Color.fromARGB(255, 255, 255, 255),
see-> onPressed: TheViewModel.of(context).update
),
以及如果用户在过滤器中输入TextField。
在homepage_appmodel.dart中。
_inputSubject.observable
.debounce( new Duration(milliseconds: 500)) // make sure we start processing if the user make a short pause
.listen( (filterText)
{
update( filtertext: filterText);
});
如果一个新的字符串被排入HomePageAppModel._inputSubject,将调用执行,这发生在这个方法中。
// Callback function that will be registered to the TextFields OnChanged Event
onFilerEntryChanged(String s) => _inputSubject.add(s);
同样,事件和反应是通过Subject和Observable来解耦的,Observable允许进一步的事件处理,如debounce操作,确保只有在给定的时间内没有其他变化时才会发出事件。
所以要使你的视图具有反应性,Streambuilder是关键。到目前为止,除了debounce之外,几乎所有的事情都可以通过使用Dart的Streams而不是Observables来完成。
第二步:添加RxCommands
为了使视图能够以反应式的方式调用函数,我编写了RxCommand包,它在很大程度上受到了.net框架ReactiveUI的ReactiveCommand类的启发。更正确地说,它使AppModels能够对来自View的函数调用做出 "反应"。
RxCommand在通过它的一个静态工厂方法创建时需要一个函数。这个方法可以通过使用它的execute方法来调用,也可以直接调用RxCommand对象,因为它是一个可调用的类。这使得直接将命令分配给一个Widget的事件成为可能。
到目前为止没有什么特别之处,但是任何被包装的函数的结果都会从RxCommand的results属性中发射出来,你可能已经期待它是一个Observable。特别是当用RxCommand包装一个异步函数时,.exceute将立即返回,但结果将在函数返回时被发射出来。
除了转移到RxCommands,这次迭代还增加了一个繁忙的旋转器和一个Switch来禁用更新功能。现在的应用程序看起来像这样。
在 homepage_model.dart:
class HomePageAppModel {
RxCommand<String,List<WeatherEntry>> updateWeatherCommand;
RxCommand<bool,bool> switchChangedCommand;
RxCommand<String,String> textChangedCommand;
HomePageAppModel()
{
// Command expects a bool value when executed and issues the value on its result Observable (stream)
switchChangedCommand = RxCommand.createSync3<bool,bool>((b)=>b);
// We pass the result of switchChangedCommand as canExecute Observable to the upDateWeatherCommand
updateWeatherCommand = RxCommand.createAsync3<String,List<WeatherEntry>>(update,switchChangedCommand.results);
// Will be called on every change of the searchfield
textChangedCommand = RxCommand.createSync3((s) => s);
// handler for results
textChangedCommand.results
.debounce( new Duration(milliseconds: 500)) // make sure we start processing only if the user make a short pause typing
.listen( (filterText)
{
updateWeatherCommand.execute( filterText); // I could omit he execute because RxCommand is a callable class but here it
// makes the intention clearer
});
// Update data on startup
updateWeatherCommand.execute();
}
没有更多的主题和处理函数,只有干净的RxCommands。让我们从textChanged命令开始,一步一步地看。它所包装的函数除了将传递的字符串推送到RxCommand.result观测器外,并没有做任何其他事情,下面的处理程序会监听这个观测器。
在其他Rx实现中,
listen被称为subscribe。由于RxDart是基于Dart的Streams,所以使用了listen。
switchChangedCommand也只是把收到的bool'推到它的result'观测点。它将被分配给 "Switch "部件的 "onChanged "处理程序,并在每次 "Switch "改变其状态时发出一个新的 "bool "值。这可能看起来有点无意义,因为似乎没有人关心这个结果。
但是看一下updateWeatherCommand,我们看到switchChangedCommand.results是作为第二个(可选)参数传递。这就是RxCommand的下一个特征,你可以传递am Observable<bool>,它决定了命令是否可以被执行。在我们的例子中,这将使updateWeatherCommand对Switch的任何变化做出自动反应。
在视图方面,命令被直接作为处理函数使用,这是因为它们是可调用的类。
在'homepage.dart'中
new TextField(
autocorrect: false,
decoration: new InputDecoration(
hintText: "Filter cities",
hintStyle: new TextStyle(color: new Color.fromARGB(150, 0, 0, 0)),
),
style: new TextStyle(
fontSize: 20.0,
color: new Color.fromARGB(255, 0, 0, 0)),
--> onChanged: TheViewModel.of(context).textChangedCommand,),
特殊功能。
RxCommand不仅提供了一个.results可观察变量,还提供了这个可观察变量。
.isExecuting在每次命令的执行状态改变时都会发出一个bool值。因此,在调用execute'后,它将发出true',当封装的函数返回时(即使是异步函数),它将发出`false'。.canExecute在命令的可执行性发生变化时发出一个bool值。这将在命令执行时发出false,但也会反映出canExecute观察值的状态,如果在创建命令时有传递给观察值的话。这样就可以有效地设置多个命令的可执行性规则。- .
thrownExceptions如果在这个观察点上有一个监听器,RxCommand将捕捉被包装的函数所抛出的任何异常,并在这里发射出来。 RxCommand在执行过程中不能再被调用。如果你想在另一个命令B执行时禁用命令A,你只需要在创建A时传递B.isExecuting作为canExecute参数。很酷,不是吗? 🙂
让我们在应用程序中使用这个功能。例如,现在很容易通过使用另一个监听updateWeatherCommand.isExecuting的StreamBuilder来添加一个繁忙的Spinner。
从homepage.dart:
new StreamBuilder<bool>(
stream: TheViewModel.of(context).updateWeatherCommand.isExecuting,
builder: (BuildContext context, AsyncSnapshot<bool> isRunning)
{
// if true we show a buys Spinner otherwise the ListView
if (isRunning.hasData && isRunning.data == true)
{
return new Center(child: new Container(width: 50.0, height:50.0, child: new CircularProgressIndicator()));
}
else
{
return new WeatherListView();
}
})
),
当Brian对这个例子的应用程序做了一些重构时,他提出了一个观点:将所有三个状态(isExecuting, results, error)放在一个流中会很有帮助,因为Flutter通过重建来更新它的UI。使用单独的StreamBuilder可能并不总是理想的。
我完全同意他的观点。对于那些使用绑定并只通过它们更新单个控件的框架,单独的流是一个很好的解决方案。对于flutter,我想在重建过程中根据新的状态在一个地方显示替代的Widgets。这导致了CommandResult<T>类的引入。
/// Combined execution state of an `RxCommand`
/// Will be issued for any statechange of any of the fields
/// During normal command execution you will get this items if directly listening at the command.
/// 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: `the 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);
}
为了得到任何新的CommandResult的通知,你可以直接.listen到命令本身,因为RxCommand现在也实现了Observable接口。
在下一步,我们将利用这一优势。
使用rx_widgets使生活更轻松
使用StreamBuilder是对AppModel事件做出反应的一种方式,但它使Widget树变得有点乱,因为我也很懒,所以我写了rx_widgets包,它包含了与Streams、Observable和RxCommands一起工作的便利函数。
你可以在任何可以使用Stream的地方传递一个Observable,因为
Observables扩展了Stream接口。
目前它包含这些类。
RxSpinner是一个平台感知的忙碌的旋转器,它接受一个流,并在true和false时建立一个运行中的旋转器和替代的Widget。
有了这个,上面的代码就会变成这样。
new RxSpinner(busyEvents: TheViewModel.of(context).updateWeatherCommand.isExecuting,
platform: TargetPlatform.android,
radius: 20.0,
normal: new WeatherListView(),)
由于我们也想处理错误和空数据事件,现在也有一个扩展版本,直接接受RxCommand,并提供三个构建器。
RxLoader<List<WeatherEntry>>(
spinnerKey: AppKeys.loadingSpinner,
radius: 25.0,
commandResults: ModelProvider.of(context).updateWeatherCommand,
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()}")),
),
WidgetSelector根据所提供的Stream发出的bool值,在两个提供的Widgets中建立一个。在我们的应用程序中,我使用它来启用/禁用基于updateWeatherCommand.canExecute可观察的更新Buttton。
在homepage.dart中。
new Expanded(
child:
WidgetSelector(
buildEvents: ModelProvider.of(context).updateWeatherCommand.canExecute, //We access our ViewModel through the inherited Widget
onTrue: RaisedButton(
key: AppKeys.updateButtonEnabled,
child: Text("Update"),
onPressed: ()
{
_controller.clear();
ModelProvider.of(context).updateWeatherCommand();
}
),
onFalse: RaisedButton(
key: AppKeys.updateButtonDisabled,
child: Text("Please Wait"),
onPressed: null,
),
),
),
更多的反应式部件的好东西即将到来。如果你有一个伟大的 "RxWidget "的想法,请开一个问题,甚至更好的是做一个PR。
总结
感谢Brian Egan,我们的应用程序现在看起来像这样。
通过使用Streams或更好的RxDart的Observables与StreamBuilder和RxCommands以及rx_widgets相结合,可以使你的应用程序真正的反应。
如果你迷上了事件流的概念,请为这个Flutter issue投票,让Flutter使用Dart Streams进行通知。
请联系我。