Flutter-BLoC-第一讲

本篇已同步到 我的博客 ,欢迎常来。react

【译文】Reactive Programming - Streams - BLoC

本译文介绍Streams、Bloc 和 Reactive Programming 的概念。理论和实践范例。对于做者的我的note没有进行翻译,请自行翻阅原文地址 原文原码。和iOS开发中的RAC类似,本文推荐重点在 <如何基于流出的数据构建Widge>!git

难度:中级github

本文纪实

本译文的原文是在学 BLoC 的 第三方框架 (框架的教程)而看到的推荐连接进入该文章,为了更好的实现Flutter的BLoC而进行的翻译学习,翻译完也到了文章底部居然有推荐中文翻译 连接, 那本篇就孤芳自赏吧!也顺便记录下本身的第一篇国外技术译文吧!推荐读者结合原文 看译文效果会更佳。 笔者本文学习目的: 解耦编程

什么是流?

介绍 :为了便于想象Stream的概念,只需考虑一个带有两端的管道,只有一个容许在其中插入一些东西。当你将某物插入管道时,它会在管道内流动并从另外一端流出。后端

在Flutter中api

  • 管道称为 Stream
  • 一般(*)使用StreamController来控制Stream
  • 为了插入东西到Stream中,StreamController公开了"入口"名为StreamSink,能够sink属性进行访问你
  • StreamController经过stream属性公开了Stream的出口

注意: (*):我故意使用术语"一般",由于极可能不使用任何StreamController。可是,正如你将在本文中阅读的那样,我将只使用StreamControllers。markdown

Stream能够传递什么?

全部类型值均可以经过流传递。从值,事件,对象,集合,映射,错误或甚至另外一个流,能够由stream传达任何类型的数据。架构

我怎么知道Stream传递的东西?

当你须要通知Stream传达某些内容时,你只须要监听StreamControllerstream属性。app

定义监听器时,你会收到StreamSubscription对象。经过StreamSubscription对象,你将收到由Stream发生变化而触发通知。框架

只要有至少一个活动 监听器,Stream就会开始生成事件,以便每次都通知活动的 StreamSubscription对象:

  • 一些数据来自流,
  • 当一些错误发送到流时,
  • 当流关闭时。

StreamSubscription对象也能够容许如下操做:

  • 中止听
  • 暂停,
  • 恢复。

Stream只是一个简单的管道吗?

不,Stream还容许在流出以前处理流入其中的数据。

为了控制Stream内部数据的处理,咱们使用StreamTransformer,它只是

  • 一个“捕获” Stream内部流动数据的函数
  • 对数据作一些处理
  • 这种转变的结果也是一个Stream

你将直接从该声明中了解到,能够按顺序使用多个StreamTransformer。

StreamTransformer能够用进行任何类型的处理,例如:

  • 过滤(filtering):根据任何类型的条件过滤数据,
  • 从新组合(regrouping):从新组合数据,
  • 修改(modification):对数据应用任何类型的修改,
  • 将数据注入其余流,
  • 缓冲,
  • 处理(processing):根据数据进行任何类型的操做/操做,
  • ...

Stream流的类型

Stream有两种类型。

单订阅Stream

这种类型的Stream只容许在该Stream的整个生命周期内使用单个监听器。

即在第一个订阅被取消后,也没法在此类流上收听两次。

广播流

第二种类型的Stream容许任意数量的监听器。

能够随时向广播流添加监听器。新的监听器将在它开始收听Stream时收到事件。

基本的例子

任何类型的数据

第一个示例显示了“单订阅” 流,它只是打印输入的数据。你可能会看到可有可无的数据类型。

streams_1.dart

import 'dart:async';

void main() {
  //
  // 初始化“单订阅”流控制器
  //
  final StreamController ctrl = StreamController();
  
  //
   //初始化一个只打印数据的监听器
  //一收到它
  //
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  //
  // 咱们在这里添加将会流进Stream中的数据
  //
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  //
  // 咱们发布了StreamController
  //
  ctrl.close();
}
复制代码
StreamTransformer

