在 Flutter 中编写异步代码,可以使用 Futures 或 Streams 两种方法。异步代码基本上允许在等待另一个操作完成时运行语句。它确保应用程序不会被阻止,并且可以在另一个操作完成时更新 UI。
使用 Streams 的一些示例是:
- 获取音频播放器的持续更新。 (播放、停止、暂停)
- 用户在键盘上打字也是一个字符数据流。
- 如果用户通过验证,则登录时每 3 秒检查一次。
Flutter 中的 Stream 是什么?
Stream 基本上是一系列异步事件。将 Streams 概念化的最简单方法是将其视为管道,我们将事件从一端放置并从另一端接收。
Stream 可以包含数据事件,也可以包含错误事件。当发射完成时,它会传递一个特殊的完成信号,通知监听器没有留下任何事件。使用 Stream 的好处是它减少了前端和后端之间的耦合。发送者只需将值放入 Stream 中,而不用担心侦听器。接收者只是监听 Stream ,并不关心它的实现。这导致了小规模的抽象。
Dart 中的 Streams 概念有 4 个主要组件:
- Stream:表示异步数据流。侦听器可以侦听流并收到任何事件更新的通知。
- EventSink:它允许我们将事件添加到流中。它可以是数据事件或错误事件。
- StreamController:它简化了在 dart 中使用流。它管理流、接收器并管理流事件。
- StreamSubscription:当我们订阅任何流时,我们会得到一个流订阅对象,我们可以使用它来暂停、恢复或取消我们收到的数据流。
我们不会主要创建自己的流或接收器,StreamController 会为我们做这些。当我们收听任何流时,我们必须确保保存流订阅并在不需要流时取消。
在 Flutter 中创建流
要使用流,只需创建一个 StreamController:
import 'dart:async';
StreamController<int> controller = StreamController<int>();
现在我们可以轻松地从 StreamController 对象访问流:
Stream stream = controller.stream;
StreamController 在内部为我们创建和管理流。 StreamController 就像一个帮助接口,提供给开发者在 Flutter 中使用 Streams 和 EventSinks。
在流上添加/发射事件
我们可以使用 EventSink 将值添加到 Stream:
// Adding Items via sink
controller.sink.add(10);
使用 StreamController 实例,可以使用 listen() 方法轻松访问 Stream 的事件流。还可以访问 sink 并使用接收器上的 add() 方法轻松添加新数据。
请注意,StreamController 还提供了一个 add() 函数,该函数反过来将数据添加到 sink 中。所以不要混淆这两者,两者都做同样的任务。
controller.sink.add(10);
//OR
controller.add(10);
要添加错误事件,只需使用 addError() 函数而不是 add():
controller.addError("Error Occured!");
如何从流中访问值
现在已经使用 StreamController 创建了一个流,让我们看看如何从流中获取值。这也称为收听或订阅流。根据管道约定,我们正在修复数据流的接收点。我们只需使用 listen() 函数订阅流。
Stream stream = controller.stream;
stream.listen((value) {
print("Value: $value));
});
要收听错误或完成事件,您可以使用 StreamSubscription 对象:
final subscription = controller.stream.listen((int data){
print(data.toString());
}
subscription.onError((e){
print("An error occurred");
});
subscription.onDone((){
subscription.cancel();
// Cancelling the subscription
});
取消流
手动订阅流时,应确保在 dispose 方法中取消 StreamSubscription。如果您希望在应用程序的整个持续时间内进行流式传输,则可能不需要取消流式传输。您可以像这样取消流:
susbscription.cancel();
Flutter 中的流类型
默认情况下,Flutter 根据它允许的订阅数量有两种类型的 Streams。
- 单订阅流
在这种类型的流中,只有一个侦听器可以侦听流。把它想象成一根有一个开口和一个末端的吸管。它不允许在两者之间进行订阅。用于需要以正确的顺序传递事件而不会丢失任何事件的地方。稍后再收听意味着错过了某些数据,因此它不提供多订阅能力。
单订阅流的语法:
StreamController<double> controller = StreamController<double>();
// One Listener only
final subscriber1 = controller.stream.listen((e){});
2.广播流
在这种类型的流中,多个侦听器可以侦听流。您可以随时开始收听此类流,并且您会收到订阅后发出的事件。即使您错过了以前的一些活动,也与这里无关。
广播流的语法:
StreamController<double> controller = StreamController<double>.broadcast();
// Multiple Listeners allowed
final subscriber1 = controller.stream.listen((e){});
final subscriber2 = controller.stream.listen((e){});
修改流的方法
Flutter 提供了内置的方法来修改流。这些方法提供基于原始流的新流:
Stream<R> cast<R>();
Stream<S> expand<S>(Iterable<S> Function(T element) convert);
Stream<S> map<S>(S Function(T event) convert);
Stream<T> skip(int count);
Stream<T> skipWhile(bool Function(T element) test);
Stream<T> take(int count);
Stream<T> takeWhile(bool Function(T element) test);
Stream<T> where(bool Function(T event) test);
您可以使用以下方法:
StreamController controller = StreamController();
Stream modifiedStream = controller.stream
..where((event) {
return event.toString().length > 10;
})
..map((event) {
return event.toString().toUpperCase();
});
modifiedStream.listen((event) {
print(event.toString());
});
现在修改后的流将只包含使用 where 函数长度超过 10 个字符的事件,并且使用 map 函数将字符大写。类似地,可以使用其他方法来修改流。
使用 transform() 函数修改数据👨
transform() 函数是一种修改流的高级方法。在这里,您可以添加关于如何过滤、转换数据的个性化逻辑。我们像这样使用变换函数:
Stream stream = controller.stream;
final transformedStream = stream.transform(StreamTransformer.fromHandlers(
handleData: (data, sink){
// Add Data Modifying logic here
},
handleError: (error, stackTrace, sink){},
handleDone: (sink){},
));
transformedStream.listen((e){
print("The Event is $e");
});
要复制上述长度大于 10 和大写字母的示例,代码将是:
import 'dart:async';
StreamController<String> controller = StreamController<String>();
Stream<String> transformedStream = controller.stream.transform(
StreamTransformer<String, String>.fromHandlers(
handleData: (String data, EventSink<String> sink) {
// Modify the Data event: If data.length > 10, add capilized data to sink
if (data.length > 10) {
sink.add(data.toUpperCase());
}
},
handleDone: (sink) {
// Modify the Done Event
sink.close();
},
handleError: (error, stacktrace, sink) {
// Modify the Error Event
sink.addError(error);
},
),
);
transformedStream.listen((e)
使用 transform() 方法我们可以轻松地修改数据。 handleData 充当转换器,我们可以在其中修改数据,然后将其推送到 EventSink。同样,如果需要,我们也可以修改错误和完成事件。
在使用 Flutter 中的 Streams 之后,我们使用 StreamBuilder,它是 Flutter 中内置的 widget 来使用 Streams。
StreamBuilder
我们将构建一个应用程序,该应用程序每 5 秒从 API 获取图像 10 次。相同的流将如下所示:
Stream<String> getStream() async* {
int i = 0;
while ( i < 10){
String image = await fetchImage();
await Future.delayed(Duration(seconds: 5);
yield image;
}
}
数据流
我们将使用 yield 和 async* 关键字创建我们的自定义 Stream。 Stream 逻辑在两种实现中都是相同的,因此将其保存在另一个文件中是明智的。此外,在两种实现中用于显示图像的 widget 将是相同的。为了便于理解,我将文件命名为commons.dart,直接放在lib目录下。当我们在做项目时,建议根据任何架构、MVC、MVVM 等将组件划分到文件夹中。但是由于这个项目只是示例并且太小,我将在 lib 目录中声明所有文件。
这是来自 API 端点的响应示例:
{
"url": "https://cdn.catboys.com/images/image_73.jpg",
"artist": "CoverDesign1",
"artist_url": "https://www.deviantart.com/coverdesign1",
"source_url": "https://www.deviantart.com/coverdesign1/art/Artiste-Iya-Chen-Render-396950935",
"error": "none"
}
请注意,API 端点是 get 类型,不需要任何身份验证或任何其他参数。现在让我们创建获取数据流的逻辑:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
// Taking BuildContext to show SnackBar
Stream getDataStream(BuildContext context) async* {
int i = 1;
while (i <= 10) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Getting new Image $i"),
duration: const Duration(seconds: 1),
),
);
http.Response response =
await http.get(Uri.parse("https://api.catboys.com/img"));
Map<String, dynamic> map = json.decode(response.body);
// Return the value received using yield keyword
yield map['url'] as String;
// Delay the next yield by 5 seconds
await Future.delayed(const Duration(seconds: 5));
i++;
}
}
现在我们已经修复了流,让我们看看用我们的 UI 实现它的方法。在此之前,让我们再向 commons.dart 添加一个方法,该方法负责显示图像和 URL:
Column getImageView(String data) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.network(
data,
height: 200,
width: 200,
),
const SizedBox(height: 20),
Center(child: Text(data)),
],
);
}
传统实现
Flutter 中实现 Streams 的传统方式是这样的。我们必须跟踪请求的状态。在这里,我们使用 4 个变量来维护状态,例如:dataAvailable、loading、currentData、error statement。
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:stream_builder_example/commons.dart';
import 'dart:async';
class TraditionalStreamExample extends StatefulWidget {
const TraditionalStreamExample({Key? key}) : super(key: key);
@override
_TraditionalStreamExampleState createState() =>
_TraditionalStreamExampleState();
}
class _TraditionalStreamExampleState extends State<TraditionalStreamExample> {
bool isLoading = false, dataAvailable = false;
String errorStatement = "";
late String currentData;
@override
void initState() {
super.initState();
// Calling PostCallFrameCallback because BuildContext
// is needed in the getDataStream() function
SchedulerBinding.instance!.addPostFrameCallback((_) {
isLoading = true;
setState(() {});
StreamSubscription subs = getDataStream(context).listen(null);
subs.onData(onDataReceived);
subs.onError(onErrorReceived);
subs.onDone(() {
print("ON DONE");
subs.cancel();
});
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Traditional Stream Example")),
body: isLoading
? const Center(
child: CircularProgressIndicator(),
)
: dataAvailable
? getImageView(currentData)
: Center(child: Text(errorStatement)),
);
}
void onDataReceived(data) {
if (data == null || data.toString().isEmpty) {
errorStatement = "No Data Received";
dataAvailable = false;
isLoading = false;
setState(() {});
return;
}
isLoading = false;
dataAvailable = true;
currentData = data;
setState(() {});
}
void onErrorReceived(e) {
isLoading = false;
dataAvailable = false;
errorStatement = e;
setState(() {});
}
}
代码工作正常,但这种方法存在一些问题:
- 我们必须手动维护状态。即错误,数据,加载
- 一次又一次地调用 setState((){})。
- 每个流实现都会附带的样板代码
- 手动订阅和从流中分离。
为了解决所有这些问题,Flutter 有一个在内部使用 Stream 并在内部管理其状态的 widget 。该 widget 是 StreamBuilder,它用于在 Flutter 中有效地使用 Streams。
Flutter 中的 StreamBuilder
StreamBuilder 是一个 widget ,它使用 Stream 对象并根据收到的最新值进行重建。
StreamBuilder 的唯一目的是减少在 Flutter 应用程序中使用 Streams 所涉及的样板文件。
StreamBuilder widget 接收 2 个参数,一个 Stream 和一个 Builder 函数。 StreamBuilder 的构造函数是:
const StreamBuilder({
Key? key,
this.initialData,
Stream<T>? stream,
required this.builder,
})
以下是如何在 Flutter 中实现 StreamBuilder 的示例:
StreamBuilder(
stream: getStream(),
builder: (BuildContext context, AsyncSnapshot data){
if (a.connectionState == ConnectionState.waiting) {
// Show Waiting Indicator
return const Center(child: CircularProgressIndicator());
} else if (a.connectionState == ConnectionState.active || a.connectionState == ConnectionState.done) {
if (a.hasError) {
// Connection Done But Error Occured
return const Center(child: Text("Error Occured"));
}
else if (a.hasData) {
// Connection Done and Data Received
return getImageView(a.data);
}
// Empty Data/ No Data Received
return const Center(child: Text("No Data Received"));
}
return Center(child: Text(a.connectionState.toString()));
},
),
builder 函数中最重要的对象是 AsyncSnapshot 对象。 AsyncSnapshot 包含一些关于连接、数据、错误等的重要数据。它的一些重要字段是:
- connectionState
- hasData
- hasError
- data
- error
ConnectionState:
ConnectionState 枚举可以有 4 个可能的值:
- none,也许有一些初始数据。
- waiting,表示异步操作已经开始,通常数据为空。
- active,数据非空,流已开始产生事件。
- done,流已完成所有收益,现在将没有未来事件。
在 Streams 中,当 ConnectionState 在两种情况下都处于 active 状态或 done 时,这意味着我们收到了数据。这就是为什么在检查快照是否包含数据或错误之前,我们首先检查 ConnectionState 是否处于 active 状态或 done。
if(snapshot.connectionState == ConnectionState.active || snapshot.connectionState == ConnectionState.done){
// Check for Data
}
class StreamBuilderExample extends StatefulWidget {
const StreamBuilderExample({Key? key}) : super(key: key);
@override
State<StreamBuilderExample> createState() => _StreamBuilderExampleState();
}
class _StreamBuilderExampleState extends State<StreamBuilderExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Stream Builder Demo"),
),
body: StreamBuilder(
stream: getDataStream(context),
builder: (context, AsyncSnapshot a) {
if (a.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (a.connectionState == ConnectionState.active ||
a.connectionState == ConnectionState.done) {
if (a.hasError) {
return const Center(child: Text("Error Occured"));
}
if (a.hasData) {
return getImageView(a.data);
}
return const Center(child: Text("No Data Received"));
}
return Center(child: Text(a.connectionState.toString()));
},
),
);
}
}
结论
在这篇文章中,我详细解释了 Flutter & Dart 中的流。我们学习了如何创建、监听和取消流。我们还了解了如何使用预定义的方法和使用 transform() 方法在 Flutter 中修改流。