Flutter BlocListener仅在事件重新触发后仅执行一次 [英] Flutter BlocListener executed only once even after event gets re-fired

查看:158
本文介绍了Flutter BlocListener仅在事件重新触发后仅执行一次的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在实施 Reso Coder的简洁架构 。我按照他的指导将项目分为多个层并使用依赖项注入。在一种情况下,我希望具有以下情形:管理员用户登录,在其主屏幕上查看数据,进行编辑,然后按一个按钮,将数据保存到本地db(sqflite)。保存数据后,我想显示一个 Snackbar 并带有某种文本设置已保存!。例如。这是我的代码(部分):

  class AdministratorPage扩展了StatefulWidget {
@override
_AdministratorPageState createState() => _AdministratorPageState();
}

类_AdministratorPageState扩展了State< AdministratorPage> {
Widget build(BuildContext context){
return Scaffold(
appBar:AppBar(
backgroundColor:Theme.of(context).backgroundColor,
centerTitle:true,
开头:Container(),
标题:Text(AppLocalizations.of(context).translate('adminHomeScreen')),
),
正文:SingleChildScrollView(
个孩子:buildBody(context),
),
);
}

BlocProvider< SettingsBloc> buildBody(BuildContext context){
return BlocProvider(
create:(_)=> serviceLocator< SettingsBloc>(),
子级:BlocListener< SettingsBloc,SettingsState>(
侦听器:(上下文,状态){
如果(状态为SettingsUpdatedState){
Scaffold.of(context).showSnackBar(
SnackBar(
content:文本(
AppLocalizations .of(context).translate('settingsUpdated')),
backgroundColor:Colors.blue,
),
);
}
},
子代:列(
子代:< Widget> [
SizedBox(
高度:20.0,
),
AdministratorInput(),
SizedBox(
宽度:double.infinity,
子级:RaisedButton(
子级:Text('LOG OUT'),
onPressed:(){
serviceLocator< AuthenticationBloc>()。add(LoggedOutEvent());
Routes.sailor(Routes.loginScreen);
},
),
),
],
),
),
);
}
}

这是 AdministratorInput 小部件:

  class AdministratorInput扩展了StatefulWidget {
@override
_AdministratorInputState createState() => _AdministratorInputState();
}

类_AdministratorInputState扩展了State< AdministratorInput> {
字符串serverAddress;
字符串daysBack;
最终serverAddressController = TextEditingController();
的最后几天BackController = TextEditingController();

@override
小部件build(BuildContext context){
return Center(
child:Padding(
padding:const EdgeInsets.all(10.0),
子级:BlocBuilder< SettingsBloc,SettingsState>(
builder:(context,state){
if(state is SettingsInitialState){
BlocProvider.of< SettingsBloc>(context)
.add(SettingsPageLoadedEvent());
}否则(状态为SettingsFetchedState){
serverAddressController.text =
serverAddress = state.settings.serverAddress;
daysBackController。 text =
daysBack = state.settings.daysBack.toString();
}

return Column(
children:< Widget> [
容器(
子:Row(
crossAxisAlignment:CrossAxisAlignment.start,
子级:< Widget> [
Text(AppLocalizations.of(context)
.translate('serverAddress')),
],
),
),
容器(
高度:40.0,
子级:TextField(
控制器:serverAddressController,
装饰:InputDecoration(
边框:OutlineInputBorder(),
),
onChanged :(值){
serverAddress =值;
},
),
),
SizedBox(
height:5.0,
),
//天后文本字段
Container(
child:Row(
crossAxisAlignment:CrossAxisAlignment.start,
child:< Widget> [
Text(AppLocalizations.of(context).translate('daysBack ')),
],
),
),
容器(
高度:40.0,
子对象:TextField(
控制器: daysBackController,
装饰:InputDecoration(
边框:OutlineInputBorder(),
),
onChanged :(值){
daysBack = value;
},
),
),
SizedBox(
width:double.infinity,
子级:RaisedButton(
子级:Text('SAVE CHANGES'),
onPressed:updatePressed,
),
),
SizedBox(
width:double.infinity,
child:RaisedButton(
child:Text('REFRESH '),
onPressed:refreshPressed,
),
),
],
);
},
),
),
);
}

void updatePressed(){
BlocProvider.of< SettingsBloc>(上下文).add(
SettingsUpdateButtonPressedEvent(
settings:SettingsAggregate(
serverAddress:serverAddress,
daysBack:int.parse(daysBack),
),
),
);
}

void refreshPressed(){
BlocProvider.of< SettingsBloc>(上下文).add(
SettingsRefreshButtonPressedEvent(),
);
}
}

