Flutter状态管理 -- BLoC、ScopedModel和Provider的对比

Flutter的运行也是基于状态的变化触发绘制的。因此,Flutter开发通常是离不开这个主题的。html

最多见的就是使用StatefulWidgetsetState。可是,这样的用法没法知足日渐增加的页面数量和隐藏在这些页面里的愈来愈复杂的业务逻辑。因而,各路大神开发除了与之配套的模式和响应的库来简化App的状态管理。其中最显著的几个模式分别是BLoC、ScopedModel和Provider。下面咱们就一一的分析和对比他们的异同。以此来帮助开发者选择合适的模式和库。git

示例

本文中所使用的示例是Flutter Sample的Provider shopper, 这里能够看到。运行效果是这样的:github

运行的效果是彻底同样的,只是在Provider的部分仍是少量作了一点修改。本例使用的代码是为了代表Provider的一些基础用法。同一套代码适配到不一样的模式下,才更有对比的价值。实现同一个功能,在不一样的模式下该如何操做,不一样点、共同点都特别明显。算法

笔者也是初学者,对各类模式的理解不免有不到位的地方。欢迎各位读者指出错误,或者一块儿探讨。编程

BLoC

这是一个模式,也有对应库。它最显著的特色就是有“流”。因此,要使用BLoC就要学会数组

说道流就会有不少的读者想到响应式编程。没错这确实是响应式编程的概念,不过Dart有本身的一套流的实现。咱们来具体关注一下Dart的实现。这里补充一点,若是你想用ReactiveX的一套实现也是没有问题的。markdown

使用流控制器处理数据

Dart提供了一个叫作StreamController的类来管理流(Stream)。流控制器(StreamController)会放出一个两个成员来共开发者使用,分别能够读取流里面的值,或者向流添加数据。开发者能够经过StreamController#Stream实例来读取数据,经过StreamController#Sink`实例来添加数据。网络

在一个ViewModel里如何使用流控制器:app

/// 这里去掉了没必要要的代码
class CartBloc extends BaseBloc {
  // 实例化流控制器
  final _controller = StreamController<Item>.broadcast();
  // Stream直接做为public属性暴露出去
  Stream<Item> get stream => _controller.stream;

  void addItem(Item item) {
    // 使用Sink添加数据
    _controller.sink.add(item);
  }

  @override
  void dispose() {
    // 关闭流控制器,释放资源
    _controller.close();
  }
}
复制代码

在这个类里面,首先示例话了一个流控制器:final _controller = StreamController<Item>.broadcast();。声明了一个 使用了一个stream getter:Stream<Item> get stream => _controller.stream;把流暴露给外面使用。同时有一个方法addItem用来接收新添加的数据,并在其内部实现里使用_controller.sink.add(item)添加数据。less

在示例化流控制器的时候,是这样作的:StreamController<Item>.broadcast()。使用到了broadcast()。这里也能够是stream()。可是stream仅支持一个监听者,若是存在多个监听者的时候就会抛异常了。因此,通常都是使用stream()得到流控制器实例,若是有多个监听者的时候再使用broadcast()。简单说,就是一直用stream()直到出现多个监听者报错的时候换boradcast()

streamsink基本上能够理解为一个管子的两头。使用sink给这个管子假数据,数据流过这个管子以后能够经过stream拿到数据。

使用StreamBuilder显示流数据

流控制器处理好数据以后,就要在界面上把数据展示出来。

Flutter提供了StreamBuilder来展现流的数据。代码以下:

Widget build(BuildContext context) {
    return Scaffold(
        // StreamBuilder,须要一个stream,和一个builder
        body: StreamBuilder<CatalogModel>(
            stream: BlocProvider.of<CatalogBloc>(context).stream,
            builder: (context, snapshot) {
              // 数据能够从snapshot.data拿到
              CatalogModel catalog = snapshot.data;

              return CustomScrollView(
                // 此处省略
              );
            }));
  }
