Flutter体验 Day 26-bloc

bloc

有经验的前端工程师或多或少应该都有听过 MVCMVPMVVM 架构的开发方式,这些开发方式可以让我们达到观注点分离(Separation of concerns,SoC)的设计原则让开发团队可以遵询同一种模式进行开发工作。

今天我们就来看看目前在 Flutter 设计上经常听到 Bloc 是什麽吧。

Bloc Design Pattern

Bloc (Business Logic Component) 的设计理念会希望透过该设计原则将 View 的代码与业务逻辑拆开, 易於程序码的维护与开发、测试。

a predictable state management library for Dart.

Simple & Lightweight
Highly Testable
For Dart, Flutter, and AngularDart

架构

从官网的架构图上来看,从角色可以区分为三种类别

bloc_arch

该图片引用来自官网架构文件说明

Data Layer

负责处理资料来源的管理,通常会从 DB 或是 API 取得资料。

Business Logic Layer

接收 UI 传递过来的事件(events)触发业务逻辑的处理,可能会需要从 Data 取得相关资料,视逻辑有机会触发状态(states)的转换。

Presentation Layer

负责处理画面的呈现,画面照业务逻辑的 states 而有不同状态的显示方式。

在开发前需要定义应用上可能的状态以及会需要处理的事件为何!!!

以开发聊天室作为范例

先前的聊天室范例我们是使用 StatefulWidget 搭配 ViewModel 的写法,接下来我们试着用 bloc 改写看看。

安装 bloc 设定

与 bloc 相关的套件如下

dependencies:
  bloc: ^7.2.0
  flutter_bloc: ^7.3.0
  equatable: ^2.0.3

先列举聊天室功能的业务逻辑

  1. 在进入聊天室画面时,需要与聊天室服务器建立连线
  2. 连线完成後待接收WebSocket推送的讯息
  3. 聊天室需显示连线的状态
  4. 断线时可以重新连线
  5. 可以发送聊天讯息
  6. 保存接收过的讯息资料

bloc pattern 任务拆分

  • UI:输入文字栏位、聊天讯息列表
  • bloc:定义聊天室业务逻辑
  • 资料源:WebSocket相关
  • events:连线建立、连线中断、接到讯息、DB初始化
  • status:记录聊天室状态相关资料

chat_bloc.dart

负责处理聊天室WebSocket建立工作,新增一个类别继承Bloc并定义对应的EventState

class ChatBloc extends Bloc<ChatEvent, ChatState> {
  final Connection _connection;
  ChatBloc(this._connection) : super(const ChatState()) {}
}

Connection 是先前范例中我们包装用来建立 WebSocket 的类别,在 bloc 初始化时从外部注入。

chat_state.dart

ChatState类别中我们定义两个属性

  1. status - 记录目前连线状态
  2. data - 记录聊天室讯息记录
enum SocketStatus { initial, open, closed }

class ChatState extends Equatable {
  final SocketStatus status;
  final List<Message> data;

  const ChatState({
    this.status = SocketStatus.initial,
    this.data = const <Message>[],
  });

  ChatState copyWith({
    SocketStatus? status,
    List<Message>? data,
  }) {
    return ChatState(
      status: status ?? this.status,
      data: data ?? this.data,
    );
  }

  @override
  String toString() {
    return '''ChatState { status: $status, data_length: ${data.length} }''';
  }

  @override
  List<Object> get props => [data, status];
}

chat_event.dart

根据需求整理出聊天室会需要处理的事件内容

  • ChatDBInit - 从 DB 初始化的聊天讯息资料
  • ChatReceiveMessage - 接收到聊天室讯息
  • ChatSocketStatusChange - WebSocket连线状态改变
abstract class ChatEvent extends Equatable {
  const ChatEvent();

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

class ChatReceiveMessage extends ChatEvent {
  final Message msg;
  const ChatReceiveMessage(this.msg);
}

class ChatSocketStatusChange extends ChatEvent {
  final bool status;
  const ChatSocketStatusChange(this.status);
}

class ChatDBInit extends ChatEvent {}

完成业务逻辑的基本定义後,接下来试着跟画面结合在一下

聊天室页面

BlocProvider

使用 flutter_bloc 提供的 BlocProvider 提供聊天室 Bloc 的实例

class ChatPage extends StatelessWidget {
  ChatPage({Key? key}) : super(key: key);

  final Uri uri = Uri.parse('ws://test.dev.rde:8000/?token=sm2');

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ChatBloc(Connection(uri: uri)),
      child: const ChatView(),
    );
  }
}

事件与状态

ChatBloc 建构式需定义事件设定,这边我们可以透过 blocadd 方法触发事件 ChatDBInit

早期 bloc 的写法是在这边定义mapEventToState,不过语法比较难理解,後续已建议改成下列的写法。bloc_issues

我们在接收到 ChatDBInit 事件後透过 on 绑定 _initDB 处理,我们先从 db 取回讯息资料,并透过 ChatState copyWith 产生一个新的 state,并以 blocemit 触发状态异动。

接着在绑定Connection相关的事件:ChatSocketStatusChangeChatReceiveMessage

  ChatBloc(this._connection) : super(const ChatState()) {
    on<ChatReceiveMessage>(_onMessage);
    on<ChatSocketStatusChange>(_onStateChange);
    on<ChatDBInit>(_initDB);

    add(ChatDBInit());
  }

  void _initDB(event, emit) async {
    emit(state.copyWith(data: await db.query()));

    _connectionSubscription = _connection.connected.listen((bool status) {
      add(ChatSocketStatusChange(status));
    });
    _connectionSubscription = _connection.stream.listen((data) {
      if (data["eventName"] == "chat:msg") {
        add(ChatReceiveMessage(Message.fromJson(data)));
      }
    });
  }

简单来说:
bloc 其实是有限状态机的一种设计方式,根据业务逻辑的需要归纳出states,透过events触发业务逻辑的处理,引发 state 的转换。

BlocBuilder

View 的处理上,可使用 flutter_bloc 提供的 BlocBuilder 监控状态的变换而重新渲染画面,并使用 state 里与画面有关的资料。

例如:我们在 ChatState 中定义 status 属性处理 WebSocket 的连线状态

 Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: BlocBuilder<ChatBloc, ChatState>(
        builder: (context, state) {
          final btnTitle = state.status == SocketStatus.open ? "已连线" : "请重连";
          var controller = context.read<ChatBloc>().controller;

          return Column(
            children: [
              SizedBox(
                width: double.infinity,
                child: TextButton(
                  child: Text(btnTitle),
                  onPressed: () {
                    print(state.status);
                    if (state.status == SocketStatus.closed) {
                      context.read<ChatBloc>().reconnect();
                    }
                  },
                ),
              ),

使用bloc改写後程序码语意更易懂,也不用一直呼叫 setState,完整程序码在这

chat_bloc

其他

我自己在研究bloc时初期遇到的状况是不太晓得要怎麽将业务逻辑定义清楚以及statesevents 的内容要怎麽写。後来查看官网上的一些范例後才慢慢掌握。

心得如下:

  1. 业务逻辑的单位大小由你自己决定:在聊天室的范例中,初期我一直在纠结连线状态与聊天室讯息记录是要放在一起还是拆分成两个bloc,其实没有对与错,就看自己怎麽写符合当下状况,有需要在拆分也行。

  2. bloccubit 两种写法哪一种适合我:如果你只要处理状态资料的转换而不用事件的状态那简单应用 cubit 就好。例如:其实我可以用 cubit 处理接收到聊天室的讯息事件即可。

  3. states 的写法:定义状态抽象类别然後在依需求实作不同状态的子类别还是 使用单一状态类别透过 copyWith 产生新的状态类别实例。

  4. 我要使用什麽方式触发states的转换:在聊天室的范例我从 WebSocket 收到讯息时,我可以发出"接到讯息事件"对应後续的业务逻辑,也可以直接发出新的状态,那我到底需不需要额外使用事件

    follow 原则: 事件 > 业务逻辑 > 状态

小结

使用bloc将观察点分离,在组件中我们只要处理与 UI 有关的逻辑,将复杂多变的业务逻辑放到 bloc,在开发与维护或是测试工作上都是很不错的,建议花点时间研究。


<<:  诶那个...跳坑吗?

>>:  [Day20]程序菜鸟自学C++资料结构演算法 – 杂凑法(Hash)

汇入大量资料到 docker 上的资料库:mysql-client

前言 想透过 phpMyAdmin 把正式机资料拉下来,汇入本机 docker 上的资料库做开发;但...

html字体的变化

让网页出现文字後想要改变文字的大小、颜色、字型...这时候就需要用到css了,你可以再新建一个档案并...

[Day 9] 资料产品第五层 - 自动决策与 AI

资料的最终目的就是替代人力。 (https://qz.com/217199/softbanks-hu...

Day-06 如何不分大小写/自动引入

本期想介绍Android Studio上两个实用技巧,使程序撰写时能够事半功倍完成。 1. 代码提示...

[神经机器翻译理论与实作] Google Translate的神奇武器- Seq2Seq (II)

前言 我们紧接着切入 RNN 为架构的编码器-解码器。 在seq2seq之前 RNN Encoder...