SettingsBloc是具有事件和状态的标准bloc模式和一个映射器方法。正在使用 get_it 软件包进行注入。实例化方法如下:

  serviceLocator.registerFactory(
()=> SettingsBloc(
pullUsersFromServerCommand: serviceLocator(),
getSettingsQuery:serviceLocator(),
updateSettingsCommand:serviceLocator(),
),
);

所有命令实例和对bloc构造函数的查询均以相同的方式正确实例化。 / p>

这里是集团:

  class SettingsBloc扩展了Bloc< SettingsEvent,SettingsState> ; {
final PullUsersFromServerCommand pullUsersFromServerCommand;
最后的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);
},(结果)async * {
if(结果!= null){
yield SettingsFetchedState(settings:result);
} else {
yield SettingsFetchedFailureState(
error: settingsFetchFromDatabaseError);
}
});
}否则,如果(事件是SettingsUpdateButtonPressedEvent){
final updateSettingsEither =等待updateSettingsCommand(
UpdateSettingsParams(settingsAggregate:event.settings));

yield * updateSettingsEither.fold((failure)async * {
yield SettingsUpdatedFailureState(error: settingsDatabaseError);
},(结果)async * {
if(结果!= null){
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
error: settingsUpdateToDatabaseError);
}
});
}否则,如果(事件为SettingsRefreshButtonPressedEvent){
final pullUsersFromServerEither =
等待pullUsersFromServerCommand(NoCommandParams());

yield * pullUsersFromServerEither.fold((failure)async * {
yield SettingsRefreshedFailureState(
error: settingsRefreshDatabaseError);
},(结果)异步* {
if(结果!= null){
yield SettingsUpdatedState();
} else {
yield SettingsRefreshedFailureState(error: settingsRefreshedError);
}
});
}
}
}

我是第一次输入屏幕上一切正常。从数据库中获取数据,将其加载到屏幕上,如果我更改它并按SAVE,它会显示小吃条。我的问题是,如果我想在停留在该屏幕上的同时再次编辑数据。我再次对其进行编辑,因此触发更改事件,块将其获取,在下面调用适当的命令并将数据保存在数据库中。然后更改块的状态,以尝试告诉UI:嘿,我有一个新状态,请使用它。但是 BlocListener 再也不会被调用。



我应该如何实现我想要的行为?



编辑:
我要在我之前登录用户的App中添加另一个我正在使用的块。登录页面利用该块,并在错误的用户名或密码后显示一个小吃栏,清除输入字段,并为该页面准备更多内容。如果我使用错误的凭据再次尝试,我可以再次看到小吃栏。



这是LoginBloc:

 类LoginBloc扩展了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 =等待authenticateUserCommand(
AuthenticateUserParams(
用户名:event.username,密码:event.password));

yield * authenticateUserEither.fold((failure)async * {
yield LoginFailureState(error: loginDatabaseError);
},(结果)async * {
if(结果!= null){
authenticationBloc.add(LoggedInEvent(token:result));
产生LoginLoggedInState(result);
} else {
产生LoginFailureState(错误: loginUsernamePasswordError);
}
});
}
}
}

事件状态类在这里扩展了等价。由于它按照预期工作,因此我在设置页面(失败的地方)中以同样的方式进行了操作。从UI上,我多次调用 LoginButtonPressedEvent ,并分别调用 BlocListener

解决方案

 否则,(事件为SettingsUpdateButtonPressedEvent){
final updateSettingsEither = await updateSettingsCommand(
UpdateSettingsParams(settingsAggregate:event.settings));

yield * updateSettingsEither.fold((failure)async * {
yield SettingsUpdatedFailureState(error: settingsDatabaseError);
},(结果)async * {
if(result!= null){
//
//这是问题所在。
yield SettingsUpdatedState();
} else {
yield SettingsUpdatedFailureState(
错误: settingsUpdateToDatabaseError);
}
});




通常,如果要优化代码,应使用Equatable减少重建次数。如果要使同一状态背对背触发多个转换,则不应使用Equatable。


来源: 何时可使用

如何与flutter_bloc配合使用,是因为您无法产生相同的状态。是的,在发出事件时,上面的在yield状态之前的函数可以正常工作,但是yield本身不会被调用。



所以,基本上,您的bloc会发生什么是,


  1. 当前状态为SettingsFetchedState(设置:结果)

  2. 您发出SettingsUpdateButtonPressedEvent()

  3. 批量收益SettingsUpdatedState()

  4. 状态从SettingsFetchedState(settings:result)更改为SettingsUpdatedState()

  5. 当前状态为SettingsUpdatedState()

  6. BlocListener侦听从SettingsFetchedState(settings:result)到SettingsUpdatedState()的状态更改

  7. 您发出SettingsUpdateButtonPressedEvent()

  8. Bloc不产生SettingsUpdatedState(),因为相等比较返回true,所以它会被忽略)

  9. BlocListener不执行任何操作,因为状态没有变化。

