Flutter Bloc構建輕量級MVVM

轉載請註明出處王亟亟的大牛之路

有一段時間沒有寫東西了,然後之前1年多時間一直在做RN相關的業務和一些新技術的調研,外加“沉迷炒幣”寫文章也就很少了,然後最近因爲一些公司框架的調整所以對部分技術做了一些遷移,然後一個比較重要的思路就是用BLOC處理狀態機的業務,將肆意安放的的setState()MVVM起來,然後把寫sample的過程分享一下

什麼是Bloc ?

網上有很多鋪天蓋地的解釋啊說明,大多數也是抄來抄去的,這裏也就不炒冷飯複製粘貼了,想了解理論知識可以自行google

在這裏插入圖片描述

源碼地址

https://github.com/ddwhan0123/MaiMai/tree/blog_bloc
最近的一些整理包括輪子都會在這個倉庫裏做,然後某篇文章會單獨的拉一個分支來鎖版本
本文對應的分支是 blog_bloc


實現效果

在這裏插入圖片描述


目錄結構

在這裏插入圖片描述

設計思維源於官方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<Event,State> ,必須實現initialStatemapEventToState方法

Bloc的構造函數裏就會調用 initialState方法,這個方法需要由子類實現,它的類型就是Bloc<Event, State>中的State的類型,並且會調用_bindStateSubject()

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

_stateSubject是 BehaviorSubject<State>類型的一個對象,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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章