探索Flutter中的流

原文地址:https://medium.com/flutterpub/exploring-streams-in-flutter-4732e5524dd8

這篇文章主要介紹Flutter中的流是什么以及如何使用。

流是什么

正如Flutter文檔上所說的,流提供異步的數(shù)據(jù)序列,接下來從真實世界中取了解什么是流。

披薩屋的故事

曾幾何時,有一個人John非常擅長做pizza,他制作了很多不同種類的披薩,他制作的披薩很好吃,以至于世界各地的人都慕名而來,John有一個小房子用來接受客戶的訂單然后制作披薩給客戶。John有一個女兒Mia來幫助他做生意,John采用了一個非常簡單且有效的方式來開展業(yè)務(wù)。

  1. Mia從客戶手里拿到訂單并轉(zhuǎn)給John
  2. John檢查訂單看是否有配料以供烘烤
  3. 客戶將在收集室領(lǐng)取披薩,如果沒有配料,他們將會被通知“你的披薩無法制作的”。

接下來將列出一些有趣的東西用來有效的理解Stream的概念。

  1. 烘烤披薩的房子。房子是一個固定的地方,所以只會創(chuàng)建一次,用來接受訂單然后烘烤披薩。

  2. Mia的唯一責(zé)任就是接受命令然后傳遞給John。

  3. John是決策者,他將決定披薩是否可以烘烤,然后將披薩送到收集室。

因此,總的來說,數(shù)據(jù)按順序流入流中,然后經(jīng)過處理,并發(fā)送到輸出流,接下來我們將上面的故事轉(zhuǎn)換成代碼。

Dart中的流

當(dāng)我們談?wù)揇art中的流時,我們需要使用Dart提供的async庫,該庫支持異步編程,并包含創(chuàng)建Streams的所有類和方法。

頁面中有四個Button,每一個Button都代表一中披薩種類,如果你點擊某一個按鈕,就會把訂單給Mia,然后Mia將訂單轉(zhuǎn)給John,接著John就會制作披薩,然后將披薩放到收集屋,然后客戶可以來這里取他的披薩。如果材料沒有的話,他可能會收到缺貨的消息。

開始寫代碼

  1. 創(chuàng)建Flutter項目,如果不知道怎么做,參考這里

  2. 移除main.dart中的所有代碼,然后添加下面的代碼:

import 'package:flutter/material.dart';
import 'src/app.dart';

void main() => runApp(new MyApp());
  1. 此時會出現(xiàn)錯誤,先不要管,我們接下來會修復(fù)它。
  2. 在lib包下面創(chuàng)建一個src包,接著在src包中創(chuàng)建一個app.dart文件,接著編寫下面的代碼:
import 'package:flutter/material.dart';
import 'pizza_house.dart';
import 'blocs/provider.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider( 
      child: MaterialApp(
        home: new PizzaHouse(),
      ),
    );
  }
}
  1. 在src包下創(chuàng)建pizza_house.dart, 現(xiàn)在先不寫任何東西在里面。
  2. 在src包下創(chuàng)建bloc包,在該包下創(chuàng)建兩個文件,分別是bloc.dartprovider.dart, 下面是provider.dart中的代碼
import 'package:flutter/material.dart';
import 'bloc.dart';
export 'bloc.dart';

class Provider extends InheritedWidget {
  final bloc = Bloc();

  Provider({Key key, Widget child}) : super(key: key, child: child);

  bool updateShouldNotify(_) => true;

  static Bloc of(BuildContext context) {
    return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
  }
}

該類主要負(fù)責(zé)在組件中對Bloc的訪問。下面是項目結(jié)構(gòu)。

image.png
  1. 接下來實現(xiàn)bloc.dart,下面就是bloc的完整代碼:
import 'dart:async';

class Bloc {

  //Our pizza house
  final order = StreamController<String>();

  //Our collect office
  Stream<String> get orderOffice => order.stream.transform(validateOrder);

  //Pizza house menu and quantity
  static final _pizzaList = {
    "Sushi": 2,
    "Neapolitan": 3,
    "California-style": 4,
    "Marinara": 2
  };