第二个示例显示“ 广播 ” 流,它传达整数值并仅打印偶数。为此,咱们应用StreamTransformer来过滤(第14行)值,只让偶数通过。

import 'dart:async';

void main() {
  //
  // Initialize a "Broadcast" Stream controller of integers
  //
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  //
  // Initialize a single listener which filters out the odd numbers and
  // only prints the even numbers
  //
  final StreamSubscription subscription = ctrl.stream
					      .where((value) => (value % 2 == 0))
					      .listen((value) => print('$value'));

  //
  // We here add the data that will flow inside the stream
  //
  for(int i=1; i<11; i++){
  	ctrl.sink.add(i);
  }
  
  //
  // We release the StreamController
  //
  ctrl.close();
}
复制代码

RxDart

所述RxDart包是用于执行 Dart 所述的ReactiveX API,它扩展了原始达特流 API符合ReactiveX标准。 因为它最初并未由Google定义,所以它使用不一样的词汇表。下表给出了Dart和RxDart之间的相关性。

Dart RxDart
Stream Observable
StreamController Subject

正如刚才所说,RxDart 扩展了原始的Dart Streams API并提供了StreamController的 3个主要变体:

PublishSubject

PublishSubject是普通的广播 StreamController, 有一个例外:Stream返回一个Observable,而不是Stream。

image.png

如你所见,PublishSubject仅向监听器发送在订阅以后添加到Stream的事件。

BehaviorSubject

该BehaviorSubject也是广播 StreamController,它返回一个Observable,而不是Stream

image.png

与PublishSubject的主要区别在于BehaviorSubject还将最后发送的事件发送给刚刚订阅的监听器。

ReplaySubject

ReplaySubject 也是一个广播StreamController,它返回一个Observable,而不是Stream。

image.png

默认状况下,ReplaySubjectStream已经发出的全部事件做为第一个事件发送给任何新的监听器。

关于资源的重要说明

常常释放再也不须要的资源是一种很是好的作法。 本声明适用于:

  • StreamSubscription - 当你再也不须要监听Stream时,取消订阅;
  • StreamController - 当你再也不须要StreamController时,关闭它;
  • 这一样适用于RxDart主题,当你再也不须要BehaviourSubject,PublishSubject ...时,请将其关闭。

如何基于由Stream提供的数据构建Widget?(重点)

Flutter提供了一个很是方便的StatefulWidget,名为StreamBuilder

StreamBuilder监听Stream,每当某些数据输出Stream时,它会自动重建,调用其builder callback。

这是如何使用StreamBuilder:

StreamBuilder<T>(
    key: ...optional, the unique ID of this Widget...
    stream: ...the stream to listen to...
    initialData: ...any initial data, in case the stream would initially be empty...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...the Widget to be built based on snapshot.data
        }
        return ...the Widget to be built if no data is available
    },
)
复制代码

如下示例模仿默认的 “计数器” 应用程序,但使用Stream而再也不使用任何setState。

