[Flutter翻译]让Flutter更具反应性

228 阅读13分钟

本文由 简悦SimpRead 转码,原文地址 www.burkharts.net

当和一些开发者聊到Flutter的反应性时,他说了一句伟大的话:Flutter是RE......。

当与一些开发人员聊到Flutter的反应性时,他说Flutter是REACTive,而不是我们所知道的Reactive Extensions(Rx)意义上的反应性。

来自Xamarin和ReactiveUI,这让我有些失望,因为如果你一旦开始用Rx的方式思考,你就再也不想回去了。

从MVVM到RxVAMS

对我来说,来自Xamarin Forms的MVVM是应用程序的自然架构模式。MVVM解决了很多问题,同时还很容易理解,并且不会引入不必要的组件。

请和我呆一会,我们回到Flutter,我保证

我不会详细描述MVVM,这是这里做得很好

image.png

它基本上由三层组成,其中_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

image.png

引入RxVAMS

我不知道是否有其他人会使用这个名字,但我喜欢这个名字 🙂

还有一个观察结果,让人怀疑MVVM是否是移动App的正确模式。MVVM的目的是在不同的应用程序之间共享_Model_甚至是_ViewModel_的代码,但这种情况真的很少。 看看现实世界中的应用程序,你经常会意识到模型层退化为服务层,将应用程序与世界的其他部分连接起来,而且_VieModels_有越来越多的逻辑,而不是仅仅作为一个适配器。其原因是典型的应用程序并不处理复杂的业务逻辑。所以我提出了以下的应用程序模式。

image.png

这也是将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并在StateinitState方法中订阅一个事件,然后在每个事件上调用setState

使用StreamBuilderStreams代替事件订阅是使之更容易的第一步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框架ReactiveUIReactiveCommand类的启发。更正确地说,它使AppModels能够对来自View的函数调用做出 "反应"。

RxCommand在通过它的一个静态工厂方法创建时需要一个函数。这个方法可以通过使用它的execute方法来调用,也可以直接调用RxCommand对象,因为它是一个可调用的类。这使得直接将命令分配给一个Widget的事件成为可能。

到目前为止没有什么特别之处,但是任何被包装的函数的结果都会从RxCommandresults属性中发射出来,你可能已经期待它是一个Observable。特别是当用RxCommand包装一个异步函数时,.exceute将立即返回,但结果将在函数返回时被发射出来。

除了转移到RxCommands,这次迭代还增加了一个繁忙的旋转器和一个Switch来禁用更新功能。现在的应用程序看起来像这样。

image.png

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>,它决定了命令是否可以被执行。在我们的例子中,这将使updateWeatherCommandSwitch的任何变化做出自动反应。

在视图方面,命令被直接作为处理函数使用,这是因为它们是可调用的类。

在'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.isExecutingStreamBuilder来添加一个繁忙的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,我们的应用程序现在看起来像这样。

image.png

通过使用Streams或更好的RxDart的ObservablesStreamBuilderRxCommands以及rx_widgets相结合,可以使你的应用程序真正的反应。

如果你迷上了事件流的概念,请为这个Flutter issue投票,让Flutter使用Dart Streams进行通知。

请联系我。


www.deepl.com 翻译