Flutter状态管理4-flutter_bloc使用和原理学习总结-程序员宅基地

技术标签: dart  flutter  Flutter️Dart  

flutter_bloc今天发布了4.0.0版本,现关于其使用和原理做一个简单的总结。

Flutter状态管理系列:
Flutter状态管理1-ChangeNotifierProvider的使用
Flutter状态管理2-ChangeNotifierProxyProvider
Flutter状态管理3-InheritedWidget

flutter_bloc官网:
https://github.com/felangel/bloc
https://bloclibrary.dev/#/flutterbloccoreconcepts?id=flutter-bloc-core-concepts
pub.dev上的介绍,包括了多个Examples
https://pub.dev/packages/flutter_bloc

flutter_bloc 4.0.0内部 使用了provider状态管理框架4.0.5版本,对外提供的相关API也类似,同时flutter_bloc基于了Stream做了一些包装,并在内部使用了rxDart,归根到底还是一个观察者模式的使用,通过该方式可以将原来的StatefulWidget转换成StatelessWidget,避免了多次调用setState()导致的性能损耗。

这里以Counter为例子,简单介绍其使用:
https://bloclibrary.dev/#/fluttercountertutorial

一 使用篇

0 添加依赖

这里介绍一个库equatable,可以在使用对象比较时简化我们定义类的“==”和“hashCode”工作
https://pub.dev/packages/equatable

name: flutter_counter
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.0.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^4.0.0
  meta: ^1.1.6
  equatable: ^1.1.1

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

1 定义Event类型

由于bloc(Business Logic Component)其实会将事件Event的变化映射到状态State的变化(通过自定义Bloc来完成两者的转化),来将业务逻辑Business Logic从UI层抽离出来,所以我们需要先定义事件Event。

enum CounterEvent {
     increment, decrement }

官网的Counter例子使用的是枚举类型定义事件类,我们也可以按照Flutter Bloc构建轻量级MVVM中的方式来定义。

abstract class CounterEvent extends Equatable {
    
  const CounterEvent();
}

//点位增加 传入某个值,做更新操作
class ButtonPressAddX extends CounterEvent {
    
  final int x;

  const ButtonPressAddX({
    
    @required this.x,
  });

  @override
  // TODO: implement props
  List<Object> get props => [x];

  @override
  String toString() {
    
    return 'ButtonPressAddX { x: $x }';
  }
}

//点位数值整体重置,所以传入整个状态对象, event 本身不对值进行篡改
class ButtonPressedAdd extends CounterEvent {
    
  final PointState point;

  const ButtonPressedAdd({
    
    @required this.point,
  });

  @override
  List<Object> get props => [point];

  @override
  String toString() => getString();

  String getString() {
    
    var x = point.x;
    var y = point.y;
    var z = point.z;
    return 'ButtonPressedAdd { x: $x, y: $y , z: $z }';
  }
}

class ButtonPressedReduce extends CounterEvent {
    
  final PointState point;

  const ButtonPressedReduce({
    
    @required this.point,
  });

  @override
  List<Object> get props => [point];

  @override
  String toString() => getString();

  String getString() {
    
    var x = point.x;
    var y = point.y;
    var z = point.z;
    return 'ButtonPressedReduce { x: $x, y: $y , z: $z }';
  }
}

2 定义State类型

官网的Counter例子里的State是一个int值,所以不需要再自定义类型,当然我们可以自定义一个类:

abstract class MyState extends Equatable {
    
    const MyState();
}

class StateA extends MyState {
    
    final String property;

    const StateA(this.property);

    @override
    List<Object> get props => [property]; // pass all properties to props
}

3 定义Bloc类型

这里以官网的Counter例子为例,在自定义的Bloc中完成事件Event和State状态的转换,这里通过复写两个方法,一个初始化状态的方法,一个mapEventToState转换方法,注意在该方法中每次都要返回一个新的State对象:

@override
Stream<MyState> mapEventToState(MyEvent event) async* {
    
    // always create a new instance of the state you are going to yield
    yield state.copyWith(property: event.property);
}


@override
Stream<MyState> mapEventToState(MyEvent event) async* {
    
    final data = _getData(event.info);
    // always create a new instance of the state you are going to yield
    yield MyState(data: data);
}

