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…

相關文章
相關標籤/搜索