  //Different pizza images
  static final _pizzaImages = {
    "Sushi": "http://pngimg.com/uploads/pizza/pizza_PNG44077.png",
    "Neapolitan": "http://pngimg.com/uploads/pizza/pizza_PNG44078.png",
    "California-style": "http://pngimg.com/uploads/pizza/pizza_PNG44081.png",
    "Marinara": "http://pngimg.com/uploads/pizza/pizza_PNG44084.png"
  };


  //Validate if pizza can be baked or not. This is John
  final validateOrder =
      StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {
    if (_pizzaList[order] != null) {
      //pizza is available
      if (_pizzaList[order] != 0) {
        //pizza can be delivered
        sink.add(_pizzaImages[order]);
        final quantity = _pizzaList[order];
        _pizzaList[order] = quantity-1;
      } else {
        //out of stock
        sink.addError("Out of stock");
      }
    } else {
      //pizza is not in the menu
      sink.addError("Pizza not found");
    }
  });

  //This is Mia
  void orderItem(String pizza) {
    order.sink.add(pizza);
  }
}

StreamController

上面的代碼主要是用來接收訂單,處理訂單并且輸出。但是有什么方法可以使StreamController成為一個完整的披薩屋,有哪些方法可以用來接收訂單,處理訂單和輸出訂單。

image.png

StreamController有兩個getter,一個是sink,另一個是stream.sink是Mia在這里接受訂單并傳遞給流,還句話說就是sink會將數(shù)據(jù)添加到StreamController的流中,現(xiàn)在我們來說一下Stream,它會在做一些處理之后將數(shù)據(jù)傳遞給外界。下面的代碼就是StreamController的接收器和Stream。

  //Our pizza house
  final order = StreamController<String>();

  //Our collect office
  Stream<String> get orderOffice => order.stream.transform(validateOrder);
  //This is Mia
  void orderItem(String pizza) {
    order.sink.add(pizza);
  }

接收器有一個add的方法,它將數(shù)據(jù)添加到流中,這里是順序添加到流中。orderOffice是一個getter用來在pizza_house.dart中調(diào)用,以便將輸出提供給客戶。
在上面的代碼中,你肯定想知道transform的用途,他將調(diào)用John處理的訂單,然后將處理后的數(shù)據(jù)傳入到流中。John在代碼中是validateOrder,validateOrder是StreamTransformer。

//Validate if pizza can be baked or not. This is John
  final validateOrder =
      StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {
    if (_pizzaList[order] != null) {
      //pizza is available
      if (_pizzaList[order] != 0) {
        //pizza can be delivered
        sink.add(_pizzaImages[order]);
        final quantity = _pizzaList[order];
        _pizzaList[order] = quantity-1;
      } else {
        //out of stock
        sink.addError("Out of stock");
      }
    } else {
      //pizza is not in the menu
      sink.addError("Pizza not found");
    }
  });

StreamTransformer

簡單的來說,他將從流中接收傳入的訂單,并檢查訂單是否有效。如果訂單有效,它將使用sink.add(successOrder)(pizza pizza successfully)方法將輸出添加到流中。如果訂單無效,它將會添加sink.addError(invalidOrder),告訴客戶“您訂購的披薩缺貨”。

//Pizza house menu and quantity
  static final _pizzaList = {
    "Sushi": 2,
    "Neapolitan": 3,
    "California-style": 4,
    "Marinara": 2
  };

  //Different pizza images
  static final _pizzaImages = {
    "Sushi": "http://pngimg.com/uploads/pizza/pizza_PNG44077.png",
    "Neapolitan": "http://pngimg.com/uploads/pizza/pizza_PNG44078.png",
    "California-style": "http://pngimg.com/uploads/pizza/pizza_PNG44081.png",
    "Marinara": "http://pngimg.com/uploads/pizza/pizza_PNG44084.png"
  };

_pizzaList是我們的菜單,它將保存比薩餅的類型以及可以烘烤的總量。 _pizzaImages是一個map,上面有比薩餅屋烤制的不同種類的比薩的圖片。
最后我們?nèi)崿F(xiàn)pizza_house.dart類。

import 'package:flutter/material.dart';
import 'blocs/provider.dart';