或者按照Flutter Bloc构建轻量级MVVM中的方式来定义CounterBloc:


class CounterBloc extends Bloc<CounterEvent, PointState> {
    
  var point;

  CounterBloc({
    this.point});

  //初始化 状态机 如果没传参 就构建一个(0,0,0)的对象
  @override
  PointState get initialState => point != null ? point : PointState(0, 0, 0);

  @override
  Stream<PointState> mapEventToState(CounterEvent event) async* {
    
    PointState currentState = PointState(state.x, state.y, state.z);
    if (event is ButtonPressedReduce) {
    
      currentState.x = state.x - 1;
      currentState.y = event.point.y;
      currentState.z = state.z - 1;
      yield currentState;
    } else if (event is ButtonPressedAdd) {
    
      currentState.x = state.x + 1;
      currentState.y = event.point.y;
      currentState.z = state.z + 1;
      yield currentState;
      //重置状态
    } else if (event is ButtonPressReset) {
    
      yield PointState.reset();
      //单点位数值增加
    } else if (event is ButtonPressAddX) {
    
      yield state.update(event.x, currentState.y, currentState.z);
    }
  }
}

4 使用BlocProvider和BlocBuilder

使用BlocProvider在需要该Bloc的Widget Tree上进行包裹,这里直接放到了整个页面上,可以根据需要放到适当的Widget Tree层级上,同时这里的BlocProvider也自动处理了CounterBloc的关闭操作,所以我们不必使用一个StatefulWidget。

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
    
  @override
  Widget build(BuildContext context) {
    
    return MaterialApp(
      title: 'Flutter Demo',
      home: BlocProvider<CounterBloc>(
        create: (context) => CounterBloc(),
        child: CounterPage(),
      ),
    );
  }
}

然后就是在页面中使用BlocBuilder,我们通过扩展函数的形式或者使用BlocProvider.of(context)获取CounterBloc,然后在builder函数中,使用当前的State对象值count,并通过counterBloc对象进行事件Event变化的操作:

class CounterPage extends StatelessWidget {
    
  @override
  Widget build(BuildContext context) {
    
  //这里也可以使用扩展函数的形式 二选一
    final CounterBloc counterBloc = context.bloc<BlocA>()
    final CounterBloc counterBloc = BlocProvider.of<CounterBloc>(context);

    return Scaffold(
      appBar: AppBar(title: Text('Counter')),
      body: BlocBuilder<CounterBloc, int>(
        builder: (context, count) {
    
          return Center(
            child: Text(
              '$count',
              style: TextStyle(fontSize: 24.0),
            ),
          );
        },
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.add),
              onPressed: () {
    
                counterBloc.add(CounterEvent.increment);
              },
            ),
          ),
          Padding(
            padding: EdgeInsets.symmetric(vertical: 5.0),
            child: FloatingActionButton(
              child: Icon(Icons.remove),
              onPressed: () {
    
                counterBloc.add(CounterEvent.decrement);
              },
            ),
          ),
        ],
      ),
    );
  }
}

官方例子的全部代码见:
https://github.com/felangel/bloc/blob/master/packages/flutter_bloc/example/lib/main.dart
在例子中还使用了Bloc来控制主题theme实现darkMode的切换。

5 BlocListener、BlocConsumer和RepositoryProvider

两个关于BlocListener的使用例子:
https://bloclibrary.dev/#/recipesfluttershowsnackbar
https://bloclibrary.dev/#/recipesflutternavigation
如果要在State变化时,除了更新UI之外还要做一些其他的事情,那么这时候就可以使用BlocListener,BlocListener包含了一个listener用以做除UI更新之外的事情,该逻辑不能放到BlocBuilder里的builder中,因为这个方法会被Flutter框架调用多次,builder方法应该只是一个返回Widget的函数。

class Home extends StatelessWidget {
    
  @override
  Widget build(BuildContext context) {
    
    final dataBloc = BlocProvider.of<DataBloc>(context);
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: BlocListener<DataBloc, DataState>(
        listener: (context, state) {
    
          if (state is Success) {
    
            Scaffold.of(context).showSnackBar(
              SnackBar(
                backgroundColor: Colors.green,
                content: Text('Success'),
              ),
            );
          }
        },
        child: BlocBuilder<DataBloc, DataState>(
          builder: (context, state) {
    
            if (state is Initial) {
    
              return Center(child: Text('Press the Button'));
            }
            if (state is Loading) {
    
              return Center(child: CircularProgressIndicator());
            }
            if (state is Success) {
    
              return Center(child: Text('Success'));
            }
          },
        ),
      ),
      floatingActionButton: Column(
        crossAxisAlignment: CrossAxisAlignment.end,
        mainAxisAlignment: MainAxisAlignment.end,
        children: <Widget>[
          FloatingActionButton(
            child: Icon(Icons.play_arrow),
            onPressed: () {
    
              dataBloc.add(FetchData());
            },
          ),
        ],
      ),
    );
  }
}

这三者的使用可以见官网介绍:
https://bloclibrary.dev/#/flutterbloccoreconcepts
https://pub.dev/packages/flutter_bloc

6 Bloc代码生成插件的使用

Bloc Code Generator可以方便的生成构建Bloc的模版代码,在项目工程上右键,New -> New Bloc -> Generate New Bloc
在这里插入图片描述
一些命名State和Event的公约:https://bloclibrary.dev/#/blocnamingconventions

7 使用注意事项 与provider和Redux的对比

简要介绍一下和provider的关系:
provider被设计用于依赖项注入(其通过包装InheritedWidget实现)。您仍然需要弄清楚如何管理状态(通过ChangeNotifier,Bloc,Mobx等)。
Bloc库在内部使用provider以轻松在整个Widget 树中提供和访问bloc。

首先BlocProvider继承了SingleChildStatelessWidget, 复写的buildWithChild中返回了一个InheritedProvider,而InheritedProvider就是provider库中的API,

class BlocProvider<T extends Bloc<dynamic, dynamic>>
    extends SingleChildStatelessWidget with BlocProviderSingleChildWidget {
    
...
 @override
  Widget buildWithChild(BuildContext context, Widget child) {
    
    return InheritedProvider<T>(
      create: _create,
      dispose: _dispose,
      child: child,
      lazy: lazy,
    );
  }
...
}

参考:https://bloclibrary.dev/#/faqs

8 如何使用Bloc构建一个架构

首先上面的例子中Bloc的使用都局限在一个页面中 ,那么在多个页面之间甚至是整个app中,如何共享同一个Bloc呢,这里可以参考:https://bloclibrary.dev/#/recipesflutterblocaccess

参考:
Flutter Architecture Samples - Brian Egan
Flutter Shopping Card Example
Flutter TDD Course - ResoCoder

9 测试相关

由于Bloc的设计就是将业务逻辑从UI层中抽离,所以测试也变得更加容易,首线添加测试用的库:

dev_dependencies:
  test: ^1.3.0
  bloc_test: ^5.0.0

详细步骤参考:https://bloclibrary.dev/#/testing

二 原理篇

看一下Bloc的源码,发现它是继承Stream的,而什么是Stream呢,可以看文末的参考一,

abstract class Bloc<Event, State> extends Stream<State> implements Sink<Event> {
    
  final PublishSubject<Event> _eventSubject = PublishSubject<Event>();

  BehaviorSubject<State> _stateSubject;

  /// Returns the current [state] of the [bloc].
  State get state => _stateSubject.value;

  /// Returns the [state] before any `events` have been [add]ed.
  State get initialState;

  /// Returns whether the `Stream<State>` is a broadcast stream.
  @override
  bool get isBroadcast => _stateSubject.isBroadcast;

  /// {@macro bloc}
  Bloc() {
    
    _stateSubject = BehaviorSubject<State>.seeded(initialState);
    _bindStateSubject();
  }
...

  void _bindStateSubject() {
    
    Event currentEvent;

    transformStates(transformEvents(_eventSubject, (Event event) {
    
      currentEvent = event;
      return mapEventToState(currentEvent).handleError(_handleError);
    })).forEach(
      (State nextState) {
    
        if (state == nextState || _stateSubject.isClosed) return;
        final transition = Transition(
          currentState: state,
          event: currentEvent,
          nextState: nextState,
        );
        try {
    
          BlocSupervisor.delegate.onTransition(this, transition);
          onTransition(transition);
          _stateSubject.add(nextState);
        } on Object catch (error) {
    
          _handleError(error);
        }
      },
    );
  }
...
}