import 'dart:async';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  final StreamController<int> _streamController = StreamController<int>();

  @override
  void dispose(){
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
      // 咱们正在监听流,每次有一个新值流出这个流时,咱们用该值更新Text ;
        child: StreamBuilder<int>(
          stream: _streamController.stream,
          initialData: _counter,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
        //当咱们点击FloatingActionButton时,增长计数器并经过sink将其发送到Stream;
        //事实上 注入到stream中值会致使监听它(stream)的StreamBuilder重建并 ‘刷新’计数器;
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
复制代码
注意点:
  • 24-30行: 咱们再也不须要state的概念,全部东西都经过Stream接受;

第35行:当咱们点击FloatingActionButton时,咱们递增计数器并经过接收器将其发送到Stream; 在流中注入值的事实致使侦听它的StreamBuilder重建并“刷新”计数器;

  • 这是一个很大的改进,由于实际调用setState()方法的,会强制整个 Widget(和任何子小部件)重建。这里,只有StreamBuilder被重建(固然它的子部件,被streamBuilder包裹的子控件);

  • 咱们仍然在为页面使用StatefulWidget的惟一缘由,仅仅是由于咱们须要经过dispose方法第15行释放StreamController ;

什么是反应式编程?

反应式编程是使用异步数据流进行编程。 换句话说,任何东西好比从事件(例如点击),变量的变化,消息,......到构建请求,可能改变或发生的全部事件的全部内容都将被传送,由数据流触发。

很明显,全部这些意味着,经过反应式编程,应用程序:
  • 变得异步
  • 围绕Streams和listeners的概念进行架构
  • 当某事发生在某处(事件,变量的变化......)时,会向Stream发送通知
  • 若是 "某人" 监听该流(不管其在应用程序中的任何位置),它将被通知并将采起适当的行动.
组件之间再也不存在紧密耦合。

简而言之,当Widget向Stream发送内容时,该Widget 再也不须要知道:

  • 接下来会发生什么
  • 谁可能使用这些信息(没有一个,一个或几个小部件......)
  • 可能使用此信息的地方(无处,同一屏幕,另外一个,几个...)
  • 当这些信息可能被使用时(几乎是直接,几秒钟以后,永远不会......)
  • ...... Widget只关心本身的事业,就是这样!
乍一看,读到这个,这彷佛会致使应用程序“ 没法控制 ”,但正如咱们将看到的,状况正好相反。它给你:
  • 构建仅负责特定活动的部分应用程序的机会
  • 轻松模拟一些组件的行为,以容许更完整的测试覆盖
  • 轻松重用组件(当前应用程序或其余应用程序中的其余位置),
  • 从新设计应用程序,并可以在不进行太多重构的状况下将组件从一个地方移动到另外一个地方,

咱们将很快看到优点......但在我须要介绍最后一个主题以前:BLoC模式。


BLoC 模式

BLoC模式由Paolo Soares 和 Cong Hui设计,并谷歌在2018的 DartConf 首次提出,能够在 YouTube 上观看。

BLoC表示为业务逻辑组件 (Business Logic Component)

简而言之, Business Logic须要:

  • 转移到一个或几个BLoC,
  • 尽量从表示层(Presentation Layer)中删除。换句话说,UI组件应该只关心UI事物而不关心业务
  • 依赖 Streams 独家使用输入(Sink)和输出(stream)
  • 保持平台独立
  • 保持环境独立

事实上,BLoC模式最初被设想为容许独立于平台重用相同的代码:Web应用程序,移动应用程序,后端。

它究竟意味着什么?

BLoC模式 是利用咱们刚才上面所讨论的观念:Streams (流)

image.png

  • Widgets 经过 Sinks 向 BLoC 发送事件(event)
  • BLoC 经过流(stream)通知小部件(widgets)
  • 由BLoC实现的业务逻辑不是他们关注的问题。
从这个声明中,咱们能够直接看到一个巨大的好处。

因为业务逻辑与UI的分离:

  • 咱们能够随时更改业务逻辑,对应用程序的影响最小
  • 咱们可能会更改UI而不会对业务逻辑产生任何影响,
  • 如今,测试业务逻辑变得更加容易。
如何将此 BLoC 模式应用于 Counter 应用程序示例中

将 BLoC 模式应用于此计数器应用程序彷佛有点矫枉过正,但让我先向你展现......

代码: streams_4.dart

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Streams Demo',
        theme: new ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
        child: StreamBuilder<int>(
          stream: bloc.outCounter,
          initialData: 0,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          bloc.incrementCounter.add(null);
        },
      ),
    );
  }
}

class IncrementBloc implements BlocBase {
  int _counter;

  //
  // Stream来处理计数器
  //
  StreamController<int> _counterController = StreamController<int>();
  StreamSink<int> get _inAdd => _counterController.sink;
  Stream<int> get outCounter => _counterController.stream;

  //
  // Stream来处理计数器上的操做
  //
  StreamController _actionController = StreamController();
  StreamSink get incrementCounter => _actionController.sink;

  //
  // Constructor
  //
  IncrementBloc(){
    _counter = 0;
    _actionController.stream
                     .listen(_handleLogic);
  }

  void dispose(){
    _actionController.close();
    _counterController.close();
  }

