Flutter-Stream使用

前言

在 Flutter 中有两种处理异步操作的方式 Future 和 StreamFuture 用于处理单个异步操作,Stream 用来处理连续的异步操作。比如往水杯倒水,将一个水杯倒满为一个 Future,连续的将多个水杯倒满就是 Stream

Stream 就是事件流或者管道,事件流相信大家并不陌生,简单的说就是:基于事件流驱动设计代码,然后监听订阅事件,并针对事件变换处理响应

Flutter 中,整个 Stream 设计外部暴露的对象主要如下图,主要包含了 StreamControllerSink 、Stream 、StreamSubscription 四个对象。

 

Flutter中 StreamStreamControllerStreamSinkStreamSubscription 都是 abstract 对象,他们对外抽象出接口,而内部实现对象大部分都是 _ 开头的如 _SyncStreamControllerControllerStream 等私有类,在这基础上整个流程概括起来就是:

有一个事件源叫 Stream,为了方便控制 Stream ,官方提供了使用 StreamController 作为管理;同时它对外提供了 StreamSink 对象作为事件输入口,可通过 sink 属性访问; 又提供 stream 属性提供 Stream 对象的监听和变换,最后得到的 StreamSubscription 可以管理事件的订阅。

所以我们可以总结出:

  • StreamController :如类名描述,用于整个 Stream 过程的控制,提供各类接口用于创建各种事件流。
  • StreamSink:一般作为事件的入口,提供如 addaddStream 等。
  • Stream:事件源本身,一般可用于监听事件或者对事件进行转换,如 listenwhere
  • StreamSubscription:事件订阅后的对象,表面上用于管理订阅过等各类操作,如 cacenlpause ,同时在内部也是事件的中转关键。

回到 Stream 的工作流程上,在上图中我们知道, 通过 StreamSink.add 添加一个事件时, 事件最后会回调到 listen 中的 onData 方法,这个过程是通过 zone.runUnaryGuarded 执行的,这里 zone.runUnaryGuarded 是什么作用后面再说,我们需要知道这个 onData 是怎么来的?

如上图,通过源码我们知道:

  • 1、Streamlisten 的时候传入了 onData 回调,这个回调会传入到 StreamSubscription 中,之后通过 zone.registerUnaryCallback 注册得到 _onData 对象( 不是前面的 onData 回调哦 )。

  • 2、StreamSink 在添加事件是,会执行到 StreamSubscription 中的 _sendData 方法,然后通过 _zone.runUnaryGuarded(_onData, data); 执行 1 中得到的 _onData 对象,触发 listen 时传入的回调方法。

可以看出整个流程都是和 StreamSubscription 相关的,现在我们已经知道从 事件入口到事件出口 的整个流程时怎么运作的,那么这个过程是**怎么异步执行的呢?其中频繁出现的 zone 是什么?

线程

首先我们需要知道,Stream 是怎么实现异步的?

这就需要说到 Dart 中的异步实现逻辑了,因为 Dart 是 单线程应用 ,和大多数单线程应用一样,Dart 是以 消息循环机制 来运行的,而这里面主要包含两个任务队列,一个是 microtask 内部队列,一个是 event 外部队列,而 microtask 的优先级又高于 event

默认的在 Dart 中,如 点击、滑动、IO、绘制事件 等事件都属于 event 外部队列,microtask 内部队列主要是由 Dart 内部产生,而 Stream 中的执行异步的模式就是 scheduleMicrotask 了。

因为 microtask 的优先级又高于 event ,所以如果 microtask 太多就可能会对触摸、绘制等外部事件造成阻塞卡顿哦。

如下图,就是 Stream 内部在执行异步操作过程执行流程:

Zone

那么 Zone 又是什么?它是哪里来的?

在上一篇章中说过,因为 Dart 中 Future 之类的异步操作是无法被当前代码 try/cacth 的,而在 Dart 中你可以给执行对象指定一个 Zone,类似提供一个沙箱环境 ,而在这个沙箱内,你就可以全部可以捕获、拦截或修改一些代码行为,比如所有未被处理的异常。

那么项目中默认的 Zone 是怎么来的?在 Flutter 中,Dart 中的 Zone 启动是在 _runMainZoned 方法 ,如下代码所示 _runMainZoned@pragma("vm:entry-point") 注解表示该方式是给 Engine 调用的,到这里我们知道了 Zone 是怎么来的了。

///Dart 中