复制代码

使用StreamBuilder只须要给它一个Stream和一个Builder方法便可。在获取每一个传入给StreamBuilder的Stream的时候还有更加简化的方法。

本文使用了Flutter - BLoC模式入门所介绍的方法来实现Stream和StreamBuilder的衔接。或者能够说使用了上文所述的方法简化了在Widget里获取流的方法。而没有使用BLoC库来简化。固然,有兴趣的话你能够试着用bloc库从新实现一次上面的例子。

是先BLoC的总体流程

在前面的描述中,只是充电介绍了和BLoC直接相关的内容:流和StreamBuilder。若是要真正的开发一个App通常遵循的是MVVM的模式。

在定义ViewModel的时候须要控制粒度。由于,你不想一个简单的数据变化让整个页面都进入绘制周期,粒度控制通常是只让有关联的最小组件树从新绘制。通常是一个页面一个ViewModel,固然能够更小到若是网络请求,loading,数据展现都在一个按钮的的话,那么这个ViewModel也能够只在这个按钮上使用。

首先,要有实体类。这样能够结构化的把数据展现出来。

class CartModel {
  /// The private field backing [catalog].
  CatalogModel _catalog;

  /// Internal, private state of the cart. Stores the ids of each item.
  final List<int> _itemIds = [];

  /// The current catalog. Used to construct items from numeric ids.
  CatalogModel get catalog => _catalog;

  set catalog(CatalogModel newCatalog) {
    assert(newCatalog != null);
    assert(_itemIds.every((id) => newCatalog.getById(id) != null),
        'The catalog $newCatalog does not have one of $_itemIds in it.');
    _catalog = newCatalog;
  }

  /// List of items in the cart.
  List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();

  /// The current total price of all items.
  int get totalPrice =>
      items.fold(0, (total, current) => total + current.price);

  void add(Item item) {
    _itemIds.add(item.id);
  }
}
复制代码

定义ViewModel,并使用StreamBuilder展现数据

简洁版的方式在上文中已经有提到过了。在ViewModel中定义也无逻辑相关的部分,以及:

  • 暴露流给Widget使用
  • 在更新数据的方法中使用Sink添加数据
  • 释放资源
使用BlocProvider方便得到ViewModel

在Widget树种,StreamBuilder常常出如今接近叶子节点的部分,也就是在Widget树比较深的部分。最直接的表现就是它会出如今很是分散的文件中。每一个StreamBuilder都须要ViewModel提供的流来展现数据。那么流的声明也要随着StreamBuilder出如今这些分散的文件中。更让代码难以维护的是,ViewModel实例将会从Widget树的根部一直传递到每一个StreamBuilder。

BlockProvider正式来解决这个问题的,它就是胶水,让ViewModel里的流和StreamBuilder更好的结合在一块儿。在Widget中使用StreamBuilder如何可以让子Widget树方便的得到已经实例化好的ViewModel呢?

先来看看这个胶水怎么起做用的。在main.dart里:

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    // In this app, catalog will never change.
    // But if catalog changes, the new catalog would pass through `snapshot`.
    return BlocProvider<CatalogBloc>(
      bloc: CatalogBloc(),
      child: BlocProvider<CartBloc>(
        bloc: CartBloc(),
        child: MaterialApp(
          title: 'Provider Demo',
          theme: appTheme,
          initialRoute: '/',
          routes: {
            '/': (context) => MyLogin(),
            '/catalog': (context) => MyCatalog(),
            '/cart': (context) => MyCart(),
          },
        ),
      ),
    );
  }
}
复制代码

每一个BlocProvider初始化的时候须要一个ViewModel和一个child,子组件。多个BlocProvider能够嵌套使用。在须要用到ViewModel实例的流的时候只须要一个静态方法就能够完成。