同时这里对于Event和State的处理,出现了PublishSubject<Event>和BehaviorSubject<State>,那这里Subject又是什么呢?

这时候RxDart就出场了,原来的Stream流和控制器StreamController的概念,被扩展成了Observable和Subject。

Dart RxDart
Stream Observable
StreamController Subject

这里一共有三种类型的Subject,

1 PublishSubject

PublishSubject仅向监听器发送在订阅之后添加到Stream的事件
在这里插入图片描述

2 BehaviorSubject

BehaviorSubject也是一个广播StreamController,它返回一个Observable而不是一个Stream。
在这里插入图片描述
与PublishSubject的主要区别在于BehaviorSubject还将最后发送的事件发送给刚刚订阅的监听器。

3 ReplaySubject

ReplaySubject也是一个广播StreamController,它返回一个Observable而不是一个Stream。

在这里插入图片描述
默认情况下,ReplaySubject将Stream已经发出的所有事件作为第一个事件发送到任何新的监听器。

参考:
Dart | 什么是Stream
Flutter | 状态管理拓展篇——RxDart(四)
Flutter Bloc构建轻量级MVVM
Stream
StreamController
[译]Flutter响应式编程:Streams和BLoC
Reactive Programming - Streams - BLoC

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/unicorn97/article/details/105658378

智能推荐

USB TypeC接口和USB PD快充协议,有何区别?_pd controler能否区分usb的类型-程序员宅基地

文章浏览阅读1.1w次,点赞9次,收藏31次。很多人一下子搞不清楚,怎么USB即是TypeC又是USB PD呢?首先USB标准有几个版本的,比如USB2.0->USB3.0->USB3.1->USB 4.0,通常提到这个,说的是通信速度变快了。那USB TypeC又是什么呢?它主要是指接口,它出现之前,USB接口是多样的,比如:USB Type B:从USB 2.0开始定义,只有4个针脚,常用于打印机、显示器等的连接。Mini USB:分为A型,B型和AB型,从USB1.1/USB2.0开始就有定义,._pd controler能否区分usb的类型

qq协议 0825 和 0836 udp 登录包解析_抓包qq登录版本-程序员宅基地

文章浏览阅读3.5k次,点赞3次,收藏9次。qq协议 0825 和 0836 udp 登录包解析参考使用工具:概念解释udp报文解析0825 udp 发送包报文原始数据:解析0825 返回包原始数据解析0836 发送包原始数据解析参考0825包参考: https://www.cnblogs.com/mRRRR/p/5288931.html虽然是2016年的, 但是里面的结构大体还是不变参考: https://github.com/fa-ge/PCQQ-ProtocolPCQQ协议的实现, 这里面基本都写清楚了, 但是能不能用我还没试过, 最_抓包qq登录版本

JavaScript中的(内置)方式来检查字符串是否为有效数字_js 判断是的有效数字-程序员宅基地

文章浏览阅读1.2k次。我希望在与旧的VB6 IsNumeric()函数相同的概念空间中有东西吗? _js 判断是的有效数字

PHP5.4NTS MYSQL_windows安装Apache2.4.3(mod_fcgi)+PHP5.4.10+Mysql5.5.29-程序员宅基地

文章浏览阅读129次。最近有朋友问我,根据我的以前的一个教程,用php5apache2_4.dll来运行php的http://www.myxzy.com/post-333.html,但是现在apachelounge官网没有php5apache2_4.dll文件下载了,只有一个mod_fcgid了,php的包里面也没有php5apache2_4.dll这个文件。所以就写了这个教程。这个教程是在windows下用Apach..._windows 安装apahce2.4安装mod_fcgid.so

三分钟教你如何用Github找开源项目--值得一看!_github 网站 下载课题相关的开源代码程序。-程序员宅基地

文章浏览阅读10w+次。Git(读音为/gɪt/)是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。GitHub是一个面向开源及私有软件项目的托管平台,因为只支持git 作为唯一的版本库格式进行托管,故名GitHub。# 按照项目名/仓库名搜索(大小写不敏感)in:name xxx # 按照README搜索(大小写不敏感)in:readme xxx# 按照description..._github 网站 下载课题相关的开源代码程序。

MAC地址表、端口安全、MAC地址偏移-程序员宅基地

