在正式介绍 BLoC之前, 为什么我们需要状态管理。如果你已经对此十分清楚,那么建议直接跳过这一节。
如果我们的应用足够简单,Flutter 作为一个声明式框架,你或许只需要将 数据 映射成 视图 就可以了。你可能并不需要状态管理,就像下面这样。


但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样。

我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
Flutter 实际上在一开始就为我们提供了一种状态管理方式,那就是 StatefulWidget。但是我们很快发现,它正是造成上述原因的罪魁祸首。
State 属于某一个特定的 Widget,在多个 Widget 之间进行交流的时候,虽然你可以使用 callback 解决,但是当嵌套足够深的话,我们增加非常多可怕的垃圾代码。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。

BLoC 是什么

旨在使用Widget更加加单,更加快捷,方便不同开发者都能使用,可以记录组件的各种状态,方便测试,让许多开发者遵循相同的模式和规则在一个代码库中无缝工作。

如何使用

简单例子

老规矩,我们写一个增加和减小的数字的例子,首先定义一个存储数据的Model,我们继承Equtable来方便与操作符==的判断,Equtable实现了使用props是否相等来判断两个对象是否相等,当然我们也可以自己重写操作符==来实现判断两个对象是否相等。

自己实现操作符如下:

  
@override
bool operator ==(Object other) {
if (other is Model)
return this.count == other.count &&
age == other.count &&
name == other.name;
return false;
}
 

使用Equtable操作符==关键代码如下:

// ignore: must_be_immutable
class Model extends Equatable {
int count;
int age;
String name;
List<String> list;
Model({this.count = , this.name, this.list, this.age = }); @override
List<Object> get props => [count, name, list, age];
Model addCount(int value) {
return clone()..count = count + value;
} Model addAge(int value) {
return clone()..age = age + value;
} Model clone() {
return Model(count: count, name: name, list: list, age: age);
}
}
 

构造一个装载Model数据的Cubit

class CounterCubit extends Cubit<Model> {
CounterCubit() : super(Model(count: , name: '老王')); void increment() {
print('CounterCubit +1');
emit(state.addCount());
} void decrement() {
print('CounterCubit -1');
emit(state.clone());
} void addAge(int v) {
emit(state.addAge(v));
} void addCount(int v) {
emit(state.addCount(v));
}
}
 

数据准备好之后准备展示了,首先在需要展示数据小部件上层包裹一层BlocProvider,关键代码:

BlocProvider(
create: (_) => CounterCubit(),
child: BaseBLoCRoute(),
)
 

要是多个model的话和Provider写法基本一致。

MultiBlocProvider(
providers: [
BlocProvider(
create: (_) => CounterCubit(),
),
BlocProvider(
create: (_) => CounterCubit2(),
),
],
child: BaseBLoCRoute(),
)
 

然后在展示数字的widget上开始展示数据了,BlocBuilder<CounterCubit, Model>CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.

BlocBuilder<CounterCubit, Model>(
builder: (_, count) {
print('CounterCubit1 ');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
child: Text(
'count: ${count.count}',
),
padding: EdgeInsets.all(),
),
OutlineButton(
child: Icon(Icons.arrow_drop_up),
onPressed: () {
context.bloc<CounterCubit>().addCount();
},
),
OutlineButton(
child: Icon(Icons.arrow_drop_down),
onPressed: () {
context.bloc<CounterCubit>().addCount(-);
},
)
],
);
},
buildWhen: (m1, m2) => m1.count != m2.count,
)
监听状态变更 /// 监听状态变更
void initState() {
Bloc.observer = SimpleBlocObserver();
super.initState();
} /// 观察者来观察 事件的变化 可以使用默认的 [BlocObserver]
class SimpleBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object event) {
print(event);
super.onEvent(bloc, event);
} @override
void onChange(Cubit cubit, Change change) {
print(change);
super.onChange(cubit, change);
} @override
void onTransition(Bloc bloc, Transition transition) {
print(transition);
super.onTransition(bloc, transition);
} @override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print(error);
super.onError(cubit, error, stackTrace);
}
}

局部刷新

布局刷新是使用BlocBuilder来实现的,BlocBuilder<CounterCubit, Model>CounterCubit是载体,Model是数据,使用builder回调来刷新UI,刷新UI的条件是buildWhen: (m1, m2) => m1.count != m2.count,当条件满足时进行回调builder.
本例子是多个model,多个局部UI刷新

  Widget _body() {
return Center(
child: CustomScrollView(
slivers: <Widget>[
SliverToBoxAdapter(
child: BlocBuilder<CounterCubit, Model>(
builder: (_, count) {
print('CounterCubit1 ');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
child: Text(
'count: ${count.count}',
),
padding: EdgeInsets.all(),
),
OutlineButton(
child: Icon(Icons.arrow_drop_up),
onPressed: () {
context.bloc<CounterCubit>().addCount();
},
),
OutlineButton(
child: Icon(Icons.arrow_drop_down),
onPressed: () {
context.bloc<CounterCubit>().addCount(-);
},
)
],
);
},
buildWhen: (m1, m2) => m1.count != m2.count,
),
),
SliverToBoxAdapter(
child: SizedBox(
height: ,
),
),
SliverToBoxAdapter(
child: BlocBuilder<CounterCubit, Model>(
builder: (_, count) {
print('CounterCubit age build ');
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
child: Text(
'age:${count.age}',
),
padding: EdgeInsets.all(),
),
OutlineButton(
child: Icon(Icons.arrow_drop_up),
onPressed: () {
context.bloc<CounterCubit>().addAge();
},
),
OutlineButton(
child: Icon(Icons.arrow_drop_down),
onPressed: () {
context.bloc<CounterCubit>().addAge(-);
},
)
],
);
},
buildWhen: (m1, m2) => m1.age != m2.age,
),
),
SliverToBoxAdapter(
child: BlocBuilder<CounterCubit2, Model>(
builder: (_, count) {
print('CounterCubit2 ');
return Column(
children: <Widget>[
Text('CounterCubit2: ${count.age}'),
OutlineButton(
child: Icon(Icons.add),
onPressed: () {
context.bloc<CounterCubit2>().addAge();
},
)
],
);
},
),
)
],
),
);
}
 

当我们点击加好或者减号已经被SimpleBlocObserver监听到,看下打印信息,每次model变更都会通知监听者。

flutter: Change { currentState: Model, nextState: Model }
flutter: CounterCubit2
flutter: Change { currentState: Model, nextState: Model }
flutter: CounterCubit2
 

复杂状态变更,监听和刷新UI

一个加减例子,每次加减我们在当前组件中监听,当状态变更的时候如何实现刷新UI,而且当age+count == 10的话返回上一页。

要满足此功能的话,同一个部件至少要listenerbuilder,正好官方提供的BlocConsumer可以实现,如果只需要监听则需要使用BlocListener,简单来说是BlocConsumer=BlocListener+BlocBuilder.

看关键代码:

BlocConsumer<CounterCubit, Model>(builder: (ctx, state) {
return Column(
children: <Widget>[
Text(
'age:${context.bloc<CounterCubit>().state.age} count:${context.bloc<CounterCubit>().state.count}'),
OutlineButton(
child: Text('age+1'),
onPressed: () {
context.bloc<CounterCubit>().addAge();
},
),
OutlineButton(
child: Text('age-1'),
onPressed: () {
context.bloc<CounterCubit>().addAge(-);
},
),
OutlineButton(
child: Text('count+1'),
onPressed: () {
context.bloc<CounterCubit>().addCount();
},
),
OutlineButton(
child: Text('count-1'),
onPressed: () {
context.bloc<CounterCubit>().addCount(-);
},
)
],
);
}, listener: (ctx, state) {
if (state.age + state.count == ) Navigator.maybePop(context);
})
 

效果如下:

复杂情况(Cubit)

登陆功能(继承 Cubit)

我们再编写一个完整登陆功能,分别用到BlocListener用来监听是否可以提交数据,用到BlocBuilder用来刷新UI,名字输入框和密码输入框分别用BlocBuilder包裹,实现局部刷新,提交按钮用BlocBuilder包裹用来展示可用和不可用状态。

此为bloc_login的官方例子的简单版本,想要了解更多请查看官方版本

观察者

观察者其实一个APP只需要写一次即可,一般在APP初始化配置即可。
我们这里只提供打印状态变更信息。

class DefaultBlocObserver extends BlocObserver {
@override
void onChange(Cubit cubit, Change change) {
if (kDebugMode)
print(
'${cubit.toString()} new:${change.toString()} old:${cubit.state.toString()}');
super.onChange(cubit, change);
}
}
 

在初始化指定观察者

@override
void initState() {
Bloc.observer=DefaultBlocObserver();
super.initState();
}
 

或者使用默认观察者

Bloc.observer = BlocObserver();
State(Model)

存储数据的state(Model),这里我们需要账户信息,密码信息,是否可以点击登录按钮,是否正在登录这些信息。

enum LoginState {
success,
faild,
isLoading,
}
enum BtnState { available, unAvailable } class LoginModel extends Equatable {
final String name;
final String password;
final LoginState state;
LoginModel({this.name, this.password, this.state});
@override
List<Object> get props => [name, password, state, btnVisiable];
LoginModel copyWith({String name, String pwd, LoginState loginState}) {
return LoginModel(
name: name ?? this.name,
password: pwd ?? this.password,
state: loginState ?? this.state);
} bool get btnVisiable =>
(password?.isNotEmpty ?? false) && (name?.isNotEmpty ?? false);
@override
String toString() {
return '$props';
}
}
 

Cubit

装载state的类,当state变更需要调用emit(state),state的变更条件是==,所以我们上边的state(Model)继承了Equatable,Equatable内部实现了操作符==函数,我们只需要将它所需props重写即可。

class LoginCubit extends Cubit<LoginModel> {
LoginCubit(state) : super(state);
void login() async {
emit(state.copyWith(loginState: LoginState.isLoading));
await Future.delayed(Duration(seconds: ));
if (state.btnVisiable == true)
emit(state.copyWith(loginState: LoginState.success));
emit(state.copyWith(loginState: LoginState.faild));
} void logOut() async {
emit(state.copyWith(
name: null,
pwd: null,
));
} void changeName({String name}) {
emit(state.copyWith(
name: name, pwd: state.password, loginState: state.state));
} void changePassword({String pwd}) {
emit(state.copyWith(name: state.name, pwd: pwd, loginState: state.state));
}
}
构造view

关键还是得看如何构造UI,首先输入框分别使用BlocBuilder包裹实现局部刷新,局部刷新的关键还是buildWhen得写的漂亮,密码输入框的话只需要判断密码是否改变即可,账号的话只需要判断账号是否发生改变即可,
按钮也是如此,在UI外层使用listener来监听状态变更,取所需要的状态跳转新的页面或者弹窗。

首先看下输入框关键代码:

class TextFiledNameRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginModel>(
builder: (BuildContext context, LoginModel state) {
return TextField(
onChanged: (v) {
context.bloc<LoginCubit>().changeName(name: v);
},
decoration: InputDecoration(
labelText: 'name',
errorText: state.name?.isEmpty ?? false ? 'name不可用' : null),
);
},
buildWhen: (previos, current) => previos.name != current.name);
}
} class TextFiledPasswordRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginModel>(
builder: (BuildContext context, LoginModel state) {
return TextField(
onChanged: (v) {
context.bloc<LoginCubit>().changePassword(pwd: v);
},
decoration: InputDecoration(
labelText: 'password',
errorText:
state.password?.isEmpty ?? false ? 'password不可用' : null),
);
},
buildWhen: (previos, current) => previos.password != current.password);
}
}
 

按钮根据不同的状态来显示可用或不可用或正在提交的动画效果。

class LoginButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginCubit, LoginModel>(
builder: (BuildContext context, LoginModel state) {
switch (state.state) {
case LoginState.isLoading:
return const CircularProgressIndicator();
default:
return RaisedButton(
child: const Text('login'),
onPressed: state.btnVisiable
? () {
context.bloc<LoginCubit>().login();
}
: null,
);
}
},
buildWhen: (previos, current) =>
previos.btnVisiable != current.btnVisiable ||
(current.state != previos.state));
}
}
 

小部件写好了,那么我们将他们组合起来

class BaseLoginPageRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => LoginCubit(LoginModel()),
child: BaseLoginPage(),
);
} static String routeName = '/BaseLoginPageRoute';
MaterialPageRoute get route =>
MaterialPageRoute(builder: (_) => BaseLoginPageRoute());
} class BaseLoginPage extends StatefulWidget {
BaseLoginPage({Key key}) : super(key: key); @override
_BaseLoginPageState createState() => _BaseLoginPageState();
} class _BaseLoginPageState extends State<BaseLoginPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('loginBLoC Cubit'),
),
body: _body(),
);
} Widget _body() {
return BlocListener<LoginCubit, LoginModel>(
listener: (context, state) {
if (state.state == LoginState.success) {
Scaffold.of(context)
..hideCurrentSnackBar()
..showSnackBar(const SnackBar(content: Text('登陆成功')));
}
},
child: Center(
child: Column(
children: <Widget>[
TextFiledNameRoute(),
TextFiledPasswordRoute(),
const SizedBox(
height: ,
),
LoginButton()
],
),
),
);
} @override
void initState() {
Bloc.observer = BlocObserver();
super.initState();
}
}
 

这里我们实现了登陆成功弹出snackBar.

看下效果图哦:

复杂情况(Bloc)

情况1都我们手动emit(state),那么有没有使用流技术来直接监听的呢?答案是有,那么我们再实现一遍使用bloc的登陆功能。

state(数据载体)

首先我们使用 一个抽象类来定义事件,然后各种小的事件都继承它,比如:NameEvent装载了姓名信息,PasswordEvent装载了密码信息,SubmittedEvent装载了提交信息,简单来讲,event就是每一个按钮点击事件或者valueChange事件触发的动作,最好下载代码之后自己对比下,然后自己从简单例子写,此为稍微复杂情况,看下关键代码:

/// 登陆相关的事件
abstract class LoginEvent extends Equatable {
const LoginEvent();
@override
List<Object> get props => [];
} /// 修改密码
class LoginChagnePassword extends LoginEvent {
final String password;
const LoginChagnePassword({this.password});
@override
List<Object> get props => [password];
} /// 修改账户
class LoginChagneName extends LoginEvent {
final String name;
const LoginChagneName({this.name});
@override
List<Object> get props => [name];
} /// 提交事件
class LoginSubmitted extends LoginEvent {
const LoginSubmitted();
@override
List<Object> get props => [];
}
 

存储数据的state,在LoginBloc中将event转换成state,那么state需要存储什么数据呢?需要存储账户信息、密码、登陆状态等信息。

/// 事件变更状态[正在请求,报错,登陆成功,初始化]
enum Login2Progress { isRequesting, error, success, init } /// 存储数据的model 在[bloc]中称作[state]
class LoginState2 extends Equatable {
final String name;
final String password;
final Login2Progress progress;
LoginState2({this.name, this.password, this.progress = Login2Progress.init});
@override
List<Object> get props => [name, password, btnVisiable, progress];
LoginState2 copyWith(
{String name, String pwd, Login2Progress login2progress}) {
return LoginState2(
name: name ?? this.name,
password: pwd ?? this.password,
progress: login2progress ?? this.progress);
} /// 使用 [UserName] &&[UserPassword]来校验规则
bool get btnVisiable => nameVisiable && passwordVisiable;
bool get nameVisiable => UserName(name).visiable;
bool get passwordVisiable => UserPassword(password).visiable; /// 是否展示名字错误信息 bool get showNameErrorText {
if (name?.isEmpty ?? true) return false;
return nameVisiable == false;
} /// 是否展示密码错误信息
bool get showPasswordErrorText {
if (password?.isEmpty ?? true) return false;
return passwordVisiable == false;
} @override
String toString() {
return '$props';
}
}
 

eventstate写好了,怎么将event转换成state呢?首先新建一个类继承Bloc,覆盖函数mapEventToState,利用这个函数参数event来对state,进行转换,中间因为用到了虚拟的网络登陆,耗时操作和状态变更,所以使用了yield*返回了另外一个流函数。

class LoginBloc extends Bloc<LoginEvent, LoginState2> {
LoginBloc(initialState) : super(initialState); @override
Stream<LoginState2> mapEventToState(event) async* {
if (event is LoginChagneName) {
yield _mapChangeUserNameToState(event, state);
} else if (event is LoginChagnePassword) {
yield _mapChangePasswordToState(event, state);
} else if (event is LoginSubmitted) {
yield* _mapSubmittedToState(event, state);
}
}
/// 改变密码
LoginState2 _mapChangePasswordToState(
LoginChagnePassword event, LoginState2 state2) {
return state2.copyWith(pwd: event.password ?? '');
} /// 改变名字
LoginState2 _mapChangeUserNameToState(
LoginChagneName event, LoginState2 state2) {
return state2.copyWith(name: event.name ?? '');
} /// 提交
Stream<LoginState2> _mapSubmittedToState(
LoginSubmitted event, LoginState2 state2) async* {
try {
if (state2.name.isNotEmpty && state2.password.isNotEmpty) {
yield state2.copyWith(login2progress: Login2Progress.isRequesting);
await Future.delayed(Duration(seconds: ));
yield state2.copyWith(login2progress: Login2Progress.success); yield state2.copyWith(login2progress: Login2Progress.init);
}
} on Exception catch (e) {
yield state2.copyWith(login2progress: Login2Progress.error);
}
}
}
 

stateevent事件整理成图方便理解一下:

构造view

样式我们还是使用上边的 ,但是发送事件却不一样,原因是继承bloc其实是实现了EventSink的接口,使用add()触发监听。

class TextFiledNameRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<LoginBloc, LoginState2>(
builder: (BuildContext context, LoginState2 state) {
return TextField(
onChanged: (v) {
context.bloc<LoginBloc>().add(LoginChagneName(name: v));
},
textAlign: TextAlign.center,
decoration: InputDecoration(
labelText: 'name',
errorText:
(state.showNameErrorText == true) ? 'name不可用' : null),
);
},
buildWhen: (previos, current) => previos.name != current.name);
}
}

  

 

完整的效果是:

BLoC 流程

首先view部件持有CubitCubit持有状态(Model),当状态(Model)发生变更时通知Cubit,Cubit依次通知listenerBlocBulder.builder进行刷新UI,每次状态变更都会通知BlocObserver,可以做到全局的状态监听。

千言万语不如一张图:

参考

Flutter 状态管理之BLoC的更多相关文章

  1. Flutter 状态管理- 使用 MobX

    文 / Paul Halliday, developer.school 创始人 众所周知,状态管理是每个软件项目都需要持续迭代更新的方向.它并不是一个「一次性」的工作, 而需要不断确保你遵循的最佳实践 ...

  2. Flutter 状态管理 flutter_Provide

    项目的商品类别页面将大量的出现类和类中间的状态变化,这就需要状态管理.现在Flutter的状态管理方案很多,redux.bloc.state.Provide. Scoped Model : 最早的状态 ...

  3. Flutter | 状态管理特别篇——Provide

    前言 今天偶然发现在谷歌爸爸的仓库下出现了一个叫做flutter-provide的状态管理框架,2月8日才第一次提交,非常新鲜.在简单上手之后感觉就是一个字--爽!所以今天就跟大家分享一下这个新的状态 ...

  4. Flutter状态管理之provide和provider的使用区别

    说道状态管理不得不说谷歌的亲自开发的两款状态管理Widget:第一个是provide,第二个是provider. 这两个的区别就是一个出来的早,现在好像没整么更新了.第二个是2019才出来的目前的版本 ...

  5. Flutter状态管理Provider,简单上手

    在之前的文章中介绍了 Google 官方仓库下的一个状态管理 Provide.乍一看这俩玩意可能很容易就被认为是同一个东西,仔细一看,这不就差了一个字吗,有什么区别呢. 首先,你要知道的最大的一个区别 ...

  6. Flutter实战视频-移动电商-24.Provide状态管理基础

    24.Provide状态管理基础 Flutter | 状态管理特别篇 —— Provide:https://juejin.im/post/5c6d4b52f265da2dc675b407?tdsour ...

  7. Flutter移动电商实战 --(24)Provide状态管理基础

    Flutter | 状态管理特别篇 —— Provide:https://juejin.im/post/5c6d4b52f265da2dc675b407?tdsourcetag=s_pcqq_aiom ...

  8. Flutter Bloc状态管理 简单上手

    我们都知道,Flutter中Widget的状态控制了UI的更新,比如最常见的StatefulWidget,通过调用setState({})方法来刷新控件.那么其他类型的控件,比如StatelessWi ...

  9. (转)flutter 新状态管理方案 Provide (一)-使用

    flutter 新状态管理方案 Provide (一)-使用     版权声明:本文为博主原创文章,基于CC4.0协议,首发于https://kikt.top ,同步发于csdn,转载必须注明出处! ...

随机推荐

  1. 全卷积神经网络FCN详解(附带Tensorflow详解代码实现)

    一.导论 在图像语义分割领域,困扰了计算机科学家很多年的一个问题则是我们如何才能将我们感兴趣的对象和不感兴趣的对象分别分割开来呢?比如我们有一只小猫的图片,怎样才能够通过计算机自己对图像进行识别达到将 ...

  2. Error: no such table: device;的问题的解决,去掉表名device后面的分号;

    sqlite> .mode csvsqlite> .import device.txt device;Error: no such table: device;sqlite> .im ...

  3. Java 异常处理专题,从入门到精通

    内置异常和Throwable核心方法 Java内置异常 可查异常(必须要在方法里面捕获或者抛出) ClassNoFoundException 应⽤程序试图加载类,找不到对应的类 IllegalAcce ...

  4. scrapy分布式浅谈+京东示例

    scrapy分布式浅谈+京东示例: 学习目标: 分布式概念与使用场景 浅谈去重 浅谈断点续爬 分布式爬虫编写流程 基于scrapy_redis的分布式爬虫(阳关院务与京东图书案例) 环境准备: 下载r ...

  5. PHP end() 函数

    实例 输出数组中的当前元素和最后一个元素的值: <?php$people = array("Peter", "Joe", "Glenn" ...

  6. 5073 [Lydsy1710月赛]小A的咒语

    LINK:[Lydsy1710月赛]小A的咒语 每次给定两个串 要求从a串中选出x段拼成B串 能否做到.T组数据. \(n\leq 100000,m\leq 100000,T\leq 10,x\leq ...

  7. HashMap源码(数组算法)

    Jdk1.8初始化hashMap容量的算法 static final int tableSizeFor(int cap) { // 先减1,避免传进来的本来就是2的n次幂,导致算出来多了一次幂,比如传 ...

  8. 正确的使用HttpClient

    快捷的网络请求,多用HttpClient 但是常规的写法会一大片的TIME_OUT 比如这样的例子 static async Task<string> TestHttpClient(str ...

  9. XSS 渗透思路笔记

    了解XSS首先要了解HTML里面的元素:共有5种元素:空元素.原始文本元素. RCDATA元素.外来元素以及常规元素. 空元素area.base.br.col. command. embed.hr.i ...

  10. JavaFX桌面应用开发-HelloWorld

    JavaFX是一个强大的图形和多媒体处理工具包集合,它允许开发者来设计.创建.测试.调试和部署富客户端程序,并且和Java一样跨平台. JavaFX比Swing好用很多,它允许开发使用FXML来设计和 ...