body: StreamBuilder<CatalogModel>(
    stream: BlocProvider.of<CatalogBloc>(context).stream,
    builder: (context, snapshot) {
        return CustomScrollView(
        );
}));
复制代码

只须要BlocProvider.of<CatalogBloc>(context)就能够得到ViewModel实例,同时就能够直接拿到stream了。

最后,为何BlocProvider用到StatefulWidget呢?在本例中是为了可使用这个类的dispose方法。

class _BlocProviderState extends State<BlocProvider> {
  @override
  Widget build(BuildContext context) => widget.child;

  @override
  void dispose() {
    widget.bloc.dispose();
    super.dispose();
  }
}
复制代码

原理和本文的关系不是很大,有兴趣的同窗能够移步blocs/bloc_provider.dart

ScopedModel

在开始ScopedModel以前先作一下回顾。流在BLoC模式中的做用就是使用Sink接受数据的变化,再经过Stream结合StreamBuilder展示在界面上,从而达到状态管理的效果。ScopedModel也有相似的机制。只是更加简单,没有用到流,那么对于初学者来讲也就不须要花时间去另外学习流的知识。

通用的开发模式也是MVVM。在咱们定义好与网络请求、本地存储对应的实体类以后就能够定义VM了。

在ScopedModel里咱们用了scoped_model库。在每一个VM里继承Model以后就拥有了出发状态变动的能力。

import 'package:scoped_model/scoped_model.dart';

class BaseModel extends Model {}


import 'base_model.dart';
// 略掉了其余的impoort

class CartModel extends BaseModel {
  // 略掉部分红员定义

  set catalog(CatalogModel newCatalog) {
    // 通知状态变动
    notifyListeners();
  }

  void addItem(Item item) {
    assert(_cartInfo != null);

    // 通知状态变动
    notifyListeners();
  }
}
复制代码

上面的例子中,首先定义了一个BaseModel,每一个对应的VM继承BaseModel以后能够在数据发生变动的时候使用notifyListeners方法来通知状态发生了变化。

看起来在View Model的定义上简化了不少。那么状态的变化如何体如今界面上呢?咱们来看一下scoped_model_tutorial/lib/main.dart

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return ScopedModel<CatalogModel>(
      model: CatalogModel(),
      child: ScopedModel<CartModel>(
        model: CartModel(),
        child: MaterialApp(
            // 略
        ),
      ),
    );
  }
}
复制代码

提供View Model对象的方式基本同样,并且都存在嵌套的问题,至少是写法上。

代替StreamBuilder组件的就是ScopedModelDescendant组件了。

class MyCatalog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(body: ScopedModelDescendant<CatalogModel>(builder: (context, child, model) {
      CatalogModel catalog = model;
     
      return CustomScrollView(
        // 略
      );
    }));
  }
}
复制代码

ScopedModelDescendant接受一个类型参数和一个builder方法,在这个方法的三个参数中,第三个就是类型参数的model实例。

若是不是在组成界面的时候须要用到model的实例要如何处理呢?看代码:

final cartBloc = ScopedModel.of<CartModel>(context);
复制代码

只须要ScopedModel.of<CartModel>()方法便可。

ScopedModel使用notifyListeners()方法简化掉了BLoC模式中须要用到的流。只是在为界面提供ViewModel实例的时候依然没有摆脱嵌套的写法。下面来看下Provider模式能为开发者带来什么。

Provider

Provider模式里发起状态变动的依然是ViewModel里的notifyListeners方法。咱们来看一下具体的实现步骤:

首先,咱们要考虑引入Provider库了。具体步骤能够参考这里的文档

接着来实现ViewModel。好比有一个CartModel,能够写成:

import 'catalog.dart';

class CartModel extends ChangeNotifier {
  CatalogModel _catalog;
  final List<int> _itemIds = [];
  CatalogModel get catalog => _catalog;

  set catalog(CatalogModel newCatalog) {
    _catalog = newCatalog;
    notifyListeners();
  }

