0 purchases
decorated flutter
基于BLoC的通用框架 #
BLoC的各种元素 #
什么是IO? #
IO即Input/Output,Input/Output是相对于BLoC来说的,Input即向BLoC输入数据,Output即从BLoC输出数据。
一个BLoC中所有的属性都是IO,负责接收来自widget层的数据(供后续操作读取数据)或者向widget层输出数据。widget层只对数据做读取操作,即widget对数据是READONLY的。
当widget需要对数据发起修改操作时,需要通过BLoCProvider.of(BuildContext)/context.of<SomeBLoC>()来获取当前widget所依附的BLoC实例,然后调用BLoC的对应方法来操作数据。
BaseIO #
BaseIO是所有IO类型的祖宗类,存放了所有IO类型需要的公共属性和方法。
构造器概览
BaseIO({
/// 初始值, 传递给内部的[_subject]
T seedValue,
/// IO代表的语义
String semantics,
/// 是否同步发射数据, 传递给内部的[_subject]
bool sync = true,
/// 是否使用BehaviorSubject, 如果使用, 那么IO内部的[_subject]会保存最近一次的值
bool isBehavior = true,
}) : _semantics = semantics,
_seedValue = seedValue,
latest = seedValue,
_subject = isBehavior
? seedValue != null
? BehaviorSubject<T>.seeded(seedValue, sync: sync)
: BehaviorSubject<T>(sync: sync)
: PublishSubject<T>(sync: sync) {
_subject.listen((data) {
latest = data;
L.d('当前${semantics ??= data.runtimeType.toString()} latest: $latest'
'\n+++++++++++++++++++++++++++END+++++++++++++++++++++++++++++');
});
}
copied to clipboard
BehaviorSubject是rxdart的一个类,作为一个中转站,当有数据add到BehaviorSubject时,会转发给BehaviorSubject的订阅者。作用有些类似iOS里的NotificationCenter,Android中同RxJava中的BehaviorSubject。
Input #
Input表示只从widget接收数据,不对widget输出数据的业务单元。常见的使用场景为接收来自TextFormField的值。
Output #
Output表示只对widget输出数据,不从widget接收数据的业务单元。常见的使用场景为页面初始数据的加载,比如进入一个列表页,BLoC中的一个Output负责加载列表数据,widget层中的一个ListView绑定这个Output,进入页面前调用Output进行数据的加载,加载完成后刷新ListView。
整个过程Output不会从widget接收数据,只会对widget输出数据。
IO #
语义上,IO是既可以输入数据又可以输出数据的业务单元;实现上,IO即class IO<T> extends BaseIO<T> with InputMixin, OutputMixin<T, dynamic>。
常见的使用场景为勾选框,由于Flutter的哲学是widget本身不保存状态,所以像CheckBox这种widget的勾选状态是需要开发者自行维护的,CheckBox本身只负责通知开发者状态即将变化,当CheckBox的onChanged回调出发后,并不会真正的看到CheckBox被勾选,需要开发者自行更新CheckBox的checked的值之后才能看到被勾选。所以在这种场景下,数据流动就变成了CheckBox(onChanged)->BLoC(更新状态值)->CheckBox(监听新的状态值)。
衍生IO #
ListIO
ListIO是只接收列表类型的IO,在普通IO的基础上增加了如下方法:
/// 按条件过滤, 并发射过滤后的数据
List<T> filterItem(bool test(T element));
/// 追加, 并发射
T append(T element, {bool fromHead = false});
/// 追加一个list, 并发射
List<T> appendAll(List<T> elements, {bool fromHead = false});
/// 对list的item做变换之后重新组成list
Stream<List<S>> flatMap<S>(S mapper(T value));
/// 替换指定index的元素, 并发射
T replace(int index, T element);
/// 替换最后一个的元素, 并发射
T replaceLast(T element);
/// 替换第一个的元素, 并发射
T replaceFirst(T element);
/// 删除最后一个的元素, 并发射
T removeLast();
/// 删除一个的元素, 并发射
T remove(T element);
/// 删除第一个的元素, 并发射
T removeFirst();
/// 删除指定索引的元素, 并发射
T removeAt(int index);
copied to clipboard
BoolIO
BoolIO是只接收布尔值的IO,增加了如下方法:
/// 翻转状态值
bool toggle();
copied to clipboard
PageIO
PageIO在ListIO基础之上进一步封装了分页操作,在PageIO会自动维护当前页数,开发者只需要调用nextPage方法即可请求下一页数据,如果如要刷新数据,不能再调用IO提供的update方法,而是需要使用PageIO中提供的refresh方法。
编码规范 #
尽量减少嵌套;
尽量让一个Widget长什么样只取决于构造器参数,减少来历不明的变化引起的Widget变化,例如:
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: /* 某处获取来的stream */
builder: (context, snapshot) {
return AWidget(snapshot.data);
}
);
}
}
class AWidget extends StatelessWidget {
AWidget(AVM vm, {Key key}): super(key: key);
@override
Widget build(BuildContext context) {
return /* 根据vm参数构造出widget */
}
}
copied to clipboard
优于
class SomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AWidget();
}
}
class AWidget extends StatelessWidget {
AWidget({Key key}): super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: /* 某处获取来的stream */
builder: (context, snapshot) {
return /* 根据snapshot构造widget */;
}
);
}
}
copied to clipboard
第二种写法虽然看着代码更少,但是跟某个stream直接绑定,导致难以复用,而且AWidget长什么样不受AWidget类内部逻辑控制(被某处获取来的stream控制),非常不直观。除非这是一个全局使用的widget,绑定的stream也是全局的事件,那么可以这样使用,否则必须要从构造器中传入参数。
优先使用StatelessWidget+BLoC模式实现功能,只有功能比较简单时,可以使用StatefulWidget代替实现;
StatelessWidget类里应该只有
build方法;
override方法;
事件处理(私有)方法;
StatefulWidget不做限制;
名称 #
在遵循规范的前提下,不要怕名字太长或者太啰嗦,清晰精确的名称好于莫名其妙的省略;
除非是广泛通用的缩写(例如http等),否则不允许使用任何形式的缩写,无意义的缩写会让看代码的人困惑;
文件名 #
文件名使用下划线风格;
页面类以.screen.dart结尾;
普通widget类以.widget.dart结尾;
dialog类以.dialog.dart结尾;
bloc类以.bloc.dart结尾;
类扩展(extension)以.x.dart结尾;
mixin以.mixin.dart结尾;
其他文件目前直接以.dart结尾;
类名 #
页面类以Screen结尾;
普通widget类根据业务属性命名即可;
方法名 #
方法名使用驼峰命名法;
BLoC中的action以perform开头,比如登录动作方法命名为performLogin;
变量名 #
变量名使用驼峰命名法;
Widget结构 #
widget树中无状态变化的部分,要抽离出单独的widget类,把以构造器参数形式传入需要的数据,并修饰构造器为const;
BLoC结构 #
为了区分action和IO,IO的成员统一放在同文件内名为_ComponentMixin的mixin中,action方法放在原BLoC中,一个典型的例子:
class LoginBLoC extends LocalBLoC with _ComponentMixin {
LoginBLoC() : super('登录 BLoC');
Future<bool> performLogin() async {
// 获取最新的account和password并执行登录动作
}
}
mixin _ComponentMixin on LocalBLoC {
@override
List<BaseIO> get disposeBag => [
account,
password,
];
final account = Input<String>(semantics: '账户');
final password = Input<String>(semantics: '密码');
}
copied to clipboard
文件夹结构 #
ui相关文件组织 #
ui文件夹下分为screen文件夹和widget文件夹,screen文件夹存放所有的单页面,widget存放全局共用的控件;
screen下的文件分为主screen文件和其组成部分,当组成部分比较复杂时,可以再单独为其新建文件夹;
在一个文件夹下,只允许存在两层关系的ui,如果超过两层的关系,则为较复杂的那部分新建文件夹;
一些约定的写法 #
Dialog的使用 #
Dialog本身不处理任何业务逻辑,它只负责采集数据,采集完成之后通过Navigator.pop(data)回传给宿主widget,并在宿主widget进行业务操作;
widget #
优先使用StatelessWidget,只有当StatelessWidget实在不方便的时候再启用StatefulWidget,比如说要mixin一些辅助;
优先把IO放在局部BLoC,只有当局部BLoC无法满足需求时,再放到全局BLoC;
框架模式 #
项目的框架模式为BLoC(Business Logic Component),是一种基于流(Stream)的模式。
一"块"UI会搭配一个专属的BLoC,BLoC中存放着这一块UI所有的状态,以及所有操作数据的接口。
全局结构图 #
矩形为widget,圆角矩形普通类
单页面结构图 #
矩形为widget,圆角矩形普通类
网络请求时序图 #
以登录为例:
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.