Flutter 應用開發之Bloc模式

基本概念

響應式編程

所謂響應式編程,指的是一種面向數據流和變化傳播的編程範式。使用響應式編程範式,意味着能夠在編程語言中更加方便地表達靜態或動態的數據流,而相關的計算模型會自動將變化的值經過數據流進行傳播。程序員

響應式編程最初的目的是爲了簡化交互式用戶界面的建立和實時系統動畫的繪製而提出來的一種方法,是爲了簡化MVC軟件架構而設計的。在面向對象編程語言中,響應式編程一般以觀察者模式的擴展呈現。還能夠將響應式流模式和迭代器模式比較,一個主要的區別是,迭代器基於」拉「,而響應式流基於」推「。編程

使用迭代器是一種命令式編程,由開發者決定什麼時候去訪問序列中的next()元素。而在響應式流中,與Iterable-Iterator對應的是Publisher-Subscriber。當新的可用元素出現時,發佈者通知訂閱者,這種」推「正是響應的關鍵。此外,應用於推入元素上的操做是聲明式的而不是命令式的:程序員要作的是表達計算的邏輯,而不是描述精準的控制流程。bash

除了推送元素,響應式編程還定義了良好的錯誤處理和完成通知方式。發佈者能夠經過調用next()方法推送新的元素給訂閱者,也能夠經過調用onError()方法發送一個錯誤信號或者調用onComplete()發送一個完成信號。錯誤信號和完成信號都會終止序列。架構

響應式編程很是靈活,它支持沒有值、一個值或n個值的用例(包括無限序列),所以如今大量的應用程序開發都悄然使用這種流行的模式進行開發。app

Stream

在Dart中,Stream和Future是異步編程的兩個核心API,主要用於處理異步或者延遲任務等,返回值都是Future對象。不一樣之處在於,Future用於表示一次異步得到的數據,而Stream則能夠經過屢次觸發成功或失敗事件來獲取數據或錯誤異常。框架

Stream 是 Dart 提供的一種數據流訂閱管理工具,功能有點相似於 Android 中的 EventBus 或者 RxBus,Stream 能夠接收任何對象,包括另一個 Stream。在Flutter的Stream流模型中,發佈對象經過 StreamController 的 sink來添加數據,而後經過 StreamController 發送給 Stream,而訂閱者則經過調用Stream的listen()方法來進行監聽,listen()方法會返回一個 StreamSubscription 對象,StreamSubscription 對象支持對數據流進行暫停、恢復和取消等操做。less

根據數據流監聽器個數的不一樣,Stream數據流能夠分爲單訂閱流和多訂閱流。所謂單訂閱流,指的是整個生命週期只容許存在一個監聽器,若是該監聽器被取消,則不能繼續進行監聽,使用的場景有文件IO流讀取等。而所謂廣播訂閱流,指的是應用的生命週期內容許有多個監聽器,當監聽器被添加後就能夠對數據流進行監聽,此種類型適合須要進行多個監聽的場景。異步

例如,下面是使用Stream的單訂閱模式進行數據監聽的示例,代碼以下。async

class StreamPage extends StatefulWidget {

  StreamPage({Key key}): super(key: key);
  @override
  _StreamPageState createState() => _StreamPageState();
}

class _StreamPageState extends State<StreamPage> {

  StreamController controller = StreamController();
  Sink sink;
  StreamSubscription subscription;

  @override
  void initState() {
    super.initState();
    sink = controller.sink;
    sink.add('A');
    sink.add(1);
    sink.add({'a': 1, 'b': 2});
    subscription = controller.stream.listen((data) => print('listener: $data'));
  }

  @override
  Widget build(BuildContext context) {
    return Center();
  }

  @override
  void dispose() {
    super.dispose();
    sink.close();
    controller.close();
    subscription.cancel();
  }
}
複製代碼

運行上面的代碼,會在控制檯輸出以下日誌信息。編程語言

I/flutter ( 3519): listener: A
I/flutter ( 3519): listener: 1
I/flutter ( 3519): listener: {a: 1, b: 2}

複製代碼

與單訂閱流不一樣,多訂閱流容許有多個訂閱者,而且只要數據流中有新的數據就會進行廣播。多訂閱流的使用流程和單訂閱流同樣,只是建立Stream流控制器的方式不一樣,以下所示。

class StreamBroadcastPage extends StatefulWidget {

  StreamBroadcastPage({Key key}): super(key: key);
  @override
  _StreamBroadcastPageState createState() => _StreamBroadcastPageState();
}

class _StreamBroadcastPageState extends State<StreamBroadcastPage> {

  StreamController controller = StreamController.broadcast();
  Sink sink;
  StreamSubscription subscription;

  @override
  void initState() {
    super.initState();
    sink = controller.sink;
    sink.add('A');
    subscription = controller.stream.listen((data) => print('Listener: $data'));
    sink.add('B');
    subscription.pause();
    sink.add('C');
    subscription.resume();
  }

  @override
  Widget build(BuildContext context) {
    return Center();
  }

  @override
  void dispose() {
    super.dispose();
    sink.close();
    controller.close();
    subscription.cancel();
  }
}
複製代碼

容許上面的代碼,輸出的日誌以下所示。

I/flutter ( 3519): Listener: B
I/flutter ( 3519): Listener: C
複製代碼

不過,單訂閱 Stream 只有當存在監聽的時候才發送數據,廣播訂閱流 則不考慮這點,有數據就發送;當監聽調用 pause 之後,無論哪一種類型的 stream 都會中止發送數據,當 resume 以後,把前面存着的數據都發送出去。

sink 能夠接受任何類型的數據,也能夠經過泛型對傳入的數據進行限制,好比咱們對 StreamController 進行類型指定 StreamController _controller = StreamController.broadcast(); 由於沒有對Sink的類型進行限制,仍是能夠添加除了 int 外的類型參數,可是運行的時候就會報錯,_controller 對你傳入的參數作了類型斷定,拒絕進入。

同時,Stream 中還提供了不少 StremTransformer,用於對監聽到的數據進行處理,好比咱們發送 0~19 的 20 個數據,只接受大於 10 的前 5 個數據,那麼能夠對 stream 以下操做。

_subscription = _controller.stream
    .where((value) => value > 10)
    .take(5)
    .listen((data) => print('Listen: $data'));

List.generate(20, (index) => _sink.add(index));
複製代碼

除了 where、take 外,還有不少 Transformer, 例如 map,skip 等等,讀者能夠自行研究。

在Stream流模型中,當數據源發生變化時Stream會通知訂閱者,從而改變控件狀態,實現頁面的刷新。同時,爲了減小開發者對Stream數據流的干預,Flutter提供了一個StreamBuilder組件來輔助Stream數據流操做,它的構造函數以下所示。

StreamBuilder({
  Key key,
  this.initialData,
  Stream<T> stream,
  @required this.builder,
})

複製代碼

事實上,StreamBuilder是一個用於監控Stream數據流變化並展現數據變化的StatefulWidget組件,它會一直記錄着數據流中最新的數據,當數據流發生變化時會自動調用builder()方法進行視圖的重建。例如,下面是使用StreamController結合StreamBuider對官方的計數器應用進行改進,取代使用setState方式來刷新頁面,代碼以下。

class CountPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return CountPageState();
  }
}

class CountPageState extends State<CountPage> {
  int count = 0;
  final StreamController<int> controller = StreamController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        child: Center(
          child: StreamBuilder<int>(
              stream: controller.stream,
              builder: (BuildContext context, AsyncSnapshot snapshot) {
                return snapshot.data == null
                    ? Text("0")
                    : Text("${snapshot.data}");
              }),
        ),
      ),
      floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.add),
          onPressed: () {
            controller.sink.add(++count);
          }),
    );
  }

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

複製代碼

能夠看到,相比傳統的setState方式,StreamBuilder是一個很大的進步,由於它不須要強行重建整個組件樹和它的子組件,只須要重建StreamBuilder包裹的組件便可。而例子中使用StatefulWidget的緣由是須要在組件的dispose()方法中釋放StreamController對象。

BLoC模式

BLoC簡介

BLoC是Business Logic Component的英文縮寫,中文譯爲業務邏輯組件,是一種使用響應式編程來構建應用的方式。BLoC最先由谷歌的Paolo Soares和Cong Hui設計並開發,設計的初衷是爲了實現頁面視圖與業務邏輯的分離。以下圖所示,是採用BLoC模式的應用程序的架構示意圖。

在這裏插入圖片描述
使用BLoC方式進行狀態管理時,應用裏的全部組件被當作是一個事件流,一部分組件能夠訂閱事件,另外一部分組件則消費事件,BLoC的工程流程下圖所示。
在這裏插入圖片描述

如上圖所示,組件經過Sink向Bloc發送事件,BLoC接收到事件後執行內部邏輯處理,並把處理的結果經過流的方式通知給訂閱事件流的組件。在BLoC的工做流程中,Sink接受輸入,BLoC則對接受的內容進行處理,最後再以流的方式輸出,能夠發現,BLoC又是一個典型的觀察者模式。理解Bloc的運做原理,須要重點關注幾個對象,分別是事件、狀態、轉換和流。

  • 事件:在Bloc中,事件會經過Sink輸入到Bloc中,一般是爲了響應用戶交互或者是生命週期事件而進行的操做。
  • 狀態:用於表示Bloc輸出的東西,是應用狀態的一部分。它能夠通知UI組件,並根據當前狀態重建其自身的某些部分。
  • 轉換:從一種狀態到另外一種狀態的變更稱之爲轉換,轉換一般由當前狀態、事件和下一個狀態組成。
  • 流:表示一系列非同步的數據,Bloc創建在流的基礎之。而且,Bloc須要依賴RxDart,它封裝了Dart在流方面的底層細節實現。

BLoC Widget

Bloc既是軟件開發中的一種架構模式,也是一種軟件編程思想。在Flutter應用開發中,使用Bloc模式須要引入flutter_bloc庫,藉助flutter_bloc提供的基礎組件,開發者能夠快速高效地實現響應式編程。flutter_bloc提供的常見組件有BlocBuilder、BlocProvider、BlocListener和BlocConsumer等。

BlocBuilder

BlocBuilder是flutter_bloc提供的一個基礎組件,用於構建組件並響應組件新的狀態,它一般須要Bloc和builder兩個參數。BlocBuilder與StreamBuilder的做用同樣,可是它簡化了StreamBuilder的實現細節,減小一部分必須的模版代碼。而builder()方法會返回一個組件視圖,該方法會被潛在的觸發屢次以響應組件狀態的變化,BlocBuilder的構造函數以下所示。

const BlocBuilder({
    Key key,
    @required this.builder,
    B bloc,
    BlocBuilderCondition<S> condition,
  })
複製代碼

能夠發現,BlocBuilder的構造函數裏面一共有三個參數,而且builder是一個必傳參數。除了builder和bloc參數外,還有一個condition參數,該參數用於向BlocBuilder提供可選的條件,對builder函數進行縝密的控制。

BlocBuilder<BlocA, BlocAState>(
  condition: (previousState, state) {
    //根據返回的狀態決定是否重構組件
  },
  builder: (context, state) {
    //根據BlocA的狀態構建組件
  }
)
複製代碼

如上所示,條件獲取先前的Bloc的狀態和當前的bloc的狀態並返回一個布爾類型的值。若是condition屬性返回true,那麼將調用state執行視圖的從新構建。若是condition返回false,則不會執行視圖的重建操做。

BlocProvider

BlocProvider是一個Flutter組件,能夠經過BlocProvider.of (context)向其子級提供bloc。實際使用時,它能夠做爲依賴項注入到組件中,從而將一個bloc實例提供給子樹中的多個組件使用。

大多數狀況下,咱們可使用BlocProvider來建立一個新的blocs,並將其提供給其它子組件,因爲blocs是BlocProvider負責建立的,那麼關閉blocs也須要BlocProvider進行處理。除此以外,BlocProvider還可用於向子組件提供已有bloc,因爲bloc並非BlocProvider建立的,因此不能經過BlocProvider來關閉該bloc,以下所示。

BlocProvider.value(
  value: BlocProvider.of<BlocA>(context),
  child: ScreenA(),
);
複製代碼

MultiBlocProvider

MultiBlocProvider是一個用於將多個BlocProvider合併爲一個BlocProvider的組件,MultiBlocProvider一般用於替換須要嵌套多個BlocProviders的場景,從而下降代碼的複雜度、提升代碼的可讀性。例如,下面是一個多BlocProvider嵌套的場景。

BlocProvider<BlocA>(
  create: (BuildContext context) => BlocA(),
  child: BlocProvider<BlocB>(
    create: (BuildContext context) => BlocB(),
    child: BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
      child: ChildA(),
    )
  )
)
複製代碼

能夠發現,示例中BlocA嵌套BlocB, BlocB又嵌套BlocC,代碼邏輯很是複雜且可讀性不好。那若是使用MultiBlocProvider組件就能夠避免上面的問題,改造後的代碼以下所示。

MultiBlocProvider(
  providers: [
    BlocProvider<BlocA>(
      create: (BuildContext context) => BlocA(),
    ),
    BlocProvider<BlocB>(
      create: (BuildContext context) => BlocB(),
    ),
    BlocProvider<BlocC>(
      create: (BuildContext context) => BlocC(),
    ),
  ],
  child: ChildA(),
)
複製代碼

BlocListener

BlocListener是一個接收BlocWidgetListener和可選Bloc的組件,適用於每次狀態更改都須要發生一次的場景。BlocListener組件的listener參數能夠用來響應狀態的變化,能夠用它來處理更新UI視圖以外的其餘事情。與BlocBuilder中的builder操做不一樣,BlocBuilder組件的狀態更改僅會調用一次監聽,而且是一個空函數。BlocListener組件一般用在導航、SnackBar和顯示Dialog的場景。

BlocListener<BlocA, BlocAState>(
  bloc: blocA,
  listener: (context, state) {
    //基於BlocA的狀態執行某些操做
  }
  child: Container(),
)
複製代碼

