Flutter_bloc使用从代码生成说起
框架模板代码生成
VSCode 有个flutter的插件 Flutter Files,
安装后在项目栏,右击可以新建对应的模板代码。
选择【new small pack bloc】,输入page名称 小写下划线风格:wesin_test
对应的会生成一个wesin_test文件夹,
文件夹里有
- wesin_test_page.dart
- wesin_test_screen.dart
- wesin_test_bloc.dart
- wesin_test_event.dart
- wesin_test_state.dart
大致说明下每个页面的用途。
- page
class RegisterPage extends StatefulWidget {
static const String routeName = '/register';
RegisterPage(this.phone, this.verifyCode);
final String verifyCode;
final String phone;
@override
_RegisterPageState createState() => _RegisterPageState();
}
class _RegisterPageState extends State<RegisterPage> {
final _registerBloc = RegisterBloc(RegisterState());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(LS.of(context, "register")),
),
body: BlocProvider<RegisterBloc>(
create: (_) => _registerBloc,
child: RegisterScreen(widget.phone),
),
);
}
}
复制代码
作为一个主体的页面,定义了route名称。
可以使用StatelessWidget
。因为正常也没什么状态管理
在state里面的builder里构建了BlocProvider
,
body: BlocProvider<RegisterBloc>(
create: (_) => _registerBloc,
child: RegisterScreen(widget.phone),
)
复制代码
这里是为后续的节点注册了一个RegisterBloc
, 从这个节点下级的widget就可以通过Provider.of<RegisterBloc>
来获取。
当然也可以有其他的方式比如context.read<RegisterBloc>
来获取。
这里也可以把bloc传给screen,可以方便在screen里面获取bloc。而不是通过在配置树里查找bloc。能稍微减少些性能消耗。
- screen
screen主要是构建配置页面内容用。
class RegisterScreen extends StatelessWidget {
const RegisterScreen(
this.phone, {
Key? key,
}) : super(key: key);
final String phone;
@override
Widget build(BuildContext context) {
return BlocListener<RegisterBloc, RegisterState>(
listener: (ctx, state) {
if (state.status.isSubmissionInProgress) {
EasyLoading.show(
maskType: EasyLoadingMaskType.clear, dismissOnTap: false);
return;
}
if (state.status.isSubmissionFailure) {
EasyLoading.dismiss();
var message = LS.of(context, "register_error");
EasyLoading.showError('$message:${state.errMessage ?? ''}');
return;
}
if (state.status.isSubmissionSuccess) {
EasyLoading.dismiss();
showCupertinoDialog(
context: context,
builder: (context) {
return
AlertDialog(
content: Text(LS.of(context, "register_success")),
actions: [TextButton(onPressed: (){
Navigator.of(context).popUntil(ModalRoute.withName("/"));
}, child: Text(LS.of(context, "confirm")))],
);
});
}
},
child: Align(
alignment: Alignment(0, -1),
child: Padding(
padding: const EdgeInsets.only(top: 31, left: 36, right:36),
child: Column(children: [
_UsernameInput(),
_PasswordInput(),
_ConfirmPasswordInput(),
const SizedBox(
height: 20,
),
_RegisterButton()
]),
),
));
}
}
class _RegisterButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<RegisterBloc, RegisterState>(builder: (context, state) {
return CircleRectButton(
child: Text(LS.of(context, "register"), style: TextStyle(letterSpacing: 20)),
onPressed: state.status.isValidated
? () async {
context.read<RegisterBloc>().add(RegisterSubmitEvent());
}
: null);
});
}
}
复制代码
BlocListener:
通过BlocListener
来监听状态消息,收到消息child不会rebuild。适合拿来弹出加载页,错误信息等。
BlocBuilder
通过BlocBuilder
来包装需要修改的内容。
这里的在组装页面的时候尽量在涉及到更新的地方使用
BlocBuilder
,也尽可能的考虑buildwhen
属性。保持当必须要更新时才重新build的思路。
- bloc
bloc 在封装后没什么逻辑,他的思路就是接受页面的事件,转为状态变更返回给页面。
class RegisterBloc extends Bloc<RegisterEvent, RegisterState> {
RegisterBloc(RegisterState initialState) : super(initialState);
@override
Stream<RegisterState> mapEventToState(
RegisterEvent event,
) async* {
try {
yield* event.applyAsync(currentState: state, bloc: this);
} catch (_, stackTrace) {
developer.log('$_', name: 'RegisterBloc', error: _, stackTrace: stackTrace);
yield state;
}
}
}
复制代码
将对应的事件处理放在event里面去执行显然更符合设计模式。代码也会更清晰,而不用做很多判断
if(event is **event){
do(**)
} else if (event is **event) {
do(**)
}
复制代码
- event
@immutable
abstract class RegisterEvent {
const RegisterEvent();
Stream<RegisterState> applyAsync({required RegisterState currentState, required RegisterBloc bloc});
}
class UsernameChangeEvent extends RegisterEvent {
const UsernameChangeEvent(this.username);
final String username;
Stream<RegisterState> applyAsync({required RegisterState currentState, required RegisterBloc bloc}) async* {
var input = UsernameInput.dirty(username);
var status = Formz.validate([input, currentState.password, currentState.confirmPassword]);
yield currentState.copyWith(username: input, status: status);
}
}
复制代码
抽象event,定义事件接口。
子类event来接收参数,实现事件接口并抛出新的状态给页面。
- state
class RegisterState extends Equatable {
final UsernameInput username;
final PasswordTwoInput password;
final PasswordTwoInput confirmPassword;
final FormzStatus status;
final String? errMessage;
RegisterState({
this.username = const UsernameInput.pure(),
this.password = const PasswordTwoInput.pure(),
this.confirmPassword = const PasswordTwoInput.pure(),
this.status = FormzStatus.pure,
this.errMessage
});
@override
List<Object> get props => [username, password, confirmPassword, status];
RegisterState copyWith({
UsernameInput? username,
PasswordTwoInput? password,
PasswordTwoInput? confirmPassword,
FormzStatus? status,
}) {
return RegisterState(
username: username ?? this.username,
password: password ?? this.password,
confirmPassword: confirmPassword ?? this.confirmPassword,
status: status ?? this.status,
);
}
}
复制代码
state可以考虑抽象state和单独一个state类。看具体业务吧。如果属性很简单,可以考虑抽象。属性比较多的情况下就没必要抽象了。
思路总结
- Page作为整个页面的对外对象,给外部调用,同时定义和注册了本页面需要用的bloc.
- Screen配置了整个页面展示的widget。并把事件抛给bloc.通过接收bloc的最新状态,并rebuild UI.
- bloc里面处理事件并转为state返回给页面
题外话
Cubit
还有一套cubit的简单模板,其实就是把bloc换成了cubit。去掉了event的逻辑。
screen里面事件调用cubit的方法。在cubit里面emit(new state).如此页面就可以接收到最新的状态了。
GET
在使用体验上GET框架不依赖context。对于使用体验来说应该更好些。对于简单的状态管理,也是GET更方便。