@pragma('vm:entry-point')
// ignore: unused_element
void _runMainZoned(Function startMainIsolateFunction, Function userMainFunction) {
  startMainIsolateFunction((){
    runZoned<Future<void>>(····);
  }, null);
}

///C++ 中
if (tonic::LogIfError(tonic::DartInvokeField(
          Dart_LookupLibrary(tonic::ToDart("dart:ui")), "_runMainZoned",
          {start_main_isolate_function, user_entrypoint_function}))) {
    FML_LOG(ERROR) << "Could not invoke the main entrypoint.";
    return false;
}

 那么 zone.runUnaryGuarded 的作用是什么?相较于 scheduleMicrotask 的异步操作,官方的解释是:在此区域中使用参数执行给定操作并捕获同步错误。 类似的还有 runUnaryrunBinaryGuarded 等,所以我们知道前面提到的 zone.runUnaryGuarded 就是 Flutter 在运行的这个 zone 里执行已经注册的 _onData,并捕获异常

异步和同步

前面我们说了 Stream 的内部执行流程,那么同步和异步操作时又有什么区别?具体实现时怎么样的呢?

我们以默认 Stream 流程为例子, StreamController 的工厂创建可以通过 sync 指定同步还是异步,默认是异步模式的。 而无论异步还是同步,他们都是继承了 _StreamController 对象,区别还是在于 mixins 的是哪个 _EventDispatch 实现:

  • _AsyncStreamControllerDispatch

  • _SyncStreamControllerDispatch

上面这两个 _EventDispatch 最大的不同就是在调用 sendData 提交事件时,是直接调用 StreamSubscription_add 方法,还是调用 _addPending(new _DelayedData<T>(data)); 方法的区别。

如下图, 异步执行的逻辑就是上面说过的 scheduleMicrotask, 在 _StreamImplEventsscheduleMicrotask 执行后,会调用 _DelayedDataperform ,最后通过 _sendData 触发 StreamSubscription 去回调数据 。

 

广播和非广播

Stream 中又非为广播和非广播模式,如果是广播模式中,StreamControlle 的实现是由如下所示实现的,他们的基础关系如下图所示:

  • _SyncBroadcastStreamController

  • _AsyncBroadcastStreamController

 广播和非广播的区别在于调用 _createSubscription 时,内部对接口类 _StreamControllerLifecycle 的实现,同时它们的差异在于:

  • _StreamController 里判断了如果 Stream_isInitialState 的,也就是订阅过的,就直接报错 "Stream has already been listened to." ,只有未订阅的才创建 StreamSubscription

  • _BroadcastStreamController 中,_isInitialState 的判断被去掉了,取而代之的是 isClosed 判断,并且在广播中, _sendData 是一个 forEach 执行:

  _forEachListener((_BufferingStreamSubscription<T> subscription) {
      subscription._add(data);
    });

Stream 变换

Stream 是支持变换处理的,针对 Stream 我们可以经过多次变化来得到我们需要的结果。那么这些变化是怎么实现的呢?

如下图所示,一般操作符变换的 Stream 实现类,都是继承了 _ForwardingStream , 在它的内部的_ForwardingStreamSubscription 里,会通过上一个 Pre A Streamlisten 添加 _handleData 回调,之后在回调里再次调用新的 Current B Stream_handleData

所以事件变化的本质就是,变换都是对 Streamlisten 嵌套调用组成的。

 同时 Stream 还有转换为 Future , 如 firstWhereelementAtreduce 等操作符方法,基本都是创建一个内部 _Future 实例,然后再 listen 的回调用调用 Future 方法返回。

StreamBuilder

如下代码所示, 在 Flutter 中通过 StreamBuilder 构建 Widget ,只需提供一个 Stream 实例即可,其中 AsyncSnapshot 对象为数据快照,通过 data 缓存了当前数据和状态,那 StreamBuilder 是如何与 Stream 关联起来的呢?

StreamBuilder<List<String>>(
    stream: dataStream,
    initialData: ["none"],
    ///这里的 snapshot 是数据快照的意思
    builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) {
      ///获取到数据,为所欲为的更新 UI
      var data = snapshot.data;
      return Container();
    });

 如上图所示, StreamBuilder 的调用逻辑主要在 _StreamBuilderBaseState 中,_StreamBuilderBaseStateinitStatedidUpdateWidget 中会调用 _subscribe 方法,从而调用 Streamlisten,然后通过 setState 更新UI,就是这么简单有木有?

我们常用的 setState 中其实是调用了 markNeedsBuildmarkNeedsBuild 内部标记 elementdiry ,然后在下一帧 WidgetsBinding.drawFrame 才会被绘制,这可以看出 setState 并不是立即生效的哦。

rxdart

其实无论从订阅或者变换都可以看出, Dart 中的 Stream 已经自带了类似 rx 的效果,但是为了让 rx 的用户们更方便的使用,ReactiveX 就封装了 rxdart 来满足用户的熟悉感,如下图所示为它们的对应关系:

 在 rxdart 中, Observable 是一个 Stream,而 Subject 继承了 Observable 也是一个 Stream,并且 Subject 实现了 StreamController 的接口,所以它也具有 Controller 的作用。

如下代码所示是 rxdart 的简单使用,可以看出它屏蔽了外界需要对 StreamSubscriptionStreamSink 等的认知,更符合 rx 历史用户的理解。

final subject = PublishSubject<String>();

subject.stream.listen(observerA);
subject.add("AAAA1");
subject.add("AAAA2"));

subject.stream.listen(observeB);
subject.add("BBBB1");
subject.close();

这里我们简单分析下,以上方代码为例,

  • PublishSubject 内部实际创建是创建了一个广播 StreamController<T>.broadcast 

  • 当我们调用 add 或者 addStream 时,最终会调用到的还是我们创建的 StreamController.add

  • 当我们调用 onListen 时,也是将回调设置到 StreamController

  • rxdart 在做变换时,我们获取到的 Observable 就是 this,也就是 PublishSubject 自身这个 Stream ,而 Observable 一系列的变换,也是基于创建时传入的 stream 对象,比如:

  @override
  Observable<S> asyncMap<S>(FutureOr<S> convert(T value)) =>
      Observable<S>(_stream.asyncMap(convert));

所以我们可以看出来,rxdart 只是对 Stream 进行了概念变换,变成了我们熟悉的对象和操作符,而这也是为什么 rxdart 可以在 StreamBuilder 中直接使用的原因。

Stream分类

流可以分为两类:

  • 单订阅流(Single Subscription),这种流最多只能有一个监听器(listener)
  • 多订阅流(Broadcast),这种流可以有多个监听器监听(listener)

Stream创建与监听的方式

  • periodic创建,listen监听

  • streamController创建,listen或 StreamBuilder监听

Controller.sink.add添加事件
addError
Controller.close关闭水龙头

  1. 创建 StreamController ,
  2. 然后获取 StreamSink 用做事件入口,
  3. 获取 Stream 对象用于监听,
  4. 并且通过监听得到 StreamSubscription 管理事件订阅,最后在不需要时关闭
  • streamController.broadcast广播

可以在多处监听它返回的结果

Stream.asBroadcastStream() 可以将一个单订阅模式的 Stream 转换成一个多订阅模式的 Stream

  • async*

在streamController传入使用

Stream常用操作符

  • map 包装转化

  • where 过滤

  • distinct去重

  • Stream.take(int count)

    上面创建了一个无限每隔一秒发送一次事件的流,如果我们想指定只发送10个事件则,用take。下面就只会打印出0-9

    void _stream() async{
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10); //指定发送事件个数
        await for(int i in stream ){
         print(i);
        }
    }
    

  • Stream.takeWhile

    上面这种方式我们是只制定了发送事件的个数,如果我们也不知道发送多少个事件,我们可以从返回的结果上做一个返回值的限制,上面结果也可以用以下方式实现

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
    //    stream = stream.take(10);
        stream = stream.takeWhile((data) {
          return data < 10;
        });
        await for (int i in stream) {
          print(i);
        }
      }
    

  • Stream.skip(int count)

    skip可以指定跳过前面的几个事件,如下会跳过0和1,输出 2-9;

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream = stream.skip(2);
        await for (int i in stream) {
          print(i);
        }
      }
    

  • Stream.skipWhile

    可以指定跳过不发送事件的指定条件,如下跳过0-4的输出,输出5-9

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream = stream.skipWhile((data) => data<5);
        await for (int i in stream) {
          print(i);
        }
      }

  • Stream.toList()

    将流中所有的数据收集存放在List中,并返回 Future对象,listData里面 0-9

    1.这个是一个异步方法,要结果则需要使用await关键字

    2.这个是等待Stream当流结束时,一次返回结果

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        List<int> listData = await stream.toList();
        for (int i in listData) {
          print(i);
        }
      }

  • Stream. listen()

    这是一种特定的可以用于监听数据流的方式,和 forEach循环的效果一致,但是返回的是StreamSubscription<T>对象,如下也会输出0-9,同时打印出 ”流已完成“

    看一下源码这种方式可以接收

    StreamSubscription<T> listen(void onData(T event),
          {Function onError, void onDone(), bool cancelOnError});
    

    1.onData是接收到数据的处理,必须要实现的方法

    2.onError流发生错误时候的处理

    3.onDone流完成时候调取

    4.cancelOnError发生错误的时候是否立马终止

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream.listen((data) {
          print(data);
        }, onError: (error) {
          print("流发生错误");
        }, onDone: () {
          print("流已完成");
        }, cancelOnError: false);
      }
    

  • Stream. forEach()

    这中操作和listen()的方式基本差不多,也是一种监听流的方式,这只是监听了onData,下面代码也会输出0-9

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        stream.forEach((data) {
          print(data);
        });
      }
    

  • Stream .length

    用于获取等待流中所有事件发射完成之后统计事件的总数量,下面代码会输出 10

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream.periodic(interval, (data) => data);
        stream = stream.take(10);
        var allEvents = await stream.length;
        print(allEvents);
      }
    

  • Stream.where

    在流中添加筛选条件,过滤掉一些不想要的数据,满足条件返回true,不满足条件返回false,如下我们筛选出流中大于5小于10的数据

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream<int>.periodic(interval, (data) => data);
        stream = stream.where((data)=>data>5);
        stream = stream.where((data)=> data<10);
        await for(int i in stream){
          print(i);
        }
      }
    

  • stream.map

    对流中的数据进行一些变换,以下是我对Stream的每个数据都加1

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream<int> stream = Stream<int>.periodic(interval, (data) => data);
        stream = stream.map((data) => data + 1);
        await for (int i in stream) {
          print(i);
        }
      }
    

  • Stream.expand

    对流中的数据进行一个扩展,如下,会输出1,1,2,2,3,3….

    void _stream() async {
        Duration interval = Duration(seconds: 1);
        Stream stream = Stream.periodic(interval, (data) => data);
        stream = stream.expand((data)=>[data,data]);
        stream.listen((data)=>print(data),onError:(error)=> print("发生错误") );
      }
    
  • Stream.transform

    如果我们在在流流转的过程中需要进行一些转换和控制我们则需要使用到transform,接收一个

    StreamTransformer<S,T>,S表示转换之前的类型,T表示转换后的输入类型,如下代码我们会接收到三组数字模拟输入了三次密码,并判断真确的密码,同时输出密码正确和密码错误:

     void _stream() async {
        var stream = Stream<int>.fromIterable([123456,234567,678901]);
        var st = StreamTransformer<int, String>.fromHandlers(
            handleData: (int data, sink) {
          if (data == 678901) {
            sink.add("密码输入正确,正在开锁。。。");
          } else {
            sink.add("密码输入错误...");
          }
        });
        stream.transform(st).listen((String data) => print(data),
            onError: (error) => print("发生错误"));
      }
    

    输入如下结果

    I/flutter (18980): 密码输入错误...
    I/flutter (18980): 密码输入错误...
    I/flutter (18980): 密码输入正确,正在开锁。。。
    
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Flutter中的MultipartFile是用于表示一个文件,通常用于文件上传。你可以使用Dart的http包来创建和发送MultipartFile对象。 首先,你需要导入http包: ```dart import 'package:http/http.dart' as http; ``` 然后,你可以使用http.MultipartFile构造函数来创建一个MultipartFile对象。构造函数接受以下参数: - `String field`:字段名称,用于在上传时标识文件。 - `Stream<List<int>> stream`:文件内容的字节流。 - `int length`:文件内容的长度。 - `String filename`:文件名。 下面是一个示例代码,展示如何创建一个MultipartFile对象并将其作为表单数据发送到服务器: ```dart import 'package:http/http.dart' as http; import 'dart:io'; void main() async { var file = File('path_to_file'); // 替换为实际文件路径 var stream = http.ByteStream(file.openRead()); var length = await file.length(); var uri = Uri.parse('http://example.com/upload'); // 替换为实际的上传地址 var request = http.MultipartRequest('POST', uri); var multipartFile = http.MultipartFile('file', stream, length, filename: 'example.jpg'); request.files.add(multipartFile); var response = await request.send(); if (response.statusCode == 200) { print('File uploaded successfully'); } else { print('Error uploading file'); } } ``` 请记得将`path_to_file`替换为你要上传的文件的实际路径,以及将`http://example.com/upload`替换为实际的上传地址。 希望对你有所帮助!如有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值