flutter stream

792 阅读6分钟

在 Flutter 中编写异步代码,可以使用 Futures 或 Streams 两种方法。异步代码基本上允许在等待另一个操作完成时运行语句。它确保应用程序不会被阻止,并且可以在另一个操作完成时更新 UI。

使用 Streams 的一些示例是:

  • 获取音频播放器的持续更新。 (播放、停止、暂停)
  • 用户在键盘上打字也是一个字符数据流。
  • 如果用户通过验证,则登录时每 3 秒检查一次。

Flutter 中的 Stream 是什么?

Stream 基本上是一系列异步事件。将 Streams 概念化的最简单方法是将其视为管道,我们将事件从一端放置并从另一端接收。

Stream 可以包含数据事件,也可以包含错误事件。当发射完成时,它会传递一个特殊的完成信号,通知监听器没有留下任何事件。使用 Stream 的好处是它减少了前端和后端之间的耦合。发送者只需将值放入 Stream 中,而不用担心侦听器。接收者只是监听 Stream ,并不关心它的实现。这导致了小规模的抽象。

Dart 中的 Streams 概念有 4 个主要组件:

  • Stream:表示异步数据流。侦听器可以侦听流并收到任何事件更新的通知。
  • EventSink:它允许我们将事件添加到流中。它可以是数据事件或错误事件。
  • StreamController:它简化了在 dart 中使用流。它管理流、接收器并管理流事件。
  • StreamSubscription:当我们订阅任何流时,我们会得到一个流订阅对象,我们可以使用它来暂停、恢复或取消我们收到的数据流。

我们不会主要创建自己的流或接收器,StreamController 会为我们做这些。当我们收听任何流时,我们必须确保保存流订阅并在不需要流时取消。

image.png

在 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。

image.png

  1. 单订阅流

在这种类型的流中,只有一个侦听器可以侦听流。把它想象成一根有一个开口和一个末端的吸管。它不允许在两者之间进行订阅。用于需要以正确的顺序传递事件而不会丢失任何事件的地方。稍后再收听意味着错过了某些数据,因此它不提供多订阅能力。

单订阅流的语法:

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 包含一些关于连接、数据、错误等的重要数据。它的一些重要字段是:

  1. connectionState
  2. hasData
  3. hasError
  4. data
  5. 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
}

image.png

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 中修改流。