Flutter Stream 簡介及部分操作符使用

奮鬥的小土豆發表於2019-07-26

前言

    對於剛接觸Flutter的同學來說,Stream(流)是一個相對比較抽象,也相對比較難以理解的東西。準確的來說Stream並不是Flutter的特性,而是Dart語言自身所帶庫。StreamFuture都位於dart:async核心庫,是Dart中非同步操作的兩大高手。所以不僅僅可以用於Flutter,而是可以用於任何Dart語言上的實現。

    在我們剛開始學習Flutter的時候基本都是使用 StatefulWidgetsetState((){})來重新整理介面的資料,當我熟練使用流之後就可以基本完全使用StatelessWidget告別 StatefulWidget同樣達到資料重新整理效果。

Stream分類

流可以分為兩類:

  • 單訂閱流(Single Subscription),這種流最多隻能有一個監聽器(listener)

  • 多訂閱流(Broadcast),這種流可以有多個監聽器監聽(listener)

Stream建立方式

  • Stream.fromFuture 接收一個Future物件作為引數

    Stream<String>.fromFuture(getData());
    
    Future<String> getData() async{
        await Future.delayed(Duration(seconds: 5));
        return "返回一個Future物件";
      }
    複製程式碼

  • Stream.fromIterable 接收一個集合物件作為引數

    Stream<String>.fromIterable(['A','B','C']);
    複製程式碼

  • Stream.fromFutures 接收一個Future集合物件作為引數

    Stream<String>.fromFutures([getData()]);
    複製程式碼

  • Stream.periodic 接收一個 Duration物件作為引數

    Duration interval = Duration(seconds: 1);
    Stream<int> stream = Stream<int>.periodic(interval);
    複製程式碼

操作方式及使用

使用Stream .periodic 方式建立一個Stream物件

用於建立一個週期傳送事件的流,如下

///用於建立一個每隔一秒傳送一次無限事件的流,並列印出值
void _stream() async{
    Duration interval = Duration(seconds: 1);
    Stream<int> stream = Stream.periodic(interval, (data) => data);
    await for(int i in stream ){
      print(i); 
    }
}

//(data){return data;} 上面的這句是用來回來值,如果省略這句,列印的值都為null
複製程式碼