  List<Item> get items => _itemIds.map((id) => _catalog.getById(id)).toList();

  int get totalPrice =>
      items.fold(0, (total, current) => total + current.price);

  void add(Item item) {
    _itemIds.add(item.id);
    notifyListeners();
  }
}
复制代码

这里的ViewModel的实现很是之简单,只须要继承ChangeNotifier就能够获得notifyListeners方法。在须要改变状态的地方调用这个方法便可。

把ViewModel粘到Widget树里。这部分须要关注一下lib/main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Using MultiProvider is convenient when providing multiple objects.
    return MultiProvider(
      providers: [
        Provider(create: (context) => CatalogModel()),
        ChangeNotifierProxyProvider<CatalogModel, CartModel>(
          create: (context) => CartModel(),
          update: (context, catalog, cart) {
            cart.catalog = catalog;
            return cart;
          },
        ),
      ],
      child: MaterialApp(
        title: 'Provider Demo',
          // 略
      ),
    );
  }
}
复制代码

在界面中显示数据。有两种方法, 一种是使用Consumer,另外一种是使用Provider.of()方法:

使用Consumer的方式

Consumer<CartModel>(
    builder: (context, cart, child) =>
        Text('\?{cart.totalPrice}', style: hugeStyle)
)
复制代码

Consumer会把ViewModel的实例传入到它的builder方法里。也就是上例中builder方法的第二个参数。这个时候再ViewModel发生变化的时候Consumer和它下面的子树就回重绘。

使用Provider.of()的方式: 在Consumer内部实现也是用的这个方式。代码以下:

class Consumer<T> extends SingleChildStatelessWidget {

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    return builder(
      context,
      // 这里使用了`Provider.of`方法
      Provider.of<T>(context),
      child,
    );
  }
}
复制代码

在使用这个方式的时候须要注意一点,在传递参数的时候考虑到只是须要获取这个view model实例,那么就须要屏蔽掉默认的注册行为,因此是这么用的:

var cart = Provider.of<CartModel>(context, listen: false);
复制代码

listen: false就是用来屏蔽注册组件这个默认行为的。咱们要屏蔽的功能就是Consumer所拥有的的,在状态变化以后重绘的功能。

这里有一个默认的,或者说是约定的作法。若是须要Provider下的某个子树在状态变化以后重绘,那么将这个子树放在Consumer组件下。若是只是把view model实例的数据读出来,或者触发状态变动,那么就用Provider.of<T>(context, listen: false)。直接在调用的时候屏蔽默认行为。

另外

Provider库还定义了另一种更加简洁的方式。provider库用extension给Context添加了一些方法能够快速的读取view model实例,或者读取的时候并注册组件响应状态更新。

  • context.watch<T>():注册组件响应状态变动
  • context.read<T>():只读取view model实例
  • context.select<T, R>(R cb(T value)):容许组件至相应view model的一个子集的变动

更多能够参考文档

不一样的Provider

最经常使用的Provider都已经出如今上面的例子中了。

每一个App里正常不会只有一个Provider,为了解决这个问题就有了MultiProvider。在providers数组里塞满app用到的provider便可。

MultiProvider(
      providers: [
        Provider(create: (context) => CatalogModel()),
        ChangeNotifierProxyProvider<CatalogModel, CartModel>(
          create: // 略,
          update: // 略,
        ),
      ]
    )
复制代码

它的内部仍是嵌套的,只不过在写法上是一个数组。数组里的provider,从头至尾分别嵌套的从深到浅。

Provider只能提供一个ViewModel实例,无法响应状态的变化。在本例中这么用只是代表CartCatalog有依赖。

ChangeNotifierProvider

这是最经常使用的一个provider类型,它的做用就是让view model的变化能够反映在界面上。只要在view model类里继承ChangeNotifier(做为mixin使用亦可),并在修改数据的方法里使用notifyListeners()方法。

