Flutter Bloc构建轻量级MVVM

转载请注明出处王亟亟的大牛之路

有一段时间没有写东西了,然后之前1年多时间一直在做RN相关的业务和一些新技术的调研,外加“沉迷炒币”写文章也就很少了,然后最近因为一些公司框架的调整所以对部分技术做了一些迁移,然后一个比较重要的思路就是用BLOC处理状态机的业务,将肆意安放的的setState()MVVM起来,然后把写sample的过程分享一下

什么是Bloc ?

网上有很多铺天盖地的解释啊说明,大多数也是抄来抄去的,这里也就不炒冷饭复制粘贴了,想了解理论知识可以自行google

Flutter Bloc构建轻量级MVVM_第1张图片

源码地址

https://github.com/ddwhan0123/MaiMai/tree/blog_bloc
最近的一些整理包括轮子都会在这个仓库里做,然后某篇文章会单独的拉一个分支来锁版本
本文对应的分支是 blog_bloc


实现效果

Flutter Bloc构建轻量级MVVM_第2张图片


目录结构

Flutter Bloc构建轻量级MVVM_第3张图片

设计思维源于官方sample 把xxx_state.dart(数据/状态本身)
xxx_event(事件流/状态变化的诱因)
xxx_bloc.dart(使二者关联且产生逻辑变更的地方)


业务场景

  1. 点位数值整体覆盖 加
  2. 点位数值整体覆盖 减
  3. 点位归零 重置
  4. 点位数值加 部分值添加

依赖:

 flutter_bloc: 2.0.1
 equatable: ^1.0.0

equatable :

能够在Dart比较对象通常涉及必须重写==运算符以及hashCode .

flutter_bloc :

谷歌官方的实现,内部通过 Stream,Sink,BehaviorSubject实现(可自行百度,资料比较全)


sample_bloc.dart 状态的承载类

一个很简单的普通对象 甚至没有继承链

构造函数在初始化状态机时调用(主要为了初始化bloc的state值)

class PointState {
  int x;
  int y;
  int z;

  //构造函数
  PointState(this.x, this.y, this.z);

  //重置操作调用
  factory PointState.reset() {
    return PointState(0, 0, 0);
  }

  //部分值修改时调用
  update(int x, int y, int z) {
    return PointState(x, y, z);
  }
}

counter_event.dart 事件类


import 'package:meta/meta.dart';
import 'package:equatable/equatable.dart';

import 'counter_state.dart';

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


//重置数据 无需逻辑行为或返回参数,纯粹当指令用
class ButtonPressReset extends CounterEvent {
  @override
  List<Object> get props => null;
}

//点位增加 传入某个值,做更新操作
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 }';
  }
}

sample_bloc.dart 核心处理类(也是和页面产生关联的 view module)


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);
    }
  }
}

继承/实现Bloc ,必须实现initialStatemapEventToState方法

Bloc的构造函数里就会调用 initialState方法,这个方法需要由子类实现,它的类型就是Bloc中的State的类型,并且会调用_bindStateSubject()

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

_stateSubject是 BehaviorSubject类型的一个对象,BehaviorSubject是一个广播流,它能记录下最新一次的事件,并在新的收听者收听的时候将记录下的事件作为第一帧发送给收听者。当然这并不是dart基础库实现的,他使用了rxdart。(这一部分的知识可查看传送门)

在每次新的事件流触发的时候都会循环处理前后两次事物的对象(也就是我们的State实例),如果两个对象相等并且订阅已经结束则不处理,反之会对这个对象进行二次包装。

所以我们在mapEventToState方法里需要返回的是经过业务逻辑处理完的nextState
在该方法内我们可以拿到 事件类型也就是 event对象 根据event 对象(可传参,也可不传),以及原状态state
也就是当前订阅序列给到的值 State get state => _stateSubject.value;就可以构建我们的新状态了

例子中 全量替换的几种实现方式就是不传参的。而单点数值修改的方式就是从event中进行了取值做了处理,然而实现业务逻辑的方法还是写在了State里,是因为可以很方便的获取State对象中的本地属性,当然在实际场景里还可以做二次分离。

  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);
        }
      },
    );
  }

业务端调用

首先需要你要让状态机,bloc,state,event互相关联不调用setState()方法,就需要把你的状态树包裹在BlocProviderBlocBuilder中(代码偏多,不直接贴了,只贴重要的部分,和源码有所差异)

BlocProvider在原来 StatefulWidget 的 child 外面再包了一个 InheritedWidget, I在查找符合指定类型的 ancestor 时,就可以调用 InheritedWidget 的实例方法 context.ancestorInheritedElementForWidgetOfExactType(),而这个方法的时间复杂度是 O(1),意味着几乎可以立即查找到满足条件的 ancestor。

BlocBuilder做了很好的响应处理,builder返回了上下文对象和我们所需的状态属性。

 @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: BlocProvider(
        builder: (context) => CounterBloc(),
        child: CounterPage(),
      )
    );
  }

Widget CounterPage(){
	var a=BlocBuilder<CounterBloc, PointState>(
          builder: (context, point) {
            return Column(
              children: <Widget>[
                Text(
                  point.x.toString(),
                  style: TextStyle(fontSize: 24.0),
                ),
                Text(
                  point.y.toString(),
                  style: TextStyle(fontSize: 24.0),
                ),
                Text(
                  point.z.toString(),
                  style: TextStyle(fontSize: 24.0),
                )
              ],
            );
          },
        )
	return a
}

总结:

bloc 处理事件和状态
state 纯粹的状态实体
event 业务场景的事件
三者配合bloc+flutter_bloc优秀的剔除了人工手动维护 setState()所造成的重绘问题

相关资料

https://github.com/felangel/bloc
https://github.com/lizubing1992/flutter_bloc
https://github.com/dragonetail/flutterpoc/tree/counter_refactor

你可能感兴趣的:(Flutter,混合开发)