  void _handleLogic(data){
    _counter = _counter + 1;
    _inAdd.add(_counter);
  }
}
复制代码

我已经听到你说“ 哇......为何这一切?这都是必要的吗?”。

第一 是责任分离
若是你检查CounterPage(第21-45行),其中绝对没有任何业务逻辑。

此页面如今仅负责:

> * 显示计数器,如今只在必要时刷新(即便没有页面必须知道它)
> * 提供按钮,当按下时,将会在counter面板上请求一个动做

此外,整个业务逻辑集中在一个单独的类“ IncrementBloc”中。

若是如今,你须要更改业务逻辑,你只需更新方法_handleLogic(第77-80行)。也许新的业务逻辑将要求作很是复杂的事情...... CounterPage永远不会知道它,这是很是好的!
复制代码
第二 可测试性
如今,测试业务逻辑变得更加容易。

无需再经过用户界面测试业务逻辑。只须要测试IncrementBloc类。
复制代码
第三 自由组织布局
因为使用了Streams,你如今能够独立于业务逻辑组织布局。

能够从应用程序中的任何位置启动任何操做:只需调用.incrementCounter sink便可。

你能够在任何页面的任何位置显示计数器,只需听取.outCounter stream。
复制代码
第四 减小“build”的次数
不使用setState()而是使用StreamBuilder这一事实大大减小了“ 构建 ”的次数,只减小了所需的次数。

从性能角度来看,这是一个巨大的进步。
复制代码
只有一个约束...... BLoC的可访问性

为了让全部这些工做,BLoC须要可访问。

有几种方法能够访问它:

  • 经过全局单例 这种方式颇有简单,但不是真的推荐。此外,因为Dart中没有类析构函数,所以你永远没法正确释放资源。

  • 做为局部变量(本地实例) 你能够实例化BLoC的本地实例。在某些状况下,此解决方案彻底符合某些需求。在这种状况下,你应该始终考虑在StatefulWidget中初始化,以便你能够利用dispose()方法来释放它。

  • 由父类提供 使其可访问的最多见方式是经过祖先 Widget,实现为StatefulWidget。

如下代码显示了通用 BlocProvider的示例。

代码: streams_5

//全部BLoC的通用接口
abstract class BlocBase {
  void dispose();
}

//通用BLoC提供商
class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final T bloc;
  final Widget child;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<BlocProvider<T>>();
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
  @override
 /// 便于资源的释放
  void dispose(){
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return widget.child;
  }
}
复制代码
关于这种通用BlocProvider的一些解释

首先,如何将其做为provider使用?

若是你查看示例代码“ streams_4.dart ”,你将看到如下代码行(第12-15行)

home: BlocProvider<IncrementBloc>(
          bloc: IncrementBloc(),
          child: CounterPage(),
        ),
复制代码

经过这些代码,咱们只需实例化一个新的BlocProvider,它将处理一个IncrementBloc,并将CounterPage做为子项呈现。

从那一刻开始,从BlocProvider开始的子树的任何小部件部分都将可以经过如下代码访问IncrementBloc:

IncrementBloc bloc = BlocProvider.of<IncrementBloc>(context);
复制代码
可使用多个BLoC吗?

固然,这是很是可取的。建议是:

  • (若是有任何业务逻辑)每页顶部有一个BLoC,
  • 为何不是ApplicationBloc来处理应用程序状态?
  • 每一个“足够复杂的组件”都有相应的BLoC。

如下示例代码在整个应用程序的顶部显示ApplicationBloc,而后在CounterPage顶部显示IncrementBloc。

该示例还显示了如何检索两个blocs。

代码 streams_6.dart

void main() => runApp(
  BlocProvider<ApplicationBloc>(
    bloc: ApplicationBloc(),
    child: MyApp(),
  )
);

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    return MaterialApp(
      title: 'Streams Demo',
      home: BlocProvider<IncrementBloc>(
        bloc: IncrementBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context){
    final IncrementBloc counterBloc = BlocProvider.of<IncrementBloc>(context);
    final ApplicationBloc appBloc = BlocProvider.of<ApplicationBloc>(context);
    
    ...
  }
}
复制代码
为何不使用InheritedWidget?

