关于flutter:Flutter状态管理-BLoCScopedModel和Provider的对比

40次阅读

共计 12339 个字符,预计需要花费 31 分钟才能阅读完成。

Flutter 的运行也是基于状态的变动触发绘制的。所以,Flutter 开发个别是离不开这个主题的。

最常见的就是应用 StatefulWidgetsetState。然而,这样的用法无奈满足日渐增长的页面数量和暗藏在这些页面里的越来越简单的业务逻辑。于是,各路大神开发除了与之配套的模式和响应的库来简化 App 的状态治理。其中最显著的几个模式别离是 BLoC、ScopedModel 和 Provider。上面咱们就一一的剖析和比照他们的异同。以此来帮忙开发者抉择适合的模式和库。

示例

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

运行的成果是齐全一样的,只是在 Provider 的局部还是少许做了一点批改。本例应用的代码是为了表明 Provider 的一些根底用法。同一套代码适配到不同的模式下,才更有比照的价值。实现同一个性能,在不同的模式下该如何操作,不同点、共同点都特地显著。

笔者也是初学者,对各种模式的了解不免有不到位的中央。欢送各位读者指出谬误,或者一起探讨。

BLoC

这是一个模式,也有对应库。它最显著的特点就是有“流”。所以,要应用 BLoC 就要学会

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

应用流控制器解决数据

Dart 提供了一个叫做 StreamController 的类来治理流(Stream)。流控制器(StreamController)会放出一个两个成员来共开发者应用,别离能够读取流外面的值,或者向流增加数据。开发者能够通过StreamController#Stream 实例来读取数据,通过StreamController#Sink` 实例来增加数据。

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

/// 这里去掉了不必要的代码
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) 增加数据。

在示例化流控制器的时候,是这样做的: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 很多深度只是也还在摸索中。欢送拍砖!

参考

https://github.com/flutter/sa…
https://www.raywenderlich.com…
https://medium.com/flutter-co…

正文完
 0