Flutter状态管理学习手册[三]——Bloc

一、Bloc 介绍

Bloc 的名字比较新颖,这个状态管理框架的目的是将 UI 层和业务逻辑进行分离。Bloc 的复杂度处于 ScopedModel 和 Redux 之间,相较于 ScopedModel,Bloc 拥有分明的架构处于业务逻辑,相较于 Redux,Bloc 着重于业务逻辑的分解,使得整个框架对于开发来讲简单实用。

二、Bloc 的层次结构

Bloc 分为三层:

  • Data Layer(数据层),用于提供数据。
  • Bloc(Business Logic) Layer(业务层),通过继续 Bloc 类实现,用于处理业务逻辑。
  • Presentation Layer(表现层),用于 UI 构建。

Presentation Layer 只与 Bloc Layer 交互,Data Laye 也只与 Bloc Layer 交互。Bloc Layer 作为重要一层,处于表现层和数据层之间,使得 UI 和数据通过 Bloc Layer 进行交互。

由此可见,Bloc 的架构和客户端主流的 MVC 和 MVP 架构比较相似,但也存在 Event 和 State 的概念一同构成响应式框架。

三、Bloc 需要知道的概念

BlocProvider,通常做为 App 的根布局。BlocProvider 可以保存 Bloc,在其它页面通过BlocProvider.of<Bloc>(context)获取 Bloc。

Event,用户操作 UI 后发出的事件,用于通知 Bloc 层事件发生。

State,页面状态,可用于构建 UI。通常是 Bloc 将接收到的 Event 转化为 State。

Bloc 架构的核心是 Bloc 类,Bloc 类是一个抽象类,有一个 mapEventToState(event)方法需要实现。mapEventToState(event)顾名思义,就是将用户点击 View 时发出的 event 转化为构建 UI 所用的 State。另外,在 StatefulWidget 中使用 bloc 的话,在 widget dispose 时,要调用 bloc.dispose()方法进行释放。

四、Bloc 的实践

这里以常见的获取列表选择列表为例子。一个页面用于展示选中项和跳转到列表,一个页面用于显示列表。

  1. 引入 Redux 的第三方库

pubspec.yaml 文件中引入 flutter_bloc 第三方库支持 bloc 功能。

1
2
# 引入 bloc 第三方库
flutter_bloc: ^0.9.0
  1. 使用 Bloc 插件

这一步可有可无,但使用插件会方便开发,不使用的话也没什么问题。

Bloc 官方提供了 VSCode 和 Android studio 的插件,方便生成 Bloc 框架用到的相关类。
下文以 Android studio 的插件为例。

比如 list 页面,该插件会生成相应的类

从生成的五个文件中也可以看到,list_bloc 负责承载业务逻辑,list_page 负责编写 UI 界面,list_eventlist_state 分别是事件和状态,其中 list.dart 文件是用于导出前面四个文件的。

具体使用可见

Android studio 的 Bloc 插件

VSCode 的 Bloc 插件

  1. 使用 BlocProvider 作为根布局

main.dart 中,使用 BlocProvider 作为父布局包裹,用于传递需要的 bloc。Demo 中包含两个页面,一个是展示页面 ShowPage,一个是列表页面 ListPage。

上面讲到,Bloc 的核心功能在于 Bloc 类,对于展示页面 ShowPage,会有一个 ShowBloc 继续自 Bloc 类。由于展示页面 ShowPage 会和列表页面 ListPage 有数据的互动,所以这里将 ShowBloc 保存在 BlocProvider 中进行传递。

1
2
3
4
5
6
7
8
9
10
11
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: _showBloc,
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: ShowPage()));
}
  1. 展示页面 ShowPage

① ShowEvent

列表的 item 点击后,需要发送一个 event 通知其它页面列表被选中,这里定义一个 SelectShowEvent 作为这种 event 通知。

1
2
3
4
5
class SelectShowEvent extends ShowEvent {
String selected;

SelectShowEvent(this.selected);
}

② ShowState

State 用于表示一种界面状态,即一个 State 就对应一个界面。插件在一开始会生成一个默认状态,InitialShowState。我们可以使用 InitialShowState 来代表初始的界面。另外,我们自己定义一种状态,SelectedShowState,代表选中列表后的 State。

1
2
3
4
5
6
7
8
9
10
11
12
@immutable
abstract class ShowState {}

class InitialShowState extends ShowState {}

class SelectedShowState extends ShowState {
String _selectedString = "";

String get selected => _selectedString;

SelectedShowState(this._selectedString);
}

③ ShowBloc

Bloc 的主要职责是接收 Event,然后把 Event 转化为对应的 State。这里的 ShowBloc 继续自 Bloc,需要重写实现抽象方法 mapEventToState(event)。在这个方法中,我们判断传过来的 event 是不是 SelectShowEvent,是则拿到 SelectShowEvent 中的 selected 变量去构建 SelectedShowState。mapEventToState(event)返回的是一个 Stream,我们通过 yield 关键字去返回一个 SelectedShowState。

1
2
3
4
5
6
7
8
9
10
11
12
13
class ShowBloc extends Bloc<ShowEvent, ShowState> {
@override
ShowState get initialState => InitialShowState();

@override
Stream<ShowState> mapEventToState(
ShowEvent event,
) async* {
if (event is SelectShowEvent) {
yield SelectedShowState(event.selected);
}
}
}

④ ShowPage

在 ShowPage 的界面上,我们需要根据 showBloc 中是否有被选中的列表项目去展于页面,所以这里我们先使用使用BlocProvider.of<ShowBloc>(context)去拿到 showBloc,接着再用 BlocBuilder 根据 showBloc 构建界面。使用 BlocBuilder 的好处就是可以让页面自动响应 showBloc 的变化而变化。

1
2
3
4
5
6
7
8
9
10
var showBloc = BlocProvider.of<ShowBloc>(context);
...
BlocBuilder(
bloc: showBloc,
builder: (context, state) {
if (state is SelectedShowState) {
return Text(state.selected);
}
return Text("");
}),
  1. 列表页面 ListPage

① ListEvent

列表页面,我们一开始需要从网络中拉取列表数据,所以定义一个 FetchListEvent 事件在进入页面时通知 ListBloc 去获取列表。

1
2
3
4
5
6
@immutable
abstract class ListEvent extends Equatable {
ListEvent([List props = const []]) : super(props);
}

class FetchListEvent extends ListEvent {}

② ListState

InitialListState 是插件默认生成的初始状态,另外定义一个 FetchListState 代表获取列表完成的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@immutable
abstract class ListState extends Equatable {
ListState([List props = const []]) : super(props);
}

class InitialListState extends ListState {}

class FetchListState extends ListState {

List<String> _list = [];

UnmodifiableListView<String> get list => UnmodifiableListView(_list);

FetchListState(this._list);
}

③ ListBloc

在 ListBloc 中,进行从网络获取列表数据的业务。这里通过一个延时操作摸拟网络请求,最后用 yield 返回列表数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ListBloc extends Bloc<ListEvent, ListState> {
@override
ListState get initialState => InitialListState();

@override
Stream<ListState> mapEventToState(
ListEvent event,
) async* {
if (event is FetchListEvent) {
// 模拟网络请求
await Future.delayed(Duration(milliseconds: 2000));
var list = [
"1. Bloc artitechture",
"2. Bloc artitechture",
"3. Bloc artitechture",
"4. Bloc artitechture",
"5. Bloc artitechture",
"6. Bloc artitechture",
"7. Bloc artitechture",
"8. Bloc artitechture",
"9. Bloc artitechture",
"10. Bloc artitechture"
];

yield FetchListState(list);
}
}
}

④ ListPage

在列表页面初始化时有两个操作,一个是初始化 listBloc,一个是发出列表请求的 Event。

1
2
3
4
5
6
@override
void initState() {
bloc = ListBloc(); // 初始化listBloc
bloc.dispatch(FetchListEvent()); // 发出列表请求事件
super.initState();
}

接下用,便是用 BlocBuilder 去响应状态。当 state 是 InitialListState,说明未获取列表,则显示 loading 界面,当 state 是 FetchListState 时,说明已经成功获取列表,显示列表界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
body: BlocBuilder(
bloc: bloc,
builder: (context, state) {
// 根据状态显示界面
if (state is InitialListState) {
// 显示 loading 界面
return buildLoad();
} else if (state is FetchListState) {
// 显示列表界面
var list = state.list;
return buildList(list);
}
}));

最后,记得对 bloc 进行 dispose()

1
2
3
4
5
@override
void dispose() {
bloc.dispose();
super.dispose();
}

具体代码可以到 github 查看。

总结

在 Bloc 的架构中,将一个页面和一个 Bloc 相结合,由页面产生 Event,Bloc 根据业务需要将 Event 转化为 State,再把 State 交给页面中的 BlocBuilder 构建 UI。Demo 中只是给出了简单的状态管理,实际项目中,比如网络请求,有请求中、请求成功、请求失败的多种状态,可以做适当封装使 Bloc 更加易用。相比于 Redux,Bloc 不需要将所有状态集中管理,这样对于不同模块的页面易于拆分,对于代码量比较大的客户端而言,Bloc 的架构会相对比较友好。

Flutter状态管理学习手册[二]——Redux

上一篇讲到了一个简单的状态管理架构—— ScopedModel , 当然,这种简单的架构会用在商业项目中的概率比较小,本篇则讲述另一个架构: Redux ,一个优雅且实用的状态管理框架。本篇 Demo 地址:https://github.com/windinwork/flutter_redux_app

一、Redux 的准备工作

Redux 的概念源于 React,对于不是从事前端工作或者没有接触过 React 的人要理解 Redux 会比较繁复。对于不了解 Redux 的小伙伴,这里有两篇很不错的文章介绍了 Redux 的概念和相关知识:

Redux 入门教程(一):基本用法

Redux 入门教程(二):中间件与异步操作

二、Redux的概念

学习后 Redux 可以了解到,Redux 主要由涉及下面几种概念:

继续阅读全文 »

Flutter状态管理学习手册[一]——ScopedModel

一、ScopedModel简介

ScopedModel属于入门级别的状态管理框架,它的思想比较简单,参考官方文档便可以很容易理解其中构架。

FlutterLifting state up(状态提升)是十分必要的,状态提升可以理解为把组件之间相互共享的状态提取出来放在一个较高层级中管理的一种思想。ScopedModel提供了对于这种状态管理的便利。

二、ScopedModel中的三个概念

ScopedModel主要有三个重要的概念,也是其中的三个类:ModelScopedModelScopedModelDescendantScopedModel基本上通过这三个类实现其功能。

继续阅读全文 »

Android应用集成Office文件能力完全攻略

不同于iOS,Android的webView不支持打开office和pdf文档,所以当我们遇到在应用内打开office和pdf文档的需求时,往往无法从系统原生功能去支持。这篇文章的写下笔者在Android应用中集成office和pdf文件能力的心得,附上demo地址:https://github.com/windinwork/OfficeApplication

一、确定解决方案

Android应用打开office和pdf文件。常用的有以下四种解决方案:

  1. 在线网页打开文件方案:通过微软或谷歌提供的在线页面打开office和pdf文件
  2. 集成相关文档处理开源库:通过集成开源库类似于AndroidPdfViewer
  3. 通过系统中的第三方应用打开文档
  4. 集成腾讯x5 sdk文件能力

四种方案各有优劣,这里笔者选择了x5 sdk为主要手段,第三方应用辅助的这样一种解决方案

二、集成x5内核

腾讯官方提供的x5内核有两个版本,这里选择具有文件能和的sdk:

继续阅读全文 »

使用Gradle编写蒲公英自动上传安装包和更新说明脚本

Github: https://github.com/windinwork/PgyerGradleApplication

平时测试中发包的时候,笔者在打完包就直接拖到蒲公英上让它上传就完事了。不过前两天的会议上,测试小姐姐提出要在蒲公英上写明这次的测试包修改了什么内容。

笔者一想到上传完包还要一个个打字说明在这个包我修改了什么,立即强烈拒绝!但是测试小姐姐再三要求,碍于这确实是个好提议和会上坐着的老大,只好勉为其难地答应发包时写上改动内容T_T。但是懒惰如笔者,当然不会每次发包都手动打字啦,最好能打完包后自动把包和修改信息上传到蒲公英。

虽然嘴上说着不要不要,但笔者想到写个自动化脚本还是很兴奋的。本来想看看有没有现成的蒲公英自动上传脚本,在网上搜索了一下发现都不是很对胃口,想想还是自己写算了。所以今天花了半天写了这个脚本,在这里也分享一下相关的gradle配置,以供参考。

继续阅读全文 »

美团多渠道打包工具Walle源码解析

笔者现在在负责一个新的Android项目,前期功能不太复杂,安装包的体积小,渠道要求也较少,所以打渠道包使用Android Studio自带的打包方法。原生方法打渠道包大约八分钟左右就搞定了,顺便可以悠闲地享受一下这种打包方式的乐趣。但是,随着重的功能的加入和渠道的增加,原生方法打渠道包就显得有点慢了,所以集成了美团的多渠道打包工具Walle,顺便看了一下里面的实现原理。

一、概述

这一次的原理分析仅仅针对Android Signature V2 Scheme

在上一家公司的时候,笔者所在的Android团队经历了Android Signature V1Android Signature V2的变更,其中因为未及时从V1升级到V2而导致上线受阻,当时也紧急更换了新的多渠道打包工具来解决问题。在我自己使用多渠道打包工具时,不免对V2签名验证的方式有了一丝好奇,想去看看V2签名验证和多渠道打包的实现原理。

继续阅读全文 »

通过字节码看原理,带你去找kotlin中的static方法

kotlin在被钦定为Android的官方开发语言后,越来越多的Android开发者投向kotlin的怀抱。尽管kotlin兼容Java,但在使用上还是有很大不同的,就像static关键字,我们可以用companion object来替代static,当我们用反射去调用时,会发现调用时并不像static那样直接,笔者在日常使用中就遇到这样的问题,想拿反射去调用静态方法时无法调用,所以便通过字节码的实现来一窥究竟,顺便水一篇文章(●>∀<●)。

一、如何查看kotlin字节码

我们通过Tools->Kotlin->Show Kotlin bytecode打开Kotlin字节码界面,查看Kotlin文件的字节码形式。界面如下:

继续阅读全文 »

子弹短信没有附近的人?教你子弹短信开启附近锤友

转载请标明出处https://windinpub.com/2018/08/30/%E5%AD%90%E5%BC%B9%E7%9F%AD%E4%BF%A1%E6%B2%A1%E6%9C%89%E9%99%84%E8%BF%91%E7%9A%84%E4%BA%BA%EF%BC%9F%E6%95%99%E4%BD%A0%E5%AD%90%E5%BC%B9%E7%9F%AD%E4%BF%A1%E5%BC%80%E5%90%AF%E9%99%84%E8%BF%91%E9%94%A4%E5%8F%8B/

子弹短信这款软件现在十分流行,新颖的界面友好的交互吸引了大量的新用户。但是,听说子弹短信还有“发现锤友”这一好玩的功能,但是很多小伙伴们没发现有这个功能,今天小编带你用另类的方法让附近锤友这个功能显示出来。

一、为什么没有发现锤友功能

目前发现锤友的功能只在锤子手机上才会显示,所以如果你是其它安卓手机或者苹果手机用户,那么你的子弹短信将不会显示出发现锤友的选项。没有发现锤友的功能似乎少了些许乐趣,接下来让我们来开户它。

继续阅读全文 »

非侵入式无权限应用内悬浮窗的实现

前言

一般的悬浮窗实现方式,需要申请权限,并还是要对部分机型进行适配才能正常显示。那么这里,我们换一种思路,实现一个不一样的悬浮窗。

一、应用内悬浮窗实现思路

通常的悬浮窗是通过WindowManager直接添加的,在不同的Android系统上需要做不同的适配,在Android6.0以上的机型上,还需要引导用户跳转到设置界面手动开启悬浮窗权限。虽然这样实现悬浮窗有完整的解决方案,但是开启悬浮窗过程对用户并不是很友好。下面,我们换一种思路,去使用一个应用内悬浮窗,避免机型适配和权限申请的坑,让悬浮窗像普通的View一样显示在界面上。

一般悬浮窗的实现方案是向系统window添加typeTYPE_PHONE或者TYPE_TOASTView,从而使悬浮窗可以作为一个独立的View进行展示。Android对这一行为作了限制,那我们可以考虑从比较常规的途径添加View:向每一个展示界面,即Activity,添加一个View作为悬浮窗。这样,我们使用悬浮窗时就可以避免适配和权限问题。那么,怎么样实现这样的悬浮窗更好呢?

继续阅读全文 »

JockeyJS——WebView与JS交互解决方案,开源库使用和解析

前言

在Android上,对于JS交互,往往是通过系统原生提供的@JavascriptInterface这种方式进行交互的,而本人在项目的应该也是使用这种方式。最近听朋友提到一个库——JockeyJS,封装了JS交互逻辑,通过少量的接口让开发者只需要关注Java和JS之间的方法调用。我对它避开@JavascriptInterface的实现比较感兴趣,后来发现JockeyJS有于Java和JS之间的方法调用和回调有着不错的封装,于是便有了分析JockeyJS一文。

一、JockeyJS基本使用

JockeyJS是几年前的库了,虽然是比较久的库,但放到现在仍然可用。

首先,需要在h5页面上引用项目中的jockey.js

接下来在客户端进行配置,JockeyJS主要通过on(String type, JockeyHandler ... handler)send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
两个方法来实现Java与JS之间的交互。

继续阅读全文 »