文章浏览阅读1.4k次,点赞7次,收藏6次。文章目录MAC地址1.组成2.分类3.常见MAC地址4.MAC地址表MAC地址表分类2.端口安全安全MAC地址分类端口安全保护动作配置端口安全经常使用的场景MAC地址偏移出现场景如何避免MAC地址漂移检测MAC地址MAC(Media Access Control Address)地址:网络中每台设备都有一个唯一的网络标识。1.组成MAC地址为48位(6字节)前24bit是通过向IETF等机构申请用来表示厂商的代码,后24bit是厂商分配给产品的唯一数值。2.分类物理MAC地址:MAC地址的_mac地址偏移

随便推点

第5节LMDeploy 大模型量化部署实践:笔记_llmdeploy-程序员宅基地

文章浏览阅读1.6k次,点赞33次,收藏34次。我们先来介绍一下大模型的特点:首先就是参数量大,对于7B的模型,就需要14G以上的内存,并且由于是采用自回归的方式,所以这就需要去缓存之前的信息,这就会进一步增加消耗。而部署的定义就是将训练好的模型放在特定的环境(cpu,gpu,tpu,npu)接收输入,产生输出。这就要对模型进行优化,如模型压缩和硬化加速。从上面可以得出如何在低存储的设备上部署?如何提高token推理的速度?如何解决动态token的问题?如何提供系统吞吐量?对此现在有很多成熟的技术:低比特量化,模型并行等。_llmdeploy

信号量~~_sem_t-程序员宅基地

文章浏览阅读672次。信号量本质是一个计数器~用来描述临界资源的有效个数~POSIX和systeam V信号量都用于同步工作,达到无冲突的访问共享资源。但是POSIX可以用于线程同步~使用信号量首先就要创建一个sem_t类型的变量#include //头文件sem_t sem1;_sem_t

如何用python爬取e-hentai的图片-程序员宅基地

文章浏览阅读5.3w次,点赞6次,收藏26次。前言本来这是个正经的图片爬取教程,但是考虑到正经的教程一向是不会有人看的,所以干脆满足一下各位老绅士的心理,改成了E站的爬图教学。事先声明,本博客本质上还是个爬虫教学,所以不会提供任何ehentai访问方式和黄涩链接(代码本身也无法自动翻墙访问ehentai)。由于E站访问的问题,爬虫的效率具体也和网络稳定率有关,所以失败并不一定是代码有问题。博主并不常访问E站(不要说了,真的全是因为你们喜欢),所以对相关的页面并不是十分了解,如果有些页面反复使用该爬虫仍无法运行,那就可能是触及到了博主的知识盲区。(_e-hentai

flutter屏幕适配-程序员宅基地

文章浏览阅读240次。现在的手机品牌和型号越来越多,导致我们平时写布局的时候会在个不同的移动设备上显示的效果不同,比如我们的设计稿一个View的大小是300px,如果直接写300px,可能在当前设备显示正常,但到了其他设备可能就会偏小或者偏大,这就需要我们对屏幕进行适配。安卓原生的话有自己的适配规则,可以根据不同的尺寸建立不同的文件夹,系统会根据当前的设备尺寸取对应的大小的布局。而flutter本身并..._flutter_screenutil适配宽度和高度

课程设计--计算机钢琴_计算机钢琴程序编写程序,程序运行时使 pc 机成为一架可弹奏的钢琴,当按下数字-程序员宅基地

文章浏览阅读2.7k次。计算机钢琴_计算机钢琴程序编写程序,程序运行时使 pc 机成为一架可弹奏的钢琴,当按下数字

用python动手学统计学_3-1使用python进行描述统计:单变量_num is deprecated and will be removed in python 3.-程序员宅基地

文章浏览阅读478次。基于python 3.9,使用pycharm community实现,本人水平非常菜,想要开始学习知识。内容基于这本书和一些网上资料如菜鸟网站等进行补充数学公式使用mathtype3-1使用python进行描述统计:单变量3-1-1统计分析与scipyimport scipy as sp3-1-2单变量的操作只有一种类型的数据import scipy as spimport numpy as npfish_data = np.array([2,3,3,4,4,4,4,5,5,6_num is deprecated and will be removed in python 3.14; use ast.constant 怎么

推荐文章

热门文章

相关标签