Flutter Bloc狀態管理 簡單上手


我們都知道,Flutter中Widget的狀態控制了UI的更新,比如最常見的StatefulWidget,通過調用setState({})方法來刷新控件。那么其他類型的控件,比如StatelessWidget就不能更新狀態來嗎?答案當然是肯定可以的。前文已經介紹過幾種狀態管理

Stream

StreamDart 提供的一種數據流訂閱管理的"工具",感覺有點像 Android 中的 EventBus 或者 RxBusStream 可以接收任何對象,包括是另外一個 Stream,接收的對象通過 StreamControllersink 進行添加,然后通過 StreamController 發送給 Stream,通過 listen 進行監聽, listen 會返回一個 StreamSubscription 對象, StreamSubscription 可以操作對數據流的監聽,例如 pauseresumecancel 等。

Stream 分兩種類型:

  1. Single-subscription Stream:單訂閱 stream,整個生命周期只允許有一個監聽,如果該監聽 cancel 了,也不能再添加另一個監聽,而且只有當有監聽了,才會發送數據,主要用於文件 IO 流的讀取等。
  2. Broadcast Stream:廣播訂閱 stream,允許有多個監聽,當添加了監聽后,如果流中有數據存在就可以監聽到數據,這種類型,不管是否有監聽,只要有數據就會發送,用於需要多個監聽的情況。
還是看下例子會比較直觀:

class _StreamHomeState extends State<StreamHome> {
  StreamController _controller = StreamController();  // 創建單訂閱類型 `StreamController`
  Sink _sink;
  StreamSubscription _subscription;

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

    _sink = _controller.sink; // _sink 用於添加數據
    // _controller.stream 會返回一個單訂閱 stream,
    // 通過 listen 返回 StreamSubscription,用於操作流的監聽操作
    _subscription = _controller.stream.listen((data) => print('Listener: $data'));

    // 添加數據,stream 會通過 `listen` 方法打印
    _sink.add('A');
    _sink.add(11);
    _sink.add(11.16);
    _sink.add([1, 2, 3]);
    _sink.add({'a': 1, 'b': 2});
  }

  @override
  void dispose() {
    super.dispose();
    // 最后要釋放資源...
    _sink.close();
    _controller.close();
    _subscription.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(),
    );
  }
}

運行后看下控制台的輸出:

果然把所有的數據都打印出來了,前面有說過,單訂閱的 stream 只有當 listen 后才會發送數據,不試試我還是不相信的,我們把 _sink.add 放到 listen 前面去執行,再看控制台的打印結果。居然真的是一樣的,Google 粑粑果然誠不欺我。
接着試下 pauseresume 方法,看下數據如何監聽,修改代碼:
_sink = _controller.sink;
_subscription = _controller.stream.listen((data) => print('Listener: $data'));
_sink.add('A');
_subscription.pause(); // 暫停監聽
_sink.add(11);
_sink.add(11.16);
_subscription.resume(); // 恢復監聽
_sink.add([1, 2, 3]);
_sink.add({'a': 1, 'b': 2});
再看控制台的打印,你們可以先猜下是什么結果,我猜大部分人都會覺得應該是不會有 11 和 11.16 打印出來了。然而事實並非這樣,打印的結果並未發生變化,也就是說,調用 pause 方法后,stream 被堵住了,數據不繼續發送了。


接下來看下廣播訂閱 stream,對代碼做下修改:
StreamController _controller = StreamController.broadcast();  //廣播訂閱 stream
  Sink _sink;
  StreamSubscription _subscription;

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

    _sink = _controller.sink; // _sink 用於添加數據
   
    _sink.add('A');
    _subscription = _controller.stream.listen((data) => print('Listener: $data'));

    // 添加數據,stream 會通過 `listen` 方法打印
    _sink.add(11);
    _subscription.pause();
    _sink.add(11.16);
    _subscription.resume();

    _sink.add([1, 2, 3]);
    _sink.add({'a': 1, 'b': 2});
  }

  @override
  void dispose() {
    super.dispose();
    // 最后要釋放資源...
    _sink.close();
    _controller.close();
    _subscription.cancel();
  }

我們再看下控制台的打印:

總結:

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

sink 可以接受任何類型的數據,也可以通過泛型對傳入的數據進行限制,比如我們對 StreamController 進行類型指定 StreamController<int> _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));

那么打印出來的數據如下圖:

除了  wheretake 還有很多  Transformer, 例如  mapskip 等等,小伙伴們可以自行研究。了解了  Stream 的基本屬性后,就可以繼續往下了~

  我們上面已經說了,Stream的特性就是當數據源發生變化的時候,會通知訂閱者,那么我們是不是可以延展一下,實現當數據源發生變化時,改變控件狀態,通知控件刷新的效果呢?Flutter為我們提供了 StreamBuilder
  所以,StreamBuilder是Stream在UI方面的一種使用場景,通過它我們可以在非StatefulWidget中保存狀態,同時在狀態改變時及時地刷新UI。

StreamBuilder

StreamBuilder其實是一個StatefulWidget,它通過監聽Stream,發現有數據輸出時,自動重建,調用builder方法。前面提到了 stream 通過 listen 進行監聽數據的變化,Flutter 就為我們提供了這么個部件 StreamBuilder 專門用於監聽 stream 的變化,然后自動刷新重建。接着來看下源碼

StreamBuilder<T>(
    key: ...可選...
    stream: ...需要監聽的stream...
    initialData: ...初始數據,否則為空...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...基於snapshot.hasData返回的控件
        }
        return ...沒有數據的時候返回的控件
    },
)