ProxyProvider

当两个view model之间存在依赖关系的时候使用这个类型的provider。

ChangeNotifierProxyProvider

前两个类型的和就是ChangeNotifierProxyProvider。也是咱们在上面的代码里使用的provider类型。本类型和ProxyProvider的不一样之处在,本类型会发送更新到ChangeNotifierProviderProxyProvider会把更新发送给Provider

最重要的是,ProxyProvider不会监放任何的变化,而ChangeNtofierProxyProvider能够。

StreamProvider

StreamProvider能够简单的理解为是对StreamBulder的一层封装。如:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StreamProvider<MyModel>( // <--- StreamProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => getStreamOfMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( // <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( // <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Stream<MyModel> getStreamOfMyModel() { // <--- Stream
  return Stream<MyModel>.periodic(Duration(seconds: 1),
          (x) => MyModel(someValue: '$x'))
      .take(10);
}

class MyModel { // <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  void doSomething() {
    someValue = 'Goodbye';
    print(someValue);
  }
}
复制代码

FutureProvider

FutureProvider也是对FutureBuilder的一层封装。如:

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureProvider<MyModel>( // <--- FutureProvider
      initialData: MyModel(someValue: 'default value'),
      create: (context) => someAsyncFunctionToGetMyModel(),
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: Text('My App')),
          body: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[

              Container(
                padding: const EdgeInsets.all(20),
                color: Colors.green[200],
                child: Consumer<MyModel>( // <--- Consumer
                  builder: (context, myModel, child) {
                    return RaisedButton(
                      child: Text('Do something'),
                      onPressed: (){
                        myModel.doSomething();
                      },
                    );
                  },
                )
              ),

              Container(
                padding: const EdgeInsets.all(35),
                color: Colors.blue[200],
                child: Consumer<MyModel>( // <--- Consumer
                  builder: (context, myModel, child) {
                    return Text(myModel.someValue);
                  },
                ),
              ),

            ],
          ),
        ),
      ),
    );
    
  }
}

Future<MyModel> someAsyncFunctionToGetMyModel() async { // <--- async function
  await Future.delayed(Duration(seconds: 3));
  return MyModel(someValue: 'new data');
}

class MyModel { // <--- MyModel
  MyModel({this.someValue});
  String someValue = 'Hello';
  Future<void> doSomething() async {
    await Future.delayed(Duration(seconds: 2));
    someValue = 'Goodbye';
    print(someValue);
  }
}
复制代码

StreamProviderFutureProvider都是对于某些特殊状况的定制的Provider,在平时使用Provider模式的时候对于返回数据的Future和Stream状况作专门的处理,可让开发者少些不少自定义代码。

总结

BLoC模式在使用前须要对或者更大的一点说,须要对响应式编程有必定的理解。咱们这里给出的例子还在很是基础的阶段,虽然在尽可能接近产品级别,可是仍是有差距。因此看起来很是简单。若是你想用这个模式,那么最好能多花时间研究一下响应式编程。

ScopedModel已经成为历史。各位也看到,它和Provider的写法很接近。那是由于后者就是从ScopedModel进化来的。ScopedModel已经完成了它的历史使命。

Provider能够说是最简洁的一种模式了。虽然每次都给最小变化子树上加了另外的一个组件。可是结合Flutter号称能够达到亚线性复杂度的构建算法,其实对性能的影响很小。最关键的是,它是加载最小变化子树上的。在某些状况下,若是使用组件以外的一个巨大的状态树,开发者稍有不慎,那么就是很大范围的重绘。这样对开发者驾驭巨大状态树的能力有很高的要求。我的观点是使用Provider也比较省心。

固然笔者水平有限,对Flutter不少深度只是也还在探索中。欢迎拍砖!

参考

github.com/flutter/sam… www.raywenderlich.com/4074597-get… medium.com/flutter-com…

相关文章
相关标签/搜索