.

  • Stream.take(int count)

    上面建立了一個無限每隔一秒傳送一次事件的流,如果我們想指定只傳送10個事件則,用take。下面就只會列印出0-9

    void _stream() async{
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10); //指定傳送事件個數
        await for(int i in stream ){
         print(i);
        }
    }
    複製程式碼

  • Stream.takeWhile

    上面這種方式我們是隻制定了傳送事件的個數,如果我們也不知道傳送多少個事件,我們可以從返回的結果上做一個返回值的限制,上面結果也可以用以下方式實現

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
    //    stream = stream.take(10);
        stream = stream.takeWhile((data) {
          return data < 10;
        });
        await for (int i in stream) {
          print(i);
        }
      }
    複製程式碼

  • Stream.skip(int count)

    skip可以指定跳過前面的幾個事件,如下會跳過0和1,輸出 2-9;

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream = stream.skip(2);
        await for (int i in stream) {
          print(i);
        }
      }
    複製程式碼

  • Stream.skipWhile

    可以指定跳過不傳送事件的指定條件,如下跳過0-4的輸出,輸出5-9

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream = stream.skipWhile((data) => data<5);
        await for (int i in stream) {
          print(i);
        }
      }
    複製程式碼

  • Stream.toList()

    將流中所有的資料收集存放在List中,並返回 Future<List>物件,listData裡面 0-9

    1.這個是一個非同步方法,要結果則需要使用await關鍵字

    2.這個是等待Stream當流結束時,一次返回結果

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        List<int> listData = await stream.toList();
        for (int i in listData) {
          print(i);
        }
      }
    複製程式碼

  • Stream. listen()

    這是一種特定的可以用於監聽資料流的方式,和 forEach迴圈的效果一致,但是返回的是StreamSubscription<T>物件,如下也會輸出0-9,同時列印出 ”流已完成“

    看一下原始碼這種方式可以接收

    StreamSubscription<T> listen(void onData(T event),
          {Function onError, void onDone(), bool cancelOnError});
    複製程式碼

    1.onData是接收到資料的處理,必須要實現的方法

    2.onError流發生錯誤時候的處理

    3.onDone流完成時候調取

    4.cancelOnError發生錯誤的時候是否立馬終止

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream.listen((data) {
          print(data);
        }, onError: (error) {
          print("流發生錯誤");
        }, onDone: () {
          print("流已完成");
        }, cancelOnError: false);
      }
    複製程式碼

  • Stream. forEach()

    這中操作和listen()的方式基本差不多,也是一種監聽流的方式,這只是監聽了onData,下面程式碼也會輸出0-9

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream.forEach((data) {
          print(data);
        });
      }
    複製程式碼

  • Stream .length

    用於獲取等待流中所有事件發射完成之後統計事件的總數量,下面程式碼會輸出 10

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        var allEvents = await stream.length;
        print(allEvents);
      }
    複製程式碼

  • Stream.where

    在流中新增篩選條件,過濾掉一些不想要的資料,滿足條件返回true,不滿足條件返回false,如下我們篩選出流中大於5小於10的資料

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream<int>.periodic(interval, (data) => data);
        stream = stream.where((data)=>data>5);
        stream = stream.where((data)=> data<10);
        await for(int i in stream){
          print(i);
        }
      }
    複製程式碼

  • stream.map

    對流中的資料進行一些變換,以下是我對Stream的每個資料都加1

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream<int>.periodic(interval, (data) => data);
        stream = stream.map((data) => data + 1);
        await for (int i in stream) {
          print(i);
        }
      }
    複製程式碼

  • Stream.expand

    對流中的資料進行一個擴充套件,如下,會輸出1,1,2,2,3,3….

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream stream = Stream.periodic(interval, (data) => data);
        stream = stream.expand((data)=>[data,data]);
        stream.listen((data)=>print(data),onError:(error)=> print("發生錯誤") );
      }
    複製程式碼
  • Stream.transform

    如果我們在在流流轉的過程中需要進行一些轉換和控制我們則需要使用到transform,接收一個

    StreamTransformer<S,T>,S表示轉換之前的型別,T表示轉換後的輸入型別,如下程式碼我們會接收到三組數字模擬輸入了三次密碼,並判斷真確的密碼,同時輸出密碼正確和密碼錯誤:

     void _stream() async {
        var stream = Stream<int>.fromIterable([123456,234567,678901]);
        var st = StreamTransformer<int, String>.fromHandlers(
            handleData: (int data, sink) {
          if (data == 678901) {
            sink.add("密碼輸入正確,正在開鎖。。。");
          } else {
            sink.add("密碼輸入錯誤...");
          }
        });
        stream.transform(st).listen((String data) => print(data),
            onError: (error) => print("發生錯誤"));
      }
    複製程式碼

    輸入如下結果

    I/flutter (18980): 密碼輸入錯誤...
    I/flutter (18980): 密碼輸入錯誤...
    I/flutter (18980): 密碼輸入正確,正在開鎖。。。
    複製程式碼

StreamController使用

​ 介紹完了Stream的基本概念和基本用法,上面直接建立流的方式,對我們本身開發來說,用途不是很大,我們在實際的開發過程中,基本都是使用的StreamContoller來建立流。通過原始碼我們可以知道Stream的幾種構造方法,最終都是通過StreamController進行了包裝。

建立StreamController物件及使用

  • 構建單訂閱的Streamcontroller
//StreamController裡面會建立一個Stream,我們實際操控的Stream
StreamController<String> streamController = StreamController();
streamController.stream.listen((data)=> print(data));
streamController.sink.add("aaa");
streamController.add("bbb");
streamController.add("ccc");
streamController.close();

//上面程式碼我們會輸出 aaa,bbb,ccc
複製程式碼