下面是一個模仿官方自帶demo“計數器”的一個例子,使用了StreamBuilder,而不需要任何setState:

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

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(
        child: StreamBuilder<int>(  // 監聽Stream,每次值改變的時候,更新Text中的內容
          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: (){
          // 每次點擊按鈕,更加_counter的值,同時通過Sink將它發送給Stream;
          // 每注入一個值,都會引起StreamBuilder的監聽,StreamBuilder重建並刷新counter
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}
這種實現方式比起 setState是一個很大的改進,因為我們不需要強行重建整個控件和它的子控件,只需要重建我們希望重建的StreamBuilder(當然它的子控件也會被重建)。我們之所以依然使用StatefulWidget的唯一原因就是: StreamController需要在控件dispose()的時候被釋放

能不能完全拋棄StatefulWidget?BLoC了解下

上一步,我們摒棄了 setState 方法,那么下一步,我們試試把 StatefulWidget 替換成 StatelessWidget 吧,而且 官方也推薦使用 StatelessWidget 替換 StatefulWidget,這里就需要提下 BLoC 模式了。
BLoC是Business Logic Component(業務邏輯組建)的縮寫,就是將UI與業務邏輯分離,有點MVC的味道。
說實話,現在 Google 下 「flutter bloc」能搜到很多文章,基本上都是通過 InheritedWidget 來實現的,但是 InheritedWidget 沒有提供 dispose 方法,那么就會存在 StreamController 不能及時銷毀等問題,所以,參考了一篇國外的文章, Reactive Programming - Streams - BLoC 這里通過使用 StatefulWidget 來實現,當該部件銷毀的時候,可以在其 dispose 方法中及時銷毀 StreamController,這里我還是先當個搬運工,搬下大佬為我們實現好的基類
import 'package:flutter/material.dart';
abstract class BaseBloc {
  void dispose(); // 該方法用於及時銷毀資源
}

class BlocProvider<T extends BaseBloc> extends StatefulWidget {
  final Widget child; // 這個 `widget` 在 stream 接收到通知的時候刷新
  final T bloc; 
  
  BlocProvider({Key key, @required this.child, @required this.bloc}) : super(key: key);

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

  // 該方法用於返回 Bloc 實例
  static T of<T extends BaseBloc>(BuildContext context) {
    final type = _typeOf<BlocProvider<T>>(); // 獲取當前 Bloc 的類型
    // 通過類型獲取相應的 Provider,再通過 Provider 獲取 bloc 實例
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type); 
    return provider.bloc; 
  }

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

class _BlocProviderState<T> extends State<BlocProvider<BaseBloc>> {
    
  @override
  void dispose() {
    widget.bloc.dispose(); // 及時銷毀資源
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.child;
  }
}

接着我們對前面的例子使用 BLoC 進行修改。

首先,我們需要創建一個 Bloc 類,用於修改 count 的值:

import '../widget/baseBloc.dart';
import 'dart:async';

class
CounterBloc extends BaseBloc { int _count = 0; int get count => _count; // stream StreamController<int> _countController = StreamController.broadcast(); Stream<int> get countStream => _countController.stream; // 用於 StreamBuilder 的 stream void dispatch(int value) { _count = value; _countController.sink.add(_count); // 用於通知修改值 } @override void dispose() { _countController.close(); // 注銷資源 } }

在使用 Bloc 前,需要在最上層的容器中進行注冊,也就是 MaterialApp 中.

import 'package:flutter/material.dart';
import './widget/baseBloc.dart';
import './bloc/counter.dart';


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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 這里對創建的 bloc 類進行注冊,如果說有多個 bloc 類的話,可以通過 child 進行嵌套注冊即可
    // 放在最頂層,可以全局調用,當 App 關閉后,銷毀所有的 Bloc 資源,
    // 也可以在路由跳轉的時候進行注冊,至於在哪里注冊,完全看需求
    // 例如實現主題色的切換,則需要在全局定義,當切換主題色的時候全局切換
    // 又比如只有某個或者某幾個特殊界面調用,那么完全可以通過在路由跳轉的時候注冊
    return BlocProvider(  
        child: MaterialApp(
          debugShowCheckedModeBanner: false,
          home: StreamHome(),
        ),
        bloc: CounterBloc());
  }
}

class StreamHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 獲取注冊的 bloc,必須先注冊,再去查找
    final CounterBloc _bloc = BlocProvider.of<CounterBloc>(context); 
    return Scaffold(
      body: SafeArea(
          child: Container(
        alignment: Alignment.center,
        child: StreamBuilder(
          initialData: _bloc.count,
          stream: _bloc.countStream,
          builder: (_, snapshot) => Text('${snapshot.data}', style: TextStyle(fontSize: 20.0)),
        ),
      )),
      floatingActionButton:
          // 通過 bloc 中的 dispatch 方法進行值的修改,通知 stream 刷新界面
          FloatingActionButton(onPressed: () => 
              _bloc.dispatch(_bloc.count + 1), child: Icon(Icons.add)),
    );
  }
}

重新運行后,查看效果還是一樣的。所以我們成功的對 StatefulWidget 進行了替換。

先總結下 Bloc:

​ 1. 成功的把頁面和邏輯分離開了,頁面只展示數據,邏輯通過 BLoC 進行處理

​ 2. 減少了 setState 方法的使用,提高了性能

​ 3. 實現了狀態管理

多個Bloc的使用

  • 每一個有業務邏輯的頁面的頂層都應該有自己的BLoC;
  • 每一個“足夠復雜的組建(complex enough component)”都應該有相應的BLoC;
  • 可以使用一個ApplicationBloc來處理整個App的狀態。

下面的例子展示了在整個App的頂層使用ApplicationBloc,在CounterPage的頂層使用IncrementBloc:

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);
    
    ...
  }
}

一個實踐Demo

大佬構建了一個偽應用程序來展示如何使用所有這些概念。 完整的源代碼可以在Github上找到。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM