flutter_bloc使用解析---骚年,你还在手搭bloc吗!
前言
首先,有很多的文章在说flutter bloc模式的应用,但是百分之八九十的文章都是在说,使用StreamController+StreamBuilder搭建bloc,提升性能的会加上InheritedWidget,这些文章看了很多,真正写使用bloc作者开发的flutter_bloc却少之又少。没办法,只能去bloc的github上去找使用方式,最后去bloc官网翻文档。
蛋痛,各位叼毛,就不能好好说说flutter_bloc的使用吗?非要各种抄bloc模式提出作者的那俩篇文章。现在,搞的杂家这个伸手党要自己去翻文档总结(手动滑稽)。
项目效果(建议PC浏览器打开)
下面是Flutter_Bloc历程的一系列链接
-
前面三个,是bloc作者写的bloc模式文档,典型的观察者模式的应用,最原始的就是java中CallBack形式。前俩篇文章就是咱们这些大抄子的主要“参考”的资料来源,这三篇文章在掘金上有翻译版,搜下bloc就能找到。最后一篇文章就是我主要总结归纳的源泉,作者在官网上写了好几个demo:计时器,登录,Todos,天气等等,大家可以自己去看看。
问题
初次使用flutter_bloc框架,可能会有几个疑问
- state里面定义了太多变量,某个事件只需要更新其中一个变量,其它的变量赋相同值麻烦
- 进入某个模块,进行初始化操作:复杂的逻辑运算,网络请求等,入口在哪定义
效果
- 好了,哔哔了一堆,看下咱们要用flutter_bloc实现的效果。
- 直接开Chrome演示,大家在虚拟机上跑也一样。
引用
先说明下,bloc给的api很多,不同的api针对与解决场景不同,我要是把官网那些api全抄过也没啥意义;不,也有可能可以装币,我要是不说明,大家说不定以为是我自己总结的呢!哈哈。
OK,大家要是想知道全场景的使用,可以去官网翻翻文档,我觉得学习一个模式或者框架的时候,最主要的是把主流程跑通,起码可以符合标准的堆页面,这样的话,就可以把这玩意用起来,再遇到想要的什么细节,就可以自己去翻文档,毕竟大体上已经懂了,写过了几个页面,也有些体会,再去翻文档就很快能理解了。
库
flutter_bloc: ^6.0.6 #状态管理框架
equatable: ^1.2.3 #增强组件相等性判断
- 看看flutter_bloc都推到6.0了,别再用StreamController手搭Bloc了!
插件
在Android Studio设置的Plugins里,搜索:Bloc
安装重启下,就OK了
- 右击相应的文件夹,选择“Bloc Class”,我在main文件夹新建的,填入的名字:main,就自动生成下面三个文件;:main_bloc,main_event,main_state;main_view是我自己新建,用来写页面的。
- 是不是觉得,还在手动新建这些bloc文件low爆了;就好像fish_redux,不用插件,让我手动去创建那六个文件,写那些模板代码,真的要原地爆炸。
Bloc范例
初始化代码
来看下这三个生成的bloc文件:main_bloc,main_event,main_state
- main_bloc:这里就是咱们主要写逻辑的页面了
- mapEventToState方法只有一个参数,后面自动带了一个逗号,格式化代码就分三行了,建议删掉逗号,格式化代码。
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainInitial());
@override
Stream<MainState> mapEventToState(
MainEvent event,
) async* {
// TODO: implement mapEventToState
}
}
- main_event:这里是执行的各类事件,有点类似fish_redux的action层
@immutable
abstract class MainEvent {}
- main_state:状态数据放在这里保存,中转
@immutable
abstract class MainState {}
class MainInitial extends MainState {}
实现
- 说明
- 这里对于简单的页面,state的使用抽象状态继承实现的方式,未免有点麻烦,这里我进行一点小改动,state的实现类别有很多,官网写demo也有不用抽象类,直接class,类似实体类的方式开搞的。
- 老夫在代码关键点写上"///"类型注释,大家仔细看看,拷进Android Studio里面,这些地方会变绿!大家好好体会下绿色代码!
- main_bloc
- state变量是框架内部定义的,会默认保存上一次同步的MainSate对象的值
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainState(selectedIndex: 0, isExtended: false));
@override
Stream<MainState> mapEventToState(MainEvent event) async* {
///main_view中添加的事件,会在此处回调,此处处理完数据,将数据yield,BlocBuilder就会刷新组件
if (event is SwitchTabEvent) {
///获取到event事件传递过来的值,咱们拿到这值塞进MainState中
///直接在state上改变内部的值,然后yield,只能触发一次BlocBuilder,它内部会比较上次MainState对象,如果相同,就不build
yield MainState()
..selectedIndex = event.selectedIndex
..isExtended = state.isExtended;
} else if (event is IsExtendEvent) {
yield MainState()
..selectedIndex = state.selectedIndex
..isExtended = !state.isExtended;
}
}
}
- main_event:在这里就能看见,view触发了那些事件了;维护起来也很爽,看看这里,也很快能懂页面在干嘛了
@immutable
abstract class MainEvent extends Equatable{
const MainEvent();
}
///切换NavigationRail的tab
class SwitchTabEvent extends MainEvent{
final int selectedIndex;
const SwitchTabEvent({@required this.selectedIndex});
@override
List<Object> get props => [selectedIndex];
}
///展开NavigationRail,这个逻辑比较简单,就不用传参数了
class IsExtendEvent extends MainEvent{
const IsExtendEvent();
@override
List<Object> get props => [];
}
- main_state:state有很多种写法,在bloc官方文档上,不同项目state的写法也很多
- 这边变量名可以设置为私用,用get和set可选择性的设置读写权限,因为我这边设置的俩个变量全是必用的,读写均要,就设置公有类型,不用下划线“_”去标记私有了。
class MainState{
int selectedIndex;
bool isExtended;
MainState({this.selectedIndex, this.isExtended});
}
- 对于生成的模板代码,我们在这:去掉@immutable注解,去掉abstract;
- 这里说下加上@immutable和abstract的作用,这边是为了标定不同状态,拿很典型的列表数据加载说明,列表加载的时候一般有三种状态
- 获取数据前,列表的布局展示空样式:LoadingBeforeState
- 获取数据失败,显示出加载失败的布局,或提升重新加载的样式提升:LoadingFailureState
- 获取数据成功,显示出列表数据:LoadingSuccessState
- 针对上面三种状态,需要展示不同的布局,这样我们就可以继承抽象的状态类:LoadingState,针对不同状态实现上面三种不同的状态类,不同的状态可以定义不同的参数,然后在view中去判断调用
- 这种实现不同状态,对不同状态进行管理,有点设计模式中-状态模式的味道
- 这里说下加上@immutable和abstract的作用,这边是为了标定不同状态,拿很典型的列表数据加载说明,列表加载的时候一般有三种状态
- 下面代码是对上述描述的一种代码展示,可以瞧瞧;跑demo的时候,这下面的代码就不用抄了,仅做演示
@immutable
abstract class LoadingState extends Equatable {}
class LoadingInitial extends LoadingState {
@override
List<Object> get props => [];
}
class LoadingBeforeSate extends LoadingState{
///实现相应的字段信息
@override
List<Object> get props => [];
}
class LoadingFailureState extends LoadingState{
///实现相应的字段信息
@override
List<Object> get props => [];
}
class LoadingSuccessState extends LoadingState{
///实现相应的字段信息
@override
List<Object> get props => [];
}
///在View中使用,伪代码
BlocBuilder<MainBloc, MainState>(builder: (context, state) {
if(state is LoadingBeforeSate){
return Beforewidget(state.XX,..);
} else if(state is LoadingFailureState){
return FailureWidget(state.XX,state.XX,...);
} else if(state is LoadingSuccessState){
return SuccessWidget(state.XX);
} else {
return ErrorWidget(...);
}
})
- main_view
- 这边就是咱们的界面层了,很简单,将需要刷新的组件,用BlocBuilder包裹起来,使用BlocBuilder:提供的state去赋值就ok了,context去添加执行的事件,context用StatelessWidget中提供的或者BlocBuilder提供的都行
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MainPage(),
);
}
}
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
///创建BlocProvider的,表明该Page,我们是用MainBloc,MainBloc是属于该页面的Bloc了
return BlocProvider(
create: (BuildContext context) => MainBloc(),
child: BodyPage(),
);
}
}
class BodyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Bloc')),
body: totalPage(),
);
}
}
Widget totalPage() {
return Row(
children: [
navigationRailSide(),
Expanded(child: Center(
child: BlocBuilder<MainBloc, MainState>(builder: (context, state) {
///看这看这:刷新组件!
return Text("selectedIndex:" + state.selectedIndex.toString());
}),
))
],
);
}
//增加NavigationRail组件为侧边栏
Widget navigationRailSide() {
//顶部widget
Widget topWidget = Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
image: NetworkImage("https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3383029432,2292503864&fm=26&gp=0.jpg"),
fit: BoxFit.fill),
)),
),
);
//底部widget
Widget bottomWidget = Container(
child: BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
return FloatingActionButton(
onPressed: () {
///添加NavigationRail展开,收缩事件
context.bloc<MainBloc>().add(IsExtendEvent());
},
///看这看这:刷新组件!
child: Icon(state.isExtended ? Icons.send : Icons.navigation),
);
},
),
);
return BlocBuilder<MainBloc, MainState>(builder: (context, state) {
return NavigationRail(
backgroundColor: Colors.white12,
elevation: 3,
///看这看这:刷新组件!
extended: state.isExtended,
labelType: state.isExtended ? NavigationRailLabelType.none : NavigationRailLabelType.selected,
//侧边栏中的item
destinations: [
NavigationRailDestination(
icon: Icon(Icons.add_to_queue),
selectedIcon: Icon(Icons.add_to_photos),
label: Text("测试一")),
NavigationRailDestination(
icon: Icon(Icons.add_circle_outline),
selectedIcon: Icon(Icons.add_circle),
label: Text("测试二")),
NavigationRailDestination(
icon: Icon(Icons.bubble_chart),
selectedIcon: Icon(Icons.broken_image),
label: Text("测试三")),
],
//顶部widget
leading: topWidget,
//底部widget
trailing: bottomWidget,
selectedIndex: state.selectedIndex,
onDestinationSelected: (int index) {
///添加切换tab事件
context.bloc<MainBloc>().add(SwitchTabEvent(selectedIndex: index));
},
);
});
}
Bloc范例优化
反思
从上面的代码来看,实际存在几个隐式问题,这些问题,刚开始使用时候,没异常的感觉,但是使用bloc久了后,感觉肯定越来越强烈
- state问题
- 初始化问题:这边初始化是在bloc里,直接在构造方法里面赋初值的,state中一旦变量多了,还是这么写,会感觉极其难受,不好管理。需要优化
- 可以看见这边我们只改动selectedIndex或者isExtended;另一个变量不需要变动,需要保持上一次的数据,进行了此类:state.selectedIndex或者state.isExtended赋值,一旦变量达到十几个乃至几十个,还是如此写,是让人极其崩溃的。需要优化
- bloc问题
- 如果进行一个页面,需要进行复杂的运算或者请求接口后,才能知晓数据,进行赋值,这里肯定需要一个初始化入口,初始化入口需要怎样去定义呢?
优化实现
这边完整走一下流程,让大家能有个完整的思路
- state:首先来看看我们对state中的优化,这边进行了俩个很重要优化,增加俩个方法:init()和clone()
- init():这里初始化统一用init()方法去管理
- clone():这边克隆方法,是非常重要的,一旦变量达到俩位数以上,就能深刻体会该方法是多么的重要
class MainState {
int selectedIndex;
bool isExtended;
///初始化方法,基础变量也需要赋初值,不然会报空异常
MainState init() {
return MainState()
..selectedIndex = 0
..isExtended = false;
}
///clone方法,此方法实现参考fish_redux的clone方法
///也是对官方Flutter Login Tutorial这个demo中copyWith方法的一个优化
///Flutter Login Tutorial(https://bloclibrary.dev/#/flutterlogintutorial)
MainState clone() {
return MainState()
..selectedIndex = selectedIndex
..isExtended = isExtended;
}
}
- event
- 这边定义一个MainInit()初始化方法,同时去掉Equatable继承,在我目前的使用中,感觉它用处不大。。。
@immutable
abstract class MainEvent {}
///初始化事件,这边目前不需要传什么值
class MainInitEvent extends MainEvent {}
///切换NavigationRail的tab
class SwitchTabEvent extends MainEvent {
final int selectedIndex;
SwitchTabEvent({@required this.selectedIndex});
}
///展开NavigationRail,这个逻辑比较简单,就不用传参数了
class IsExtendEvent extends MainEvent {}
- bloc
- 这增加了初始化方法,请注意,如果需要进行异步请求,同时需要将相关逻辑提炼一个方法,咱们在这里配套Future和await就能解决在异步场景下同步数据问题
- 这里使用了克隆方法,可以发现,我们只要关注自己需要改变的变量就行了,其它的变量都在内部赋值好了,我们不需要去关注;这就大大的便捷了页面中有很多变量,只需要变动一俩个变量的场景
- 注意:如果变量的数据未改变,界面相关的widget是不会重绘的;只会重绘变量被改变的widget
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainState().init());
@override
Stream<MainState> mapEventToState(MainEvent event) async* {
///main_view中添加的事件,会在此处回调,此处处理完数据,将数据yield,BlocBuilder就会刷新组件
if (event is MainInitEvent) {
yield await init();
} else if (event is SwitchTabEvent) {
///获取到event事件传递过来的值,咱们拿到这值塞进MainState中
///直接在state上改变内部的值,然后yield,只能触发一次BlocBuilder,它内部会比较上次MainState对象,如果相同,就不build
yield switchTap(event);
} else if (event is IsExtendEvent) {
yield isExtend();
}
}
///初始化操作,在网络请求的情况下,需要使用如此方法同步数据
Future<MainState> init() async {
return state.clone();
}
///切换tab
MainState switchTap(SwitchTabEvent event) {
return state.clone()..selectedIndex = event.selectedIndex;
}
///是否展开
MainState isExtend() {
return state.clone()..isExtended = !state.isExtended;
}
}
- view
- view层代码太多,这边只增加了个初始化事件,就不重新把全部代码贴出来了,初始化操作直接在创建的时候,在XxxBloc上使用add()方法就行了,就能起到进入页面,初始化一次的效果;add()方法也是Bloc类中提供的,遍历事件的时候,就特地检查了add()这个方法是否添加了事件;说明,这是框架特地提供了一个初始化的方法
- 这个初始化方式是在官方示例找到的
- 项目名:Flutter Infinite List Tutorial
- 项目地址:flutter-infinite-list-tutorial
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
///在MainBloc上使用add方法,添加初始化事件
create: (BuildContext context) => MainBloc()..add(MainInitEvent()),
child: BodyPage(),
);
}
}
///下方其余代码省略...........
搞定
- OK,经过这样的优化,解决了几个痛点。实际在view中反复是要用BlocBuilder去更新view,写起来有点麻烦,这里我们可以写一个,将其中state和context变量,往提出来的Widget方法传值,也是蛮不错的
- 大家保持观察者模式的思想就行了;观察者(回调刷新控件)和被观察者(产生相应事件,添加事件,去通知观察者),bloc层是处于观察者和被观察者中间的一层,我们可以在bloc里面搞业务,搞逻辑,搞网络请求,不能搞基;拿到Event事件传递过来的数据,把处理好的、符合要求的数据返回给view层的观察者就行了。
- 使用框架,不拘泥框架,在观察者模式的思想上,灵活的去使用flutter_bloc提供Api,这样可以大大的缩短我们的开发时间!
Cubit范例
- Cubit是Bloc模式的一种简化版,去掉了event这一层,对于简单的页面,用Cubit来实现,开发体验是大大的好啊,下面介绍下该种模式的写法
创建
- 首先创建Cubit一组文件,选择“Cubit Class”,点击,新建名称填写:Counter
新建好后,他会生成俩个文件:counter_cubit,counter_state,来看下生成的代码
原始生成代码
- counter_cubit
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterInitial());
}
- counter_state
@immutable
abstract class CounterState {}
class CounterInitial extends CounterState {}
按照生成的这种state方式去写,比较麻烦,这边调整下
调整后代码
- counter_cubit
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState().init());
}
- counter_state
class CounterState {
///初始化方法
CounterState init() {
return CounterState();
}
///克隆方法,针对于刷新界面数据
CounterState clone() {
return CounterState();
}
}
OK,这样调整了下,下面写起来就会舒服很多,也会很省事
实现计时器
- 来实现下一个灰常简单的计数器
效果
- 来看下实现效果吧,这边不上图了,大家点击下面的链接,可以直接体验Cubit模式写的计时器
- 实现效果:点我体验实际效果
实现
实现很简单,三个文件就搞定,看下流程:state -> cubit -> view
- state:这个很简单,加个计时变量
class CounterState {
int count;
CounterState init() {
return CounterState()..count = 0;
}
CounterState clone() {
return CounterState()..count = count;
}
}
- cubit
- 这边加了个自增方法:increase()
- event层实际是所有行为的一种整合,方便对逻辑过于复杂的页面,所有行为的一种维护;但是过于简单的页面,就那么几个事件,还单独维护,就没什么必要了
- 在cubit层写的公共方法,在view里面能直接调用,更新数据使用:emit()
- cubit层应该可以算是:bloc层和event层一种结合后的简写
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState().init());
///自增
void increase() => emit(state.clone()..count = ++state.count);
}
- view
- view层的代码就非常简单了,点击方法里面调用cubit层的自增方法就ok了
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (BuildContext context) => CounterCubit(),
child: BlocBuilder<CounterCubit, CounterState>(builder: _counter),
);
}
Widget _counter(BuildContext context, CounterState state) {
return Scaffold(
appBar: AppBar(title: const Text('Cubit范例')),
body: Center(
child: Text('点击了 ${state.count} 次', style: TextStyle(fontSize: 30.0)),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.bloc<CounterCubit>().increase(),
child: const Icon(Icons.add),
),
);
}
}
总结
在Bloc模式里面,如果页面不是过于复杂,使用Cubit去写,基本完全够用了;但是如果业务过于复杂,还是需要用Bloc去写,需要将所有的事件行为管理起来,便于后期维护
OK,Bloc的简化模块,Cubit模式就这样讲完了,对于自己业务写的小项目,我就经常用这个Cubit去写
最后
- Bloc还有很多Api针对不同的场景非常的实用,例如:MultiBlocProvider,BlocListener,MultiBlocListener,BlocConsumer等等,这里面有些Api和Provider的Api是非常相似的,例如MultiXxxxx,这都是为了减少嵌套,提供多个全局Bloc而提供,大家可以去瞧瞧看,用法也都非常的相似
- Cubit范例代码地址
- Bloc范例代码地址
- flutter_bloc相关Api白嫖地址
- flutter_bloc
flutter_bloc使用解析---骚年,你还在手搭bloc吗!的更多相关文章
- JSON解析的成长史——原来还可以这么简单
本文系统介绍,JSON解析的成长史,未经允许,禁止转载. JSON是一种轻量级的数据格式,一般用于数据交互 Android交互数据主要有两种方式:Json和Xml,Xml格式的数据量要比Json格式略 ...
- git log命令全解析,打log还能这么随心所欲!
git log命令非常强大而好用,在复杂系统的版本管理中扮演着重要的角色,但默认的git log命令显示出的东西实在太丑,不好好打扮一下根本没法见人,打扮好了用alias命令拍个照片,就正式出道了! ...
- spring-data-redis的事务操作深度解析--原来客户端库还可以攒够了事务命令再发?
一.官方文档 简单介绍下redis的几个事务命令: redis事务四大指令: MULTI.EXEC.DISCARD.WATCH. 这四个指令构成了redis事务处理的基础. 1.MULTI用来组装一个 ...
- Kubernetes实战指南(三十三):都0202了,你还在手写k8s的yaml文件?
目录 1. k8s的yaml文件到底有多复杂 2. 基于图形化的方式自动生成yaml 2.1 k8s图形化管理工具Ratel安装 2.2 使用Ratel创建生成yaml文件 2.2.1 基本配置 2. ...
- 你还在手撕微服务?快试试 go-zero 的微服务自动生成
0. 为什么说做好微服务很难? 要想做好微服务,我们需要理解和掌握的知识点非常多,从几个维度上来说: 基本功能层面 并发控制&限流,避免服务被突发流量击垮 服务注册与服务发现,确保能够动态侦测 ...
- Flutter GetX使用---简洁的魅力!
前言 使用Bloc的时候,有一个让我至今为止十分在意的问题,无法真正的跨页面交互!在反复的查阅官方文档后,使用一个全局Bloc的方式,实现了"伪"跨页面交互,详细可查看:flutt ...
- 修改Flume-NG的hdfs sink解析时间戳源码大幅提高写入性能
Flume-NG中的hdfs sink的路径名(对应参数"hdfs.path",不允许为空)以及文件前缀(对应参数"hdfs.filePrefix")支持正则解 ...
- AFN解析器里的坑
AFN框架是用来用来发送网络请求的,它的好处是可以自动给你解析JSON数据,还可以发送带参数的请求AFN框架还可以监测当前的网络状态,还支持HTTPS请求,分别对用的类为AFNetworkReacha ...
- 通读AFN①--从创建manager到数据解析完毕
流程梳理 今天开始会写几篇关于AFN源码解读的一些Blog,首先要梳理一下AFN的整体结构(主要是讨论2.x版本的Session访问模块): 我们先看看我们最常用的一段代码: AFHTTPSessio ...
随机推荐
- Badboy脚本录制工具
Badboy 目录 Badboy 1.Badboy安装 2.脚本的录制 1.Badboy安装 下载地址: http://www.badboy.com.au/download/index 安装其实傻瓜式 ...
- css3 压缩及验证工具
1.css w3c统一验证工具 网址:http://www.csstats.com/ 如果你想要更全面的,这个神奇,你值得拥有: w3c统一验证工具:http://validator.w3.org/u ...
- 云计算openstack核心组件——glance— 镜像服务(6)
一.glance介绍: Glance是Openstack项目中负责镜像管理的模块,其功能包括虚拟机镜像的查找.注册和检索等. Glance提供Restful API可以查询虚 ...
- Linux里隐藏的计算器,你知道它的奥秘吗?
Linux里隐藏的计算器,你知道它的奥秘吗? 大家都知道,windows下有个计算器工具,我们在工作生活中经常使用到它.但是,你可知Linux下也同样有个计算器吗? 当然,良许说的是命令行下的计算器工 ...
- redis连接池参数动态化
有的时候要从后端获取数据,真实的key可能在参数之上做一些修改,查了下set-misc模块,set_unescape_uri命令支持变量替换 location ~ /get_redis$ { ...
- Java 基础知识面试题(2020 最新版)
Java面试总结汇总,整理了包括Java基础知识,集合容器,并发编程,JVM,常用开源框架Spring,MyBatis,数据库,中间件等,包含了作为一个Java工程师在面试中需要用到或者可能用到的绝大 ...
- Flutter 使用image_gallery_saver保存图片
Flutter 使用image_gallery_saver保存图片 其实我们开发项目app的时候, 你会发现有很多问题, 比如保存图片功能时 ,不仅导入包依赖包: image_gallery_sav ...
- 预科班D11
2020.09.21星期一 预科班D11 学习内容: 一.基本数据类型及内置方法 1.整形int及浮点型float + - * / // ** % > < >= <= 2.字符 ...
- ERP与EHR系统的恩怨纠葛--开源软件诞生13
ERP中需要EHR的存在吗--第13篇 用日志记录"开源软件"的诞生 [点亮星标]----祈盼着一个鼓励 博主开源地址: 码云:https://gitee.com/redragon ...
- 使用监听器来启动spring -------使用监听器初始化上下文参数
问题: 数据初始化监听器要注入spring容器的对象,必须先启动spring容器才能使用监听器初始化数据. 解决: 使用监听器来启动spring框架 问题:spring框架启动需要哪些参数? 1.需要 ...