在与BLoC相关的大多数文章中,你会看到经过InheritedWidget实现Provider。

固然,没有什么能阻止这种类型的实现。然而,

  • 一个InheritedWidget没有提供任何dispose方法,记住,在再也不须要资源时老是释放资源是一个很好的作法。
  • 固然,没有什么能阻止你将InheritedWidget包装在另外一个StatefulWidget中,可是,使用 InheritedWidget 增长了什么呢?
  • 最后,若是不受控制,使用InheritedWidget常常会致使反作用(请参阅下面的InheritedWidget上的提醒)。

以上三点解释了我为何选择经过StatefulWidget实现BlocProvider,这样作可让我在Widget dispose时释放相关资源。

Flutter没法实例化泛型类型 不幸的是,Flutter没法实例化泛型类型,咱们必须将BLoC的实例传递给BlocProvider。为了在每一个BLoC中强制执行dispose()方法,全部BLoC都必须实现BlocBase接口。

提醒InheritedWidget

在使用InheritedWidget并经过context.inheritFromWidgetOfExactType(...)来得到指定类型最近的widget, 每次InheritedWidget的父级或者子布局发生变化时,这个方法会自动将当前“context”(= BuildContext)注册到要重建的widget当中。。

请注意,为了彻底正确,我刚才解释的与InheritedWidget相关的问题只发生在咱们将InheritedWidget与StatefulWidget结合使用时。当你只使用没有State的InheritedWidget时,问题就不会发生。可是......我将在下一篇文章 中回到这句话。

连接到BuildContext的Widget类型(Stateful或Stateless)可有可无。

关于BLoC的我的建议

与BLoC相关的第三条规则是:“依赖于Streams的输入(Sink)和输出(stream)的使用优点”。

个人我的经历稍微关系到这个说法......让我解释一下。

首先,BLoC模式被设想为跨平台共享相同的代码(AngularDart,......),而且从这个角度来看,该陈述彻底有意义。

可是,若是你只打算开发一个Flutter应用程序,这是基于个人谦逊经验,有点矫枉过正。

若是咱们坚持声明,没有可能的getter或setter,只有sink和stream。缺点是“全部这些都是异步的”。

让咱们用2个样原本说明缺点:

你须要从BLoC中检索一些数据,以便将这些数据用做应该当即显示这些参数的页面的输入(例如,想一个参数页面),若是咱们不得不依赖Streams,这使得页面的构建异步(这很复杂)。经过Streams使其工做的示例代码可能以下所示......很丑陋不是吗。

代码 streams_7.dart 以下:

class FiltersPage extends StatefulWidget {
  @override
  FiltersPageState createState() => FiltersPageState();
}

class FiltersPageState extends State<FiltersPage> {
  MovieCatalogBloc _movieBloc;
  double _minReleaseDate;
  double _maxReleaseDate;
  MovieGenre _movieGenre;
  bool _isInit = false;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    // 做为initState()级别还没有提供的上下文,若是还没有初始化,咱们将得到过滤器参数列表
    if (_isInit == false){
      _movieBloc = BlocProvider.of<MovieCatalogBloc>(context);
      _getFilterParameters();
    }
  }

  @override
  Widget build(BuildContext context) {
    return _isInit == false
      ? Container()
      : Scaffold(
    ...
    );
  }

  ///
  /// 很是棘手.
  /// 
  /// 因为咱们但愿100%符合BLoC标准,咱们须要使用Streams从BLoCs中检索全部内容......
  /// 
  /// 这很难看,但被视为一个研究案例。
  ///
  void _getFilterParameters() {
    StreamSubscription subscriptionFilters;

    subscriptionFilters = _movieBloc.outFilters.listen((MovieFilters filters) {
        _minReleaseDate = filters.minReleaseDate.toDouble();
        _maxReleaseDate = filters.maxReleaseDate.toDouble();

        // 只需确保订阅已发布
        subscriptionFilters.cancel();
        
        // 如今咱们有了全部参数,咱们能够构建实际的页面
        if (mounted){
          setState((){
            _isInit = true;
          });
        }
      });
    });
  }
}
复制代码

