Flutter 中的流(Stream)详解

Flutter 中的流(Stream)详解

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

什么是流?

“流”即 Stream,根据Flutter的文档,“流提供了异步的数据序列。”

Dart 中的流

当我们谈论特定于dart的Streams时。 我们将使用dart提供的async库。 该库支持异步编程,并包含创建Streams的所有类和方法。 因此,无需进一步讨论,我们就开始对我们的比萨店进行编码。 请稍等一下,在执行任何操作之前,我先向您展示最终产品。 这样,当我在此处放下代码时,您就可以进行连接,并且很容易理解每一行代码的解释。 有一个包含4个按钮的菜单。 每个按钮上都有一个披萨名称。 如果单击任何按钮,它将下订单该特定类型的披萨。 如果单击该按钮,该订单将由Mia接受,并传递给John。 John将对其进行处理,并将输出结果放入收款办公室。 客户将来收集办公室并收集他的订单。 如果没有披萨,他可能会收到订购的披萨(图像和成功消息)或“缺货”消息。 客户可以多次订购相同的比萨。 但是,如果特定比萨饼的存货结束,则输出将为“缺货”。 希望您对实现有一些了解。 如果没有,我将尽力向您详细解释完整的实现。

开始编码

1.使用创建一个新的Flutter项目。 具体参考这里。 2.从main.dart文件中删除所有代码,然后粘贴以下代码:

1
2
3
4
5
import 'package:flutter/material.dart';
import 'src/app.dart';

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

3.在MyApp()上看到错误。 不要急,我们将在下一步修复它。

4.在lib包下创建一个名为src的包。 现在在src包下创建一个dart文件app.dart。 将以下代码粘贴到“ app.dart”文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(),
      ),
    );
  }
}

正如我在先决条件部分中所述,您应该了解BLoC模式。 如果您在此处标记为绿色,则说明您在此处理解了Provider()的用法。 如果您不能简单地概括一下,那是一个InheritedWidget,它可以为整个应用程序提供对bloc实例的访问。 粘贴上面的代码后,您必须看到一些错误。 不用担心,所有内容都会在接下来的步骤中清除。

5.在src包下创建一个dart文件,并将其命名为pizza_house.dart。 到目前为止,让我们保持空白。 一旦我向您解释了攻击的概念和计划,我们将在其中编写一些代码。

6.接下来,在src包下创建另一个名为blocs的包。 在blocs包内创建两个dart文件,并将它们命名为bloc.dart,provider.dart。 将以下代码粘贴到“ provider.dart”文件中,然后将“ bloc.dart”文件保留为空:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
  }
}

这节课很简单。 它唯一负责的是提供对我们的小部件树中的bloc实例(“ bloc.dart”)的访问。

以防万一您想查看完整的项目结构:

7.现在该实现bloc.dart文件了。 如果您看到下面的代码,请不要惊慌。 我将向您详细解释每一行。 因此,这里是bloc.dart文件的完整实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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);
  }
}

在第一行本身中,您必须挠头head。 这是什么StreamController? 我知道它要来了。 让我解释一下并减轻您的负担。

StreamController

简单来说就是我们的披萨屋。 它负责接订单,处理订单并发出输出。 但是,使StreamController成为完整的Pizza House的方法是什么?换句话说,负责下订单,处理并提供输出的方法是什么? 这是一幅小图片,可以回答上述问题:

StreamController有两个获取器,一个是sink,另一个是stream。 简单来说,“接收器”会将数据添加到StreamController的“流”中。 现在,我们来谈谈“ stream”。经过一些处理(John)后,它将把数据传递到外界(收集办公室)。 现在,如果您看到bloc.dart代码。 以下几行是StreamController的sinkstream

1
2
3
4
5
6
7
8
9
10
11
//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);
  }

“接收器”有一个称为“添加(数据)”的方法,它将数据添加到“流”中。 在这里它将订单添加到stream中。 “ orderOffice”(收款办公室)是一种吸气剂,它将在我们的“ pizza_house.dart”(尚未实现)中调用,以将输出提供给客户。

在上面的代码中,您一定想知道transform()是干什么用的。 这是一种方法,该方法将调用John处理传入的订单,并将处理后的输出放入流中(收集办公室)。 现在让我们来讨论一下约翰,他是比萨饼屋的主要心脏。 John是上面代码中的validateOrder。 ValidateOrder是一个StreamTransformer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//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)(成功烘焙比萨饼)方法将输出添加到流中。 在这里,我们在流中添加了烤比萨饼的图像,以向客户显示他们的比萨饼已准备就绪。 如果订单无效,则会将其添加到sink.addError(invalidOrder)中,告知客户“您订购的披萨已无货”。

现在,我希望一切都为您连接。 在实现pizza_house.dart之前,bloc.dart文件中还有两件事需要解释。 这些是这些代码行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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是地图,将保存在比萨屋中烤制的不同类型比萨的图像。 这些图像将显示给客户,使他感到自己已收到订单order。

现在我们将要实现的最后一个类即是pizza_house.dart文件。 这是它的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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实例的声明。使用此实例,我们可以下订单并检查收款办公室内的输出。订购特定类型的披萨。我们实现了在每个按钮的onPressed()方法内将特定的比萨饼添加到流中的逻辑。在onPressed()方法内部,我们调用了orderItem(pizza),后者又调用了ink.add()方法。为了向客户显示收款办公室订单的输出是什么,我们需要使小部件能够与流一起使用。我们将为此使用**StreamBuilder

StreamBuilder将侦听来自orderOffice()方法的流,如果有可用数据,它将基于来自流的数据返回一个小部件。 StreamBuilder具有两个重要参数。 1)stream和2)builder。 stream以一个方法作为参数,该方法返回一个流,即来自bloc.dart文件的orderOffice方法。 StreamBuilder的stream会监听传入的任何新数据。builder会采用具有两个参数的方法,即context和snapshot。快照类似于数据提供程序。它将从流中获取数据并提供它,以便我们可以对其进行处理并返回正确的小部件以进行显示。如您所见,快照中是否有任何数据(我们可以使用**hasData**进行检查),我们将返回ImageText小部件。 “图片”是订购的披萨的图片。每次订购比萨饼时,比萨饼的数量都会减少(逻辑在变压器内部处理)。如果没有更多的比萨了。输出将为“缺货”。

在整个应用程序中,我什至没有使用过一个StatefulWidget,但我仍然能够更改应用程序的状态。 换句话说,我利用Streams的功能使应用程序具有反应性。 当您想听听数据中的变化并据此做出反应时,流非常有用。 现在希望这有道理,为什么我们应该使用Streams使我们的应用程序尽可能地具有响应性。

完整代码可参考https://github.com/SAGARSURI/PizzaHouse

Rating: