Flutter ——状态管理 | StreamBuild

1.什么是stream?

StreamBuild从字面意思来讲是数据流构建,是一种基于数据流的订阅管理。Stream可以接受任何类型的数据,值、事件、对象、集合、映射、错误、甚至是另一个Stream,通过StreamController中的sink作为入口,往Stream中插入数据,然后通过你的自定义监听StreamSubscription对象,接受数据变化的通知。如果你需要对输出数据进行处理,可以使用StreamTransformer,它可以对输出数据进行过滤、重组、修改、将数据注入其他流等等任何类型的数据操作。

2.stream都有哪些类型

Stream有两种类型:单订阅Stream和广播Stream。单订阅Stream只允许在该Stream的整个生命周期内使用单个监听器,即使第一个subscription被取消了,你也没法在这个流上监听到第二次事件;而广播Stream允许任意个数的subscription,你可以随时随地给它添加subscription,只要新的监听开始工作流,它就能收到新的事件。

2.1 单订阅类型实例

import 'dart:async';

void main() {
  // 初始化一个单订阅的Stream controller
  final StreamController ctrl = StreamController();
  
  // 初始化一个监听
  final StreamSubscription subscription = ctrl.stream.listen((data) => print('$data'));

  // 往Stream中添加数据
  ctrl.sink.add('my name');
  ctrl.sink.add(1234);
  ctrl.sink.add({'a': 'element A', 'b': 'element B'});
  ctrl.sink.add(123.45);
  
  // StreamController用完后需要释放
  ctrl.close();
}

2.2广播类stream

import 'dart:async';

void main() {
  // 初始化一个int类型的广播Stream controller
  final StreamController<int> ctrl = StreamController<int>.broadcast();
  
  // 初始化一个监听,同时通过transform对数据进行简单处理
  final StreamSubscription subscription = ctrl.stream
                          .where((value) => (value % 2 == 0))
                          .listen((value) => print('$value'));

  // 往Stream中添加数据
  for(int i=1; i<11; i++){
    ctrl.sink.add(i);
  }
  
  // StreamController用完后需要释放
  ctrl.close();
}

3.stream有哪些好处?

3.1.随意操作数据流。

刚才在stream定义那里已经说过了,stream是基于数据流的,从skin管道入口到StreamController提供stream属性作为数据的出口之间,可以对数据做任何操作,包括过滤、重组、修改等等。

3.2 当数据流变化时,可以刷新小部件。

Stream是一种订阅者模式,当数据发生变化时,通知订阅者发生改变,重新构建小部件,刷新UI。

4.如何使用streamBuild?

  StreamBuilder<T>(
    key: ...可选...
    stream: ...需要监听的stream...
    initialData: ...初始数据,尽量不要填null...
    builder: (BuildContext context, AsyncSnapshot<T> snapshot){
        if (snapshot.hasData){
            return ...基于snapshot.hasData返回的控件
        }
        return ...没有数据的时候返回的控件
    },
)

下面是一个模仿官方自带demo“计数器”的一个例子,使用了StreamBuilder,而不需要任何setState:
我在代码里注释了步骤(四步):

import 'dart:async';
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  //步骤1:初始化一个StreamController<任何数据> 简单的可以扔一个int,string,开发中经常扔一个网络请求的model进去,具体看你使用场景了。
  final StreamController<int> _streamController = StreamController<int>();

  @override
  void dispose(){
  //步骤2.关流,不管流会消耗资源,同时会引起内存泄漏
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Stream version of the Counter App')),
      body: Center(
      //步骤3.使用StreamBuilder构造器
        child: StreamBuilder<int>(  // 监听Stream,每次值改变的时候,更新Text中的内容
          stream: _streamController.stream,
          initialData: _counter,
          builder: (BuildContext context, AsyncSnapshot<int> snapshot){
            return Text('You hit me: ${snapshot.data} times');
          }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          // 每次点击按钮,更加_counter的值,同时通过Sink将它发送给Stream;
          // 每注入一个值,都会引起StreamBuilder的监听,StreamBuilder重建并刷新counter
          //步骤4.往StreamBuilder里添加流,数据变了,就用通知小部件
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}

5.具体如何使用?

刚刚介绍了stream的如何使用,是不是感觉还是懵的状态,实例代码仅仅是实例,如何应用到项目中呢?我们的项目不仅仅是一个简单的计数器,接下来我将结合项目,简单讲述一下如何使用streamBuild。这是我司的一张UI。

UI.png

要求点击“关注”变为“已关注”
如何去实现的?实现的方法有好多种。
1.这个item是StatefulWidget,点击“关注”,然后setstate(){}
2.使用其他的状态管理去实现。如 【 ScopedModel 】 【 Provide 】 【Bloc】
3.使用 StreamBuild 实现。

我选择使用StreamBuild去实现,稍后我会解释为何要用streambuild 去实现。

import 'dart:async';
import 'package:easy_alert/easy_alert.dart';
import 'package:flutter/material.dart';
import 'package:hongka_flutter/app/Manager/IO/hk_request.dart';
import 'package:hongka_flutter/app/Manager/api/ConfigApi.dart';
import 'package:hongka_flutter/app/Modules/basemodel/focuseItemModel.dart';
import 'package:hongka_flutter/app/Modules/home/info_organization.dart';
//我会省略部分代码,并且注释使用步骤
//步骤一:使用 StatefulWidget,为何要用StatefulWidget?待会解释
class FollowsItem extends StatefulWidget {
  FocuseItemModel focusItemModel;
  String focusType; //不为空就是关注界面,隐藏关注按钮
  int index;
  String studentId;

  FollowsItem(
      {Key key,
      this.focusItemModel,
      this.focusType,
      this.index,
      this.studentId});

  @override
  _FollowsItemState createState() => _FollowsItemState();
}

class _FollowsItemState extends State<FollowsItem> {
  FocuseItemModel focusItemModel;
  String focusType; //不为空就是关注界面,隐藏关注按钮
  String headerImage;
  double screenWidth, marginLeft = 15.0, marginRight = 15.0, marginAll = 30.0;
  int index;
  String studentId;
  //步骤二:声明StreamController
  StreamController<FocuseItemModel> _streamController;

  @override
  void initState() {
    super.initState();
    this.focusItemModel = this.widget.focusItemModel;
    this.focusType = this.widget.focusType;
    this.index = this.widget.index;
    this.studentId = this.widget.studentId;
    //步骤三实现 StreamController<FocuseItemModel>,FocuseItemModel是我的实体类
    _streamController = StreamController<FocuseItemModel>.broadcast();
    //步骤四将数据添加到 _streamController
    _streamController.sink.add(focusItemModel);
  }

  @override
  void dispose() {
  //步骤五:关流
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _clickItem(context);
      },
      child: Container(
        .....省略无关UI代码
        child: Stack(
          children: <Widget>[
            .....省略无关UI代码
            Positioned(
              right: 0,
              top: 0,
              child: Offstage(
                  offstage: !(focusType == null),
                  child: GestureDetector(
                    onTap: () {
                    //已关注return
                      if (focusItemModel.focusStatus == 1) {
                        return;
                      }
                      //未关注进行网络请求
                      focusOrganizationRequest(focusItemModel.positionId);
                    },
                    //步骤五:使用StreamBuilder构建
                    child: StreamBuilder<FocuseItemModel>(
                        stream: _streamController.stream,//数据流
                        initialData: focusItemModel,//初始值
                        builder: (BuildContext context,
                            AsyncSnapshot<FocuseItemModel> snapshot) {
                          return Image.asset(
                            (snapshot?.data?.focusStatus ?? 0) == 1
                                ? 'images/view/recommend_flowered.png'
                                : 'images/view/recommend_flower.png',
                            width: 54,
                            height: 40,
                          );
                        }),
                  )),
            )
          ],
        ),
      ),
    );
  }

  ///关注红广号
  Future focusOrganizationRequest(String positionId) async {
    String url = 'xxxx';
    var data = {
      'studentId': studentId,
      'positionId': positionId,
    };
    var options = await HK_Request().getRequestOptions();
    var response = await HK_Request()
        .post(url, context, isShowLoading: true, data: data, options: options);
    print(response.toString());
    if (response['status'] == 200) {
    //步骤六:改变model值,并将model 扔到_streamController里,此时数据改变,通知小部件,重新构建
      focusItemModel.focusStatus = 1;
      _streamController.sink.add(focusItemModel);
      Alert.toast(context, '关注成功!',
          position: ToastPosition.center, duration: ToastDuration.short);
      print('关注红广号成功' + response.toString());
    
    } else {
      print('关注红广号错误');
      Alert.toast(context, response['msg'],
          position: ToastPosition.center, duration: ToastDuration.short);
    }
  }

model值改变,streamBuild 通知小部件,并刷新小部件。

问题1 为何选择使用streamBuild

1.方法一使用StatefulWidget,刷新时使用setstate(){},使用setstate(){}刷新,会将整个item 进行重新构建,整个item 仅仅只有“关注”需要改变,其它控件都刷新,会造成资源浪费。
2.方法二使用状态管理bloc,如果使用了bloc,streamBuild中的stream 就因该传bloc< AModel >的数据,如果我其它地方使用也使用了这个item,那么这个stream就应该传bloc< BMode >,此时streamBuild中的stream 类型就不匹配了,这个item 就无法复用了,所以我放弃使用bloc等状态管理
3.为何item 最外层使用StatefulWidget?不是使用streamBuild 就可以不用使用StatefulWidget了吗?
的确使用streamBuild,就可以不使用StatefulWidget。但是 不用StatefulWidget,如何关流? StatelessWidget 没有dispose()方法,不能关流,所以此时还需要使用StatefulWidget。

 @override
  void dispose() {
  //步骤五:关流
    _streamController.close();
    super.dispose();
  }

有群友提出,可以将“关注”的属性提取出来,单独一个bloc去管理,我觉得为了一个按钮的改变,去做很多操作,有点不值得了。当然有兴趣的可以去实现一下。

问题2.怎样才能不使用StatefulWidget?

bloc+streamBuild,此时的stream是bloc里的,不需要在dispose()方法中去关流,这样就可以放弃使用StatefulWidget了。

6. bloc结合streamBuild 实现状态管理会在下一篇内容中讲解。

本人对于 streamBuild 理解的也不是很深刻,没有往太细节去讲解,只是结合自己的项目去讲解了开发中遇到的问题,希望大家提提意见,共同进步。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容