在BLoC级别,您还须要转换某些数据的“假”注入,以触发提供您但愿经过流接收的数据。使这项工做的示例代码能够是:

代码streams_8.dart

class ApplicationBloc implements BlocBase {
  ///
  /// 同步流来处理提供的电影类型
  ///
  StreamController<List<MovieGenre>> _syncController = StreamController<List<MovieGenre>>.broadcast();
  Stream<List<MovieGenre>> get outMovieGenres => _syncController.stream;

  ///
  /// 流处理假命令以经过Stream触发提供MovieGenres列表
  ///
  StreamController<List<MovieGenre>> _cmdController = StreamController<List<MovieGenre>>.broadcast();
  StreamSink get getMovieGenres => _cmdController.sink;

  ApplicationBloc() {
    //
    // 若是咱们经过此接收器接收任何数据,咱们只需将MovieGenre列表提供给输出流
    //
    _cmdController.stream.listen((_){
      _syncController.sink.add(UnmodifiableListView<MovieGenre>(_genresList.genres));
    });
  }

  void dispose(){
    _syncController.close();
    _cmdController.close();
  }

  MovieGenresList _genresList;
}

// Example of external call
BlocProvider.of<ApplicationBloc>(context).getMovieGenres.add(null);
复制代码

我不知道你的意见,但就我的而言,若是我没有任何与代码移植/共享相关的限制,我发现这过重了,我宁愿在须要时使用常规的getter / setter并使用Streams / Sinks来保持分离责任并在须要的地方广播信息,这很棒。

如今是时候在实践中看到这一切......

正如本文开头所提到的,我构建了一个伪应用程序来展现如何使用全部这些概念。 完整的源代码能够在 Github 上找到。

请谅解,由于这段代码远非完美,可能更好和/或更好的架构,但惟一的目标只是向您展现这一切是如何工做的。

因为源代码太多不少,我只会解释主要的几条。

电影目录的来源

我使用免费的TMDB API来获取全部电影的列表,以及海报,评级和描述。

为了可以运行此示例应用程序,您须要注册并获取API密钥(彻底免费),而后将您的API密钥放在文件“/api/tmdb_api.dart”第15行。

应用程序的架构以下:

该应用程序使用到了:

3个主要的BLoC:

    1. ApplicationBloc(在全部内容之上),负责提供全部电影类型的列表;
  • 2.FavoriteBloc(就在下面),负责处理“收藏夹”的概念;
  • 3.MovieCatalogBloc(在2个主要页面之上),负责根据过滤器提供电影列表;

6个页面:

  • 1.HomePage:登录页面,容许导航到3个子页面;
  • 2.ListPage:将电影列为GridView的页面,容许过滤,收藏夹选择,访问收藏夹以及在后续页面中显示电影详细信息;
  • 3.ListOnePage:相似于ListPage,但电影列表显示为水平列表,下面是详细信息;
    1. FavoritesPage:列出收藏夹的页面,容许取消选择任何收藏夹;
  • 5.* Filters:容许定义过滤器的EndDrawer:流派和最小/最大发布日期。从ListPage或ListOnePage调用此页面;
  1. Details*详细信息:页面仅由ListPage调用以显示电影的详细信息,但也容许选择/取消选择电影做为收藏;

1个子BLoC:

  • 1.FavoriteMovieBloc,连接到MovieCardWidget或MovieDetailsWidget,以处理做为收藏的电影的选择/取消选择

5个主要Widget:

  • 1.FavoriteButton:负责显示收藏夹的数量,实时,并在按下时重定向到FavoritesPage;
  • 2.FavoriteWidget:负责显示一个喜欢的电影的细节并容许其取消选择;
  • 3.FiltersSummary:负责显示当前定义的过滤器;
  • 4.MovieCardWidget:负责将一部电影显示为卡片,电影海报,评级和名称,以及一个图标,表示该特定电影的选择是最喜欢的;
  • 5.MovieDetailsWidget:负责显示与特定电影相关的详细信息,并容许其选择/取消选择做为收藏。