class PizzaHouse extends StatelessWidget {
  var pizzaName = "";
  @override
  Widget build(BuildContext context) {
    final _bloc = Provider.of(context);
    return Scaffold(
      appBar: AppBar(
        title: Text("Pizza House"),
      ),
      body: Container(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          children: <Widget>[menu1(_bloc), menu2(_bloc), orderOffice(_bloc)],
        ),
      ),
    );
  }

  menu1(Bloc bloc) {
    return Container(
      margin: EdgeInsets.all(8.0),
      child: Row(
        children: <Widget>[
          Expanded(
            child: RaisedButton(
              child: Text("Neapolitan"),
              onPressed: () {
                bloc.orderItem("Neapolitan");
                pizzaName = "Neapolitan";
              },
            ),
          ),
          Container(
            margin: EdgeInsets.only(left: 2.0, right: 2.0),
          ),
          Expanded(
            child: RaisedButton(
              child: Text("California-style"),
              onPressed: () {
                bloc.orderItem("California-style");
                pizzaName = "California-style";
              },
            ),
          )
        ],
      ),
    );
  }

  menu2(Bloc bloc) {
    return Container(
      margin: EdgeInsets.all(8.0),
      child: Row(
        children: <Widget>[
          Expanded(
            child: RaisedButton(
              child: Text("Sushi"),
              onPressed: () {
                bloc.orderItem("Sushi");
                pizzaName = "Sushi";
              },
            ),
          ),
          Container(
            margin: EdgeInsets.only(left: 2.0, right: 2.0),
          ),
          Expanded(
            child: RaisedButton(
              child: Text("Marinara"),
              onPressed: () {
                bloc.orderItem("Marinara");
                pizzaName = "Marinara";
              },
            ),
          )
        ],
      ),
    );
  }

  orderOffice(Bloc bloc) {
    return StreamBuilder(
      stream: bloc.orderOffice,
      builder: (context, AsyncSnapshot<String> snapshot) {
        if (snapshot.hasData) {
          return Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Image.network(
                snapshot.data,
                fit: BoxFit.fill,
              ),
              Container(
                margin: EdgeInsets.only(top: 8.0),
              ),
              Text("Yay! Collect your $pizzaName pizza")
            ],
          );
        } else if (snapshot.hasError) {
          return Column(
            children: <Widget>[
              Image.network("http://megatron.co.il/en/wp-content/uploads/sites/2/2017/11/out-of-stock.jpg",
              fit: BoxFit.fill),
              Text(
                snapshot.error,
                style: TextStyle(
                  fontSize: 20.0,
                ),
              ),
            ],
          );
        }
        return Text("No item in collect office");
      },
    );
  }
}

build方法中第一行是聲明bloc實例,使用此實例,我們可以下訂單并檢查collect office總的輸出。我們已經(jīng)將下不同類型的訂單的功能添加到了每個button的onPressed方法中,在onPressed方法中我們調(diào)用orderItem,然后會調(diào)用sink.add方法。為了向客戶展示輸出的數(shù)據(jù),這里我們需要使用StreamBuilder。

StreamBuilder將從orderOffice()方法中監(jiān)聽Stream,如果有任何可用數(shù)據(jù),它將根據(jù)來自流的數(shù)據(jù)返回一個widget,StreamBuilder有兩個參數(shù),一個是stream,另一個是builder,stream接收一個從bloc中返回的一個流,即orderOffice。StreamBuilder將監(jiān)聽任何進(jìn)來的新數(shù)據(jù),builder有兩個參數(shù),context和snapshot,snapshot就像是數(shù)據(jù)提供者,它將從流中獲取數(shù)據(jù),以便我們可以處理它并返回要顯示的正確組件。正如你看到的那樣,我們可以通過hasData來判斷是否有數(shù)據(jù),從而正確的顯示圖片或者文字。

在整個應(yīng)用程序中沒有使用過一個StatefulWidget,仍然能夠改變應(yīng)用程序的狀態(tài)。換句話說,使用Streams能夠讓應(yīng)用程序被動反應(yīng)。當(dāng)您想要監(jiān)聽數(shù)據(jù)中的更改并根據(jù)數(shù)據(jù)做出反應(yīng)時,流非常有用。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容