即使在事件重新触发后,Flutter BlocListener 也只执行一次
Flutter BlocListener executed only once even after event gets re-fired
我正在用 flutter 实现 Reso Coder's clean architecture。我按照他的指导将项目分成几层并使用依赖注入。在其中一种情况下,我希望出现以下情况:管理员用户登录,在其主屏幕上查看数据,对其进行编辑,然后按一个按钮,将数据保存到本地数据库 (sqflite)。保存数据后,我想显示一个带有某种文本 "Settings saved!" 的 Snackbar
。这是我的代码(部分):
class AdministratorPage extends StatefulWidget {
@override
_AdministratorPageState createState() => _AdministratorPageState();
}
class _AdministratorPageState extends State<AdministratorPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).backgroundColor,
centerTitle: true,
leading: Container(),
title: Text(AppLocalizations.of(context).translate('adminHomeScreen')),
),
body: SingleChildScrollView(
child: buildBody(context),
),
);
}
BlocProvider<SettingsBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => serviceLocator<SettingsBloc>(),
child: BlocListener<SettingsBloc, SettingsState>(
listener: (context, state) {
if (state is SettingsUpdatedState) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).translate('settingsUpdated')),
backgroundColor: Colors.blue,
),
);
}
},
child: Column(
children: <Widget>[
SizedBox(
height: 20.0,
),
AdministratorInput(),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('LOG OUT'),
onPressed: () {
serviceLocator<AuthenticationBloc>().add(LoggedOutEvent());
Routes.sailor(Routes.loginScreen);
},
),
),
],
),
),
);
}
}
这是 AdministratorInput
小部件:
class AdministratorInput extends StatefulWidget {
@override
_AdministratorInputState createState() => _AdministratorInputState();
}
class _AdministratorInputState extends State<AdministratorInput> {
String serverAddress;
String daysBack;
final serverAddressController = TextEditingController();
final daysBackController = TextEditingController();
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
if (state is SettingsInitialState) {
BlocProvider.of<SettingsBloc>(context)
.add(SettingsPageLoadedEvent());
} else if (state is SettingsFetchedState) {
serverAddressController.text =
serverAddress = state.settings.serverAddress;
daysBackController.text =
daysBack = state.settings.daysBack.toString();
}
return Column(
children: <Widget>[
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context)
.translate('serverAddress')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: serverAddressController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
serverAddress = value;
},
),
),
SizedBox(
height: 5.0,
),
// Days Back Text Field
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context).translate('daysBack')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: daysBackController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
daysBack = value;
},
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('SAVE CHANGES'),
onPressed: updatePressed,
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('REFRESH'),
onPressed: refreshPressed,
),
),
],
);
},
),
),
);
}
void updatePressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsUpdateButtonPressedEvent(
settings: SettingsAggregate(
serverAddress: serverAddress,
daysBack: int.parse(daysBack),
),
),
);
}
void refreshPressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsRefreshButtonPressedEvent(),
);
}
}
SettingsBloc 是带有事件和状态以及映射器方法的标准块模式。它是使用 get_it
包注入的。下面是如何实例化:
serviceLocator.registerFactory(
() => SettingsBloc(
pullUsersFromServerCommand: serviceLocator(),
getSettingsQuery: serviceLocator(),
updateSettingsCommand: serviceLocator(),
),
);
命令的所有实例和 bloc 构造函数的查询都以相同的方式正确实例化。
这是集团:
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final PullUsersFromServerCommand pullUsersFromServerCommand;
final UpdateSettingsCommand updateSettingsCommand;
final GetSettingsQuery getSettingsQuery;
SettingsBloc({
@required PullUsersFromServerCommand pullUsersFromServerCommand,
@required UpdateSettingsCommand updateSettingsCommand,
@required GetSettingsQuery getSettingsQuery,
}) : assert(pullUsersFromServerCommand != null),
assert(updateSettingsCommand != null),
assert(getSettingsQuery != null),
pullUsersFromServerCommand = pullUsersFromServerCommand,
updateSettingsCommand = updateSettingsCommand,
getSettingsQuery = getSettingsQuery;
@override
SettingsState get initialState => SettingsInitialState();
@override
Stream<SettingsState> mapEventToState(SettingsEvent event) async* {
if (event is SettingsPageLoadedEvent) {
final getSettingsEither = await getSettingsQuery(NoQueryParams());
yield* getSettingsEither.fold((failure) async* {
yield SettingsFetchedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsFetchedState(settings: result);
} else {
yield SettingsFetchedFailureState(
error: "settingsFetchFromDatabaseError");
}
});
} else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
} else if (event is SettingsRefreshButtonPressedEvent) {
final pullUsersFromServerEither =
await pullUsersFromServerCommand(NoCommandParams());
yield* pullUsersFromServerEither.fold((failure) async* {
yield SettingsRefreshedFailureState(
error: "settingsRefreshDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsRefreshedFailureState(error: "settingsRefreshedError");
}
});
}
}
}
我第一次进入此屏幕时一切正常。数据从数据库中获取,加载到屏幕上,如果我更改它并按保存,它会显示 snackbar
。我的问题是如果我想在停留在该屏幕上时再次编辑数据。我再次编辑它,因此触发更改事件,bloc 获取它,调用下面的正确命令并将数据保存在数据库中。然后改变 bloc 的状态,试图告诉 UI、"hey, I have a new state, get use of it"。但是 BlocListener
再也不会被调用了。
我应该如何实现我想要的行为?
编辑:
我在我登录用户的应用程序中添加了我之前使用的另一个集团。登录页面使用该块,如果用户名或密码错误,我将显示一个快餐栏,清除输入字段并让页面为更多内容做好准备。如果我用错误的凭据重试,我可以再次看到小吃店。
这是 LoginBloc:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticateUserCommand authenticateUserCommand;
final AuthenticationBloc authenticationBloc;
LoginBloc({
@required AuthenticateUserCommand authenticateUserCommand,
@required AuthenticationBloc authenticationBloc,
}) : assert(authenticateUserCommand != null),
assert(authenticationBloc != null),
authenticateUserCommand = authenticateUserCommand,
authenticationBloc = authenticationBloc;
@override
LoginState get initialState => LoginInitialState();
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressedEvent) {
yield LoginLoadingState();
final authenticateUserEither = await authenticateUserCommand(
AuthenticateUserParams(
username: event.username, password: event.password));
yield* authenticateUserEither.fold((failure) async* {
yield LoginFailureState(error: "loginDatabaseError");
}, (result) async* {
if (result != null) {
authenticationBloc.add(LoggedInEvent(token: result));
yield LoginLoggedInState(result);
} else {
yield LoginFailureState(error: "loginUsernamePasswordError");
}
});
}
}
}
这里的Event
和State
类扩展了Equatable
。由于它按照预期工作,我在设置页面(失败的地方)中以相同的方式进行了操作。从 UI 开始,我根据需要多次提高 LoginButtonPressedEvent
并分别调用 BlocListener
。
else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
//
// this part is the problem.
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
In general, you should use Equatable if you want to optimize your code to reduce the number of rebuilds. You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
它与 flutter_bloc 的工作原理是您不能产生相同的状态。是的,当您发出事件时,yield 状态之前的上述函数工作正常,但 yield 本身不会被调用。
所以基本上你的集团会发生什么,
- 当前状态为 SettingsFetchedState(settings: result)
- 您发出 SettingsUpdateButtonPressedEvent()
- Bloc yield SettingsUpdatedState()
- 状态从 SettingsFetchedState(settings: result) 更改为 SettingsUpdatedState()
- 当前状态为 SettingsUpdatedState()
- BlocListener 监听从 SettingsFetchedState(settings: result) 到 SettingsUpdatedState() 的状态变化
- 您发出 SettingsUpdateButtonPressedEvent()
- Bloc 不产生 SettingsUpdatedState(),它被忽略,因为相等比较 returns true)
- BlocListener 什么都不做,因为没有状态变化。
如何解决这个问题?根据我目前的知识,我没有足够的信心给出建议,所以也许可以试试引述 You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
编辑:
LoginBloc 的工作原理很简单,因为它为每个事件产生不同的状态。我认为您没有注意到,但它会在产生 LoginLoggedInState(result) 或 LoginFailureState(error: "loginUsernamePasswordError")
之前产生 LoginLoadingState()
- 当前状态为 LoginInitialState()
- 发出事件
- 让出 LoginLoadingState()
- 状态从 LoginInitialState() 更改为 LoginLoadingState()
- 产生 LoginLoggedInState() 或 LoginFailurestate()
- 状态从 LoginLoadingState() 更改为 LoginLoggedInState() 或 LoginFailurestate()
- 返回每个事件的第 2 步
@Federick Jonathan 已经对这个问题给出了足够的解释,但我想在这方面做插件。
第一件事:
这是 Equatable
的标准行为,事件侦听器在状态发生变化时被调用。如果你 yield
每次都处于相同的状态,那么什么都不会发生。
让我们讨论所有可能的解决方案。
从 bloc 中移除 Equatable
然后当 state
改变时每个事件触发。
为状态定义start
和end
状态。例如,将第一个 state
创建为 StartDataUpdate
,将第二个创建为 EndDataUpdate
.
参考以下代码
yield StartDataUpdate();
//Here... Please specified data changes related to operation.
yield EndDataUpdate();
Stream<ReportsState> setupState({required ReportsState state}) async* {
yield StartReportsState();
yield state;
yield EndReportsState();
}
使用:
yield* setupState( state: NavigationState() );
我正在用 flutter 实现 Reso Coder's clean architecture。我按照他的指导将项目分成几层并使用依赖注入。在其中一种情况下,我希望出现以下情况:管理员用户登录,在其主屏幕上查看数据,对其进行编辑,然后按一个按钮,将数据保存到本地数据库 (sqflite)。保存数据后,我想显示一个带有某种文本 "Settings saved!" 的 Snackbar
。这是我的代码(部分):
class AdministratorPage extends StatefulWidget {
@override
_AdministratorPageState createState() => _AdministratorPageState();
}
class _AdministratorPageState extends State<AdministratorPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).backgroundColor,
centerTitle: true,
leading: Container(),
title: Text(AppLocalizations.of(context).translate('adminHomeScreen')),
),
body: SingleChildScrollView(
child: buildBody(context),
),
);
}
BlocProvider<SettingsBloc> buildBody(BuildContext context) {
return BlocProvider(
create: (_) => serviceLocator<SettingsBloc>(),
child: BlocListener<SettingsBloc, SettingsState>(
listener: (context, state) {
if (state is SettingsUpdatedState) {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context).translate('settingsUpdated')),
backgroundColor: Colors.blue,
),
);
}
},
child: Column(
children: <Widget>[
SizedBox(
height: 20.0,
),
AdministratorInput(),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('LOG OUT'),
onPressed: () {
serviceLocator<AuthenticationBloc>().add(LoggedOutEvent());
Routes.sailor(Routes.loginScreen);
},
),
),
],
),
),
);
}
}
这是 AdministratorInput
小部件:
class AdministratorInput extends StatefulWidget {
@override
_AdministratorInputState createState() => _AdministratorInputState();
}
class _AdministratorInputState extends State<AdministratorInput> {
String serverAddress;
String daysBack;
final serverAddressController = TextEditingController();
final daysBackController = TextEditingController();
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
if (state is SettingsInitialState) {
BlocProvider.of<SettingsBloc>(context)
.add(SettingsPageLoadedEvent());
} else if (state is SettingsFetchedState) {
serverAddressController.text =
serverAddress = state.settings.serverAddress;
daysBackController.text =
daysBack = state.settings.daysBack.toString();
}
return Column(
children: <Widget>[
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context)
.translate('serverAddress')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: serverAddressController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
serverAddress = value;
},
),
),
SizedBox(
height: 5.0,
),
// Days Back Text Field
Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(AppLocalizations.of(context).translate('daysBack')),
],
),
),
Container(
height: 40.0,
child: TextField(
controller: daysBackController,
decoration: InputDecoration(
border: OutlineInputBorder(),
),
onChanged: (value) {
daysBack = value;
},
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('SAVE CHANGES'),
onPressed: updatePressed,
),
),
SizedBox(
width: double.infinity,
child: RaisedButton(
child: Text('REFRESH'),
onPressed: refreshPressed,
),
),
],
);
},
),
),
);
}
void updatePressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsUpdateButtonPressedEvent(
settings: SettingsAggregate(
serverAddress: serverAddress,
daysBack: int.parse(daysBack),
),
),
);
}
void refreshPressed() {
BlocProvider.of<SettingsBloc>(context).add(
SettingsRefreshButtonPressedEvent(),
);
}
}
SettingsBloc 是带有事件和状态以及映射器方法的标准块模式。它是使用 get_it
包注入的。下面是如何实例化:
serviceLocator.registerFactory(
() => SettingsBloc(
pullUsersFromServerCommand: serviceLocator(),
getSettingsQuery: serviceLocator(),
updateSettingsCommand: serviceLocator(),
),
);
命令的所有实例和 bloc 构造函数的查询都以相同的方式正确实例化。
这是集团:
class SettingsBloc extends Bloc<SettingsEvent, SettingsState> {
final PullUsersFromServerCommand pullUsersFromServerCommand;
final UpdateSettingsCommand updateSettingsCommand;
final GetSettingsQuery getSettingsQuery;
SettingsBloc({
@required PullUsersFromServerCommand pullUsersFromServerCommand,
@required UpdateSettingsCommand updateSettingsCommand,
@required GetSettingsQuery getSettingsQuery,
}) : assert(pullUsersFromServerCommand != null),
assert(updateSettingsCommand != null),
assert(getSettingsQuery != null),
pullUsersFromServerCommand = pullUsersFromServerCommand,
updateSettingsCommand = updateSettingsCommand,
getSettingsQuery = getSettingsQuery;
@override
SettingsState get initialState => SettingsInitialState();
@override
Stream<SettingsState> mapEventToState(SettingsEvent event) async* {
if (event is SettingsPageLoadedEvent) {
final getSettingsEither = await getSettingsQuery(NoQueryParams());
yield* getSettingsEither.fold((failure) async* {
yield SettingsFetchedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsFetchedState(settings: result);
} else {
yield SettingsFetchedFailureState(
error: "settingsFetchFromDatabaseError");
}
});
} else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
} else if (event is SettingsRefreshButtonPressedEvent) {
final pullUsersFromServerEither =
await pullUsersFromServerCommand(NoCommandParams());
yield* pullUsersFromServerEither.fold((failure) async* {
yield SettingsRefreshedFailureState(
error: "settingsRefreshDatabaseError");
}, (result) async* {
if (result != null) {
yield SettingsUpdatedState();
} else {
yield SettingsRefreshedFailureState(error: "settingsRefreshedError");
}
});
}
}
}
我第一次进入此屏幕时一切正常。数据从数据库中获取,加载到屏幕上,如果我更改它并按保存,它会显示 snackbar
。我的问题是如果我想在停留在该屏幕上时再次编辑数据。我再次编辑它,因此触发更改事件,bloc 获取它,调用下面的正确命令并将数据保存在数据库中。然后改变 bloc 的状态,试图告诉 UI、"hey, I have a new state, get use of it"。但是 BlocListener
再也不会被调用了。
我应该如何实现我想要的行为?
编辑: 我在我登录用户的应用程序中添加了我之前使用的另一个集团。登录页面使用该块,如果用户名或密码错误,我将显示一个快餐栏,清除输入字段并让页面为更多内容做好准备。如果我用错误的凭据重试,我可以再次看到小吃店。
这是 LoginBloc:
class LoginBloc extends Bloc<LoginEvent, LoginState> {
final AuthenticateUserCommand authenticateUserCommand;
final AuthenticationBloc authenticationBloc;
LoginBloc({
@required AuthenticateUserCommand authenticateUserCommand,
@required AuthenticationBloc authenticationBloc,
}) : assert(authenticateUserCommand != null),
assert(authenticationBloc != null),
authenticateUserCommand = authenticateUserCommand,
authenticationBloc = authenticationBloc;
@override
LoginState get initialState => LoginInitialState();
@override
Stream<LoginState> mapEventToState(LoginEvent event) async* {
if (event is LoginButtonPressedEvent) {
yield LoginLoadingState();
final authenticateUserEither = await authenticateUserCommand(
AuthenticateUserParams(
username: event.username, password: event.password));
yield* authenticateUserEither.fold((failure) async* {
yield LoginFailureState(error: "loginDatabaseError");
}, (result) async* {
if (result != null) {
authenticationBloc.add(LoggedInEvent(token: result));
yield LoginLoggedInState(result);
} else {
yield LoginFailureState(error: "loginUsernamePasswordError");
}
});
}
}
}
这里的Event
和State
类扩展了Equatable
。由于它按照预期工作,我在设置页面(失败的地方)中以相同的方式进行了操作。从 UI 开始,我根据需要多次提高 LoginButtonPressedEvent
并分别调用 BlocListener
。
else if (event is SettingsUpdateButtonPressedEvent) {
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate: event.settings));
yield* updateSettingsEither.fold((failure) async* {
yield SettingsUpdatedFailureState(error: "settingsDatabaseError");
}, (result) async* {
if (result != null) {
//
// this part is the problem.
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: "settingsUpdateToDatabaseError");
}
});
In general, you should use Equatable if you want to optimize your code to reduce the number of rebuilds. You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
它与 flutter_bloc 的工作原理是您不能产生相同的状态。是的,当您发出事件时,yield 状态之前的上述函数工作正常,但 yield 本身不会被调用。
所以基本上你的集团会发生什么,
- 当前状态为 SettingsFetchedState(settings: result)
- 您发出 SettingsUpdateButtonPressedEvent()
- Bloc yield SettingsUpdatedState()
- 状态从 SettingsFetchedState(settings: result) 更改为 SettingsUpdatedState()
- 当前状态为 SettingsUpdatedState()
- BlocListener 监听从 SettingsFetchedState(settings: result) 到 SettingsUpdatedState() 的状态变化
- 您发出 SettingsUpdateButtonPressedEvent()
- Bloc 不产生 SettingsUpdatedState(),它被忽略,因为相等比较 returns true)
- BlocListener 什么都不做,因为没有状态变化。
如何解决这个问题?根据我目前的知识,我没有足够的信心给出建议,所以也许可以试试引述 You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.
编辑:
LoginBloc 的工作原理很简单,因为它为每个事件产生不同的状态。我认为您没有注意到,但它会在产生 LoginLoggedInState(result) 或 LoginFailureState(error: "loginUsernamePasswordError")
之前产生 LoginLoadingState()- 当前状态为 LoginInitialState()
- 发出事件
- 让出 LoginLoadingState()
- 状态从 LoginInitialState() 更改为 LoginLoadingState()
- 产生 LoginLoggedInState() 或 LoginFailurestate()
- 状态从 LoginLoadingState() 更改为 LoginLoggedInState() 或 LoginFailurestate()
- 返回每个事件的第 2 步
@Federick Jonathan 已经对这个问题给出了足够的解释,但我想在这方面做插件。
第一件事:
这是 Equatable
的标准行为,事件侦听器在状态发生变化时被调用。如果你 yield
每次都处于相同的状态,那么什么都不会发生。
让我们讨论所有可能的解决方案。
从 bloc 中移除
Equatable
然后当state
改变时每个事件触发。为状态定义
start
和end
状态。例如,将第一个state
创建为StartDataUpdate
,将第二个创建为EndDataUpdate
.
参考以下代码
yield StartDataUpdate();
//Here... Please specified data changes related to operation.
yield EndDataUpdate();
Stream<ReportsState> setupState({required ReportsState state}) async* {
yield StartReportsState();
yield state;
yield EndReportsState();
}
使用:
yield* setupState( state: NavigationState() );