注意:如果我們給上面的程式碼再加一個listen會報如下異常,所以單訂閱流,只能有一個listen。一般情況下我們多數都是使用的單訂閱流,我們也可以將單訂閱流轉成多訂閱流。

  • 構建多監聽器的StreamController有兩種方式

    1.直接建立多訂閱Stream

    StreamController<String> streamController = StreamController.broadcast();
        streamController.stream.listen((data){
          print(data);
        },onError: (error){
          print(error.toString());
        });
        streamController.stream.listen((data) => print(data));
        streamController.add("bbb");
    
    	//上面程式碼回輸出 bbb,bbb
    複製程式碼

    2.將單訂閱流轉成多訂閱流

    	StreamController<String> streamController = StreamController();
      Stream stream =streamController.stream.asBroadcastStream();
      stream.listen((data) => print(data));
      stream.listen((data) => print(data));
      streamController.sink.add("aaa");
      streamController.close();
      
      //上面程式碼會輸出 aaa,aaa
    複製程式碼

注意:在流用完了之後記得關閉,呼叫streamController.close()

StreamBuilder使用

​    前面我把Stream的常用方式做了簡單的介紹和演示,我們怎麼結合Flutter使用呢?在Flutter裡面提供了一個WidgetStreamBuilderStreamBuilder其實是個StatefulWidget它一直記錄著流中最新的資料,當資料流發生變化時,會自動呼叫builder方法進行重建。

  • StreamBuilder的原始碼如下,需要接受一個流,我們可以傳入一個StreamControllerStream

    const StreamBuilder({
        Key key,
        this.initialData,
        Stream<T> stream,
        @required this.builder,
      }) : assert(builder != null),
           super(key: key, stream: stream);
    複製程式碼

  • 使用StreamController 結合 StreamBuider對官方的計數器進行改進,取代setState重新整理頁面,程式碼如下

    class MyHomePage extends StatefulWidget {
      @override
      _MyHomePageState createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      int _count = 0;
      final StreamController<int> _streamController = StreamController();
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Container(
            child: Center(
              child: StreamBuilder<int>(
                  stream: _streamController.stream,
                  builder: (BuildContext context, AsyncSnapshot snapshot) {
                    return snapshot.data == null
                        ? Text("0")
                        : Text("${snapshot.data}");
                  }),
            ),
          ),
          floatingActionButton: FloatingActionButton(
              child: const Icon(Icons.add),
              onPressed: () {
                _streamController.sink.add(++_count);
              }),
        );
      }
    
      @override
      void dispose() {
        _streamController.close();
        super.dispose();
      }
    }
    複製程式碼

原始碼相關

    從StreamController原始碼我們可以看出來裡面建立的得過程 預設建立一個_SyncStreamController,具體可以去讀取下原始碼,看看StreamController是怎麼建立StreamStreamSink

factory StreamController(
      {void onListen(),
      void onPause(),
      void onResume(),
      onCancel(),
      bool sync: false}) {
    return sync
        ? new _SyncStreamController<T>(onListen, onPause, onResume, onCancel)
        : new _AsyncStreamController<T>(onListen, onPause, onResume, onCancel);
  }
複製程式碼

    注意:上面我既使用了streamController.sink.add("aaa");新增資料,也使用了streamController.add("bbb");方式新增資料。實際效果是一樣的,檢視原始碼可知 sink如下,實際上sink是對StreamController的一種包裝,最終都是調取的StreamController.add方法;

class _StreamSinkWrapper<T> implements StreamSink<T> {
  final StreamController _target;
  _StreamSinkWrapper(this._target);
  void add(T data) {
    _target.add(data);
  }

  void addError(Object error, [StackTrace stackTrace]) {
    _target.addError(error, stackTrace);
  }

  Future close() => _target.close();

  Future addStream(Stream<T> source) => _target.addStream(source);

  Future get done => _target.done;
複製程式碼

總結

    以上是我在開發過程中,對FlutterStream使用的一些簡單的認識和常用方法的總結,StreamBuilderStreamController結合使用,實現區域性重新整理效果(比喻一個頁面有多個介面,各自應該重新整理自己控制的UI的部分而不應該整個頁面重新整理,時候我們可以使用StreamControllerStreamBuilder達到這種效果)如有不到之處或者有偏差的地方,望指正~

相關文章