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的sink
和stream
:
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**
进行检查),我们将返回Image
和Text
小部件。 “图片”是订购的披萨的图片。每次订购比萨饼时,比萨饼的数量都会减少(逻辑在变压器内部处理)。如果没有更多的比萨了。输出将为“缺货”。
在整个应用程序中,我什至没有使用过一个StatefulWidget,但我仍然能够更改应用程序的状态。 换句话说,我利用Streams的功能使应用程序具有反应性。 当您想听听数据中的变化并据此做出反应时,流非常有用。 现在希望这有道理,为什么我们应该使用Streams使我们的应用程序尽可能地具有响应性。