除此以外,還可使用條件屬性來對監聽器函數進行更加縝密的控制。條件屬性會經過比較先前的bloc的狀態和當前的bloc的狀態返回一個布爾值,若是條件返回true,那麼監聽汗水將會被調用,若是條件返回false,監聽函數則不會被調用,以下所示。

BlocListener<BlocA, BlocAState>(
  condition: (previousState, state) {
    //返回truefalse決定是否須要調用監聽
  },
  listener: (context, state) {
    
  }
)
複製代碼

若是須要同時監聽多個bloc的狀態,那麼可使用MultiBlocListener組件,以下所示。

BlocListener<BlocA, BlocAState>(
MultiBlocListener(
  listeners: [
    BlocListener<BlocA, BlocAState>(
      listener: (context, state) {},
    ),
    BlocListener<BlocB, BlocBState>(
      listener: (context, state) {},
    ),
…
  ],
  child: ChildA(),
)
複製代碼

除此以外,flutter_bloc提供的組件還有BlocConsumer、RepositoryProvider和MultiRepositoryProvider等。 當狀態發生變化時,除了須要更新UI視圖以外還須要處理一些其餘的事情,那麼可使用BlocListener,BlocListener包含了一個listener用以作除UI更新以外的事情,該邏輯不能放到BlocBuilder裏的builder中,由於這個方法會被Flutter框架調用屢次,builder方法應該只是一個返回Widget的函數。

flutter_bloc快速上手

使用flutter_bloc以前,須要先在工程的pubspec.yaml配置文件中添加庫依賴,以下所示。

dependencies:
  flutter_bloc: ^4.0.0
複製代碼

使用flutter packages get命令將依賴庫拉取到本地,而後就可使用flutter_bloc庫進行應用開發了。

下面就經過一個計數器應用程序示例來講明flutter_bloc庫的基本使用流程。在示例程序中有兩個按鈕和一個用於顯示當前計數器值的文本組件,兩個按鈕分別用來增長和減小計數器的值。按照Bloc模式的基本使用規範,首先須要新建一個事件對象,以下所示。

enum CounterEvent { increment, decrement }
複製代碼

而後,新建一個Bloc類,用於對計數器的狀態進行管理,以下所示。

class CounterBloc extends Bloc<CounterEvent, int> {
  
  @override
  int get initialState => 0;

  @override
  Stream<int> mapEventToState(CounterEvent event) async* {
    switch (event) {
      case CounterEvent.decrement:
        yield state - 1;
        break;
      case CounterEvent.increment:
        yield state + 1;
        break;
      default:
        throw Exception('oops');
    }
  }
}
複製代碼

一般,繼承Bloc<Event,State>必須實現initialState()和mapEventToState()兩個方法。其中,initialState()用於表示事件的初始狀態,而mapEventToState()方法返回的是通過業務邏輯處理完成以後的狀態,此方法能夠拿到具體的事件類型,而後根據事件類型進行某些邏輯處理。

爲了方便編寫Bloc文件,咱們還可使用Bloc Code Generator插件來輔助Bloc文件的生成。安裝完成後,在項目上右鍵,並依次選擇【Bloc Generator】-> 【New Bloc】來建立Bloc 文件,以下圖所示。

在這裏插入圖片描述
Bloc Code Generator插件生成的Bloc文件以下:

bloc
 ├── counter_bloc.dart    // 全部business logic, 例如加減操做
 ├── counter_state.dart  // 全部state, 例如Added、Decreased 
 ├── counter_event.dart  // 全部event, 例如Add , Remove 
 └── bloc.dart 
複製代碼

使用Bloc以前,須要在應用的最上層容器中進行註冊,即在MaterialApp組件中註冊Bloc。而後,再使用BlocProvider.of (context)獲取註冊的Bloc對象,經過Bloc處理業務邏輯。接收和響應狀態的變化則須要使用BlocBuilder組件,BlocBuilder組件的builder參數會返回組件視圖,以下所示。

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home:BlocProvider<CounterBloc>(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

class CounterPage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
          return Center(
            child: Text('$count', style: TextStyle(fontSize: 48.0)),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
                counterBloc.add(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
                counterBloc.add(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}
複製代碼

運行上面的示例代碼,當點擊計數器的增長按鈕時就會執行加法操做,而點擊減小按鈕時就會執行減法操做,以下圖所示。

在這裏插入圖片描述

能夠發現,使用flutter_bloc狀態管理框架,不須要調用setState()方法也能夠實現數據狀態的改變,而且頁面和邏輯是分開的,更適合在中大型項目中使用。本文只介紹了Bloc的一些基本知識,詳細狀況能夠查看:Bloc官方文檔

相關文章
相關標籤/搜索