如何解决此问题?我没有足够的信心根据当前的知识提出建议,所以也许尝试引用引号如果要使同一状态连续背对触发多个转换,则不要使用Equatable。



编辑:



LoginBloc的工作仅仅是因为它产生每个事件的状态不同。我认为您没有注意到,但是在产生LoginLoggedInState(result)或LoginFailureState(error: loginUsernamePasswordError)之前会产生LoginLoadingState()


  1. 当前状态为LoginInitialState()

  2. 发出事件

  3. Yield LoginLoadingState()

  4. LoginInitialState的状态更改()到LoginLoadingState()

  5. 通过LoginLoggedInState()或LoginFailurestate()

  6. 状态从LoginLoadingState()更改为LoginLoggedInState()或LoginFailurestate ()

  7. 对于每个事件返回步骤2


I am implementing Reso Coder's clean architecture in flutter. I followed his guides in dividing the project to layers and using dependency injection. In one of the cases I want to have the following scenario: An administrator user logs in, sees data on their home screen, edits it and by pressing a button, saves the data to the local db (sqflite). Upon saving the data I want to show a Snackbar with some sort of text "Settings saved!" for example. Here's my code (parts):

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);
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Here's the AdministratorInput widget:

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(),
    );
  }
}

The SettingsBloc is a standard bloc pattern with events and states and a mapper method. It is being injected using get_it package. Here's how is instantiated:

serviceLocator.registerFactory(
    () => SettingsBloc(
      pullUsersFromServerCommand: serviceLocator(),
      getSettingsQuery: serviceLocator(),
      updateSettingsCommand: serviceLocator(),
    ),
  );

All instances of the commands and query for the constructor of the bloc are instantiated properly the same way.

Here's the 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");
        }
      });
    }
  }
}

The first time I enter this screen everything works perfect. The data is fetched from the database, loaded on screen and if I change it and press SAVE, it shows the snackbar. My problem is if I want to edit the data again while staying on that screen. I edit it again, therefore fire the changing event, the bloc gets it, calls the proper command below and the data is saved in the database. Then the state of the bloc is changed in attempt to tell the UI, "hey, I have a new state, get use of it". But the BlocListener never gets called again.

How should I achieve the behavior I desire?

EDIT: I am adding another bloc I am using earlier in the App where I log in users. The Login Page utilizes that bloc and upon wrong username or password, I am showing a snackbar, clearing the input fields and leaving the page ready for more. If I try again with wrong credentials, I can see the snackbar again.

Here is the 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");
        }
      });
    }
  }
}

The Event and State classes here extend Equatable. And since it was working according to the expectations, I did it the same way in the Settings Page (where it failed). From the UI I raise the LoginButtonPressedEvent as many times as I want and the BlocListener gets called respectively.

解决方案

    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.

The source: when-to-use-equatable

How it works with flutter_bloc is you can't yield the same state. Yes, the above function before yield the state is working fine when you emit the event, but the yield itself doesn't get called.

So basically what happens with your bloc is,

  1. Current state is SettingsFetchedState(settings: result)
  2. You emit SettingsUpdateButtonPressedEvent()
  3. Bloc yield SettingsUpdatedState()
  4. State changes from SettingsFetchedState(settings: result) to SettingsUpdatedState()
  5. Current state is SettingsUpdatedState()
  6. BlocListener listens to state changes from SettingsFetchedState(settings: result) to SettingsUpdatedState()
  7. You emit SettingsUpdateButtonPressedEvent()
  8. Bloc doesn't yield SettingsUpdatedState(), it is ignored because the equality comparison returns true)
  9. BlocListener does nothing because there is no state changes.

How to fix this? I am not confident enough to give suggestion based on my current knowledge, so maybe try what the quote says You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.

EDIT :

LoginBloc works simply because it yield different state for each event. I think you don't notice but it yield LoginLoadingState() before yield either LoginLoggedInState(result) or LoginFailureState(error: "loginUsernamePasswordError")

  1. Current state is LoginInitialState()
  2. Emit event
  3. Yield LoginLoadingState()
  4. State changes from LoginInitialState() to LoginLoadingState()
  5. Yield either LoginLoggedInState() or LoginFailurestate()
  6. State changes from LoginLoadingState() to either LoginLoggedInState() or LoginFailurestate()
  7. Back to step 2 for every event

这篇关于Flutter BlocListener仅在事件重新触发后仅执行一次的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