不一样BLoCs / Streams的编排

下图显示了如何使用主要3个BLoC:

  • 在BLoC的左侧,哪些组件调用Sink
  • 在右侧,哪些组件监听流

例如,当MovieDetailsWidget调用inAddFavorite Sink时,会触发2个stream:

  • outTotalFavorites流强制重建FavoriteButton
  • outFavorites流 强制重建MovieDetailsWidget(“最喜欢的”图标) 强制重建_buildMoieCard(“最喜欢的”图标) 用于构建每一个MovieDetailsWidget

image.png

观察

大多数Widget和Page都是StatelessWidgets,这意味着:

  • 强制重建的setState()几乎从未使用过。 例外状况是: 在ListOnePage中,当用户点击MovieCard时,刷新MovieDetailsWidget。 这也多是由一个stream驱动的...... 在FiltersPage中容许用户在接受筛选条件以前经过Sink更改过筛选条件。
  • 应用程序不使用任何InheritedWidget
  • 该应用程序几乎是100%BLoCs / Streams驱动,这意味着大多数小部件彼此独立,而且它们在应用程序中的位置

一个实际的例子是FavoriteButton,它显示徽章中所选收藏夹的数量。 该应用程序共有3个FavoriteButton实例,每一个实例显示在3个不一样的页面中。

显示电影列表(显示无限列表的技巧说明)

要显示符合过滤条件的电影列表,咱们使用GridView.builder(ListPage)或ListView.builder(ListOnePage)做为无限滚动列表。

电影是经过TMDB API获取的,每次拉取20个。

提醒一下,GridView.builder和ListView.builder都将itemCount做为输入,若是提供了item数量,则表示要根据itemCount的数量来显示列表。itemBuilder的index从0到itemCount - 1不等。

正如您将在代码中看到的那样,我随意为GridView.builder添加了30多个。 理由是,在这个例子中,咱们正在操纵假定的无限数量的项目(这不是彻底正确可是又有谁关心这个例子)。 这将强制GridView.builder请求显示“最多30个”项目。

此外,GridView.builder和ListView.builder只在认为必须在视口中呈现某个项目(索引)时才调用itemBuilder。

MovieCatalogBloc.outMoviesList返回一个List ,它被迭代以构建每一个Movie Card。 第一次,这个List 是空的,可是因为itemCount:... + 30,咱们欺骗系统,它将要求经过_buildMovieCard(...)呈现30个不存在的项目。

正如您将在代码中看到的,此例程对Sink进行了一次奇怪的调用:

//通知MovieCatalogBloc咱们正在渲染MovieCard[index]
movieBloc.inMovieIndex.add(index);
复制代码

这个调用告诉MovieCatalogBloc咱们要渲染MovieCard [index]。

而后_buildMovieCard(...)继续验证与MovieCard [index]相关的数据是否存在。 若是是,则渲染后者,不然显示CircularProgressIndicator。

对StreamCatalogBloc.inMovieIndex.add(index)的调用由StreamSubscription监听,StreamSubscription将索引转换为某个pageIndex数字(一页最多可计20部电影)。 若是还没有从TMDB API获取相应页面,则会调用API。 获取页面后,全部已获取电影的新列表将发送到_moviesController。 当GridView.builder监听该Stream(= movieBloc.outMoviesList)时,后者请求重建相应的MovieCard。 因为咱们如今拥有数据,咱们能够渲染它了。

名单和其余连接 介绍PublishSubject,BehaviorSubject和ReplaySubject的图片由ReactiveX发布。 其余一些有趣的文章值得一读:

Fundamentals of Dart Streams [Thomas Burkhart]

rx_command package [Thomas Burkhart]

Build reactive mobile apps in Flutter - companion article [Filip Hracek]

Flutter with Streams and RxDart [Brian Egan]

总结

很长的文章,但还有更多的话要说,由于对我而言,这是展开Flutter应用程序的方法。 它提供了很大的灵活性。

很快就会继续关注新文章。 快乐写代码。

这篇文章也能够在 Medium -Flutter Community 找到。

本文源码

如需转载本译文,请注明出处.