Flutter BlocListener выполняется только один раз, даже после повторного запуска события - PullRequest
1 голос
/ 09 марта 2020

Я реализую Чистую архитектуру Reso Coder во флаттере. Я следовал его указаниям при разделении проекта на слои и использовании внедрения зависимостей. В одном из случаев я хочу иметь следующий сценарий: пользователь-администратор входит в систему, видит данные на своем домашнем экране, редактирует их и, нажав кнопку, сохраняет данные в локальную базу данных (sqflite). После сохранения данных я хочу показать 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(),
    );
  }
}

SettingsBlo c - это стандартный шаблон blo c с событиями и состояниями и метод картографирования. Он вводится с использованием пакета get_it. Вот как создается экземпляр:

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

Все экземпляры команд и запросов для конструктора блока blo c правильно создаются одинаково.

Вот блок blo c:

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

При первом входе на этот экран все работает отлично. Данные извлекаются из базы данных, загружаются на экран, и если я изменяю их и нажимаю SAVE, отображается snackbar. Моя проблема в том, что я хочу снова редактировать данные, оставаясь на этом экране. Я снова его редактирую, поэтому запускаю событие изменения, блок blo c получает его, вызывает соответствующую команду ниже и данные сохраняются в базе данных. Затем состояние blo c изменяется в попытке сказать пользовательскому интерфейсу: «Эй, у меня есть новое состояние, используйте его». Но BlocListener больше никогда не вызывают.

Как мне добиться желаемого поведения?

РЕДАКТИРОВАТЬ: Я добавляю еще один блок c Я использую ранее в приложении, где я вхожу в систему пользователей. Страница входа использует этот блок c, и при неправильном имени пользователя или пароля я показываю снэк-бар, очищаю поля ввода и оставляю страницу готовой к дальнейшим действиям. Если я попытаюсь снова с неправильными учетными данными, я снова смогу увидеть снэк-бар.

Вот LoginBlo c:

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. И поскольку он работал в соответствии с ожиданиями, я сделал то же самое на странице настроек (где это не удалось). Из пользовательского интерфейса я поднимаю LoginButtonPressedEvent столько раз, сколько хочу, и BlocListener вызывается соответственно.

1 Ответ

2 голосов
/ 09 марта 2020
    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");
        }
      });

В общем, вы должны использовать Equatable, если вы хотите оптимизировать свой код, чтобы уменьшить количество пересборок. Вам не следует использовать Equatable, если вы хотите, чтобы одно и то же состояние в спине-спине вызывало несколько переходов.

Источник: когда используется для эквалайзера

Как это работает с flutter_blo c, если вы не можете выдать одно и то же состояние. Да, вышеупомянутая функция перед yield возвращает состояние нормально, когда вы генерируете событие, но сам yield не вызывается.

Так что в основном то, что происходит с вашим блоком blo c,

  1. Текущее состояние - SettingsFetchedState (настройки: результат)
  2. Вы отправляете SettingsUpdateButtonPressedEvent ()
  3. Blo c yield SettingsUpdatedState ()
  4. Состояние изменяется из SettingsFetchedState ( settings: result) to SettingsUpdatedState ()
  5. Текущее состояние: SettingsUpdatedState ()
  6. BlocListener прослушивает изменения состояния из SettingsFetchedState (settings: result) в SettingsUpdatedState ()
  7. Вы излучаете SettingsUpdateButtonPressedEvent ()
  8. Blo c не выдает SettingsUpdatedState (), он игнорируется, поскольку сравнение равенств возвращает true)
  9. BlocListener ничего не делает, так как нет изменений состояния.

Как это исправить? Я не достаточно уверен, чтобы дать предложение на основе моих текущих знаний, поэтому, возможно, попробуйте то, что говорит цитата You should not use Equatable if you want the same state back-to-back to trigger multiple transitions.

РЕДАКТИРОВАТЬ:

LoginBlo c работает просто потому что это дает различное состояние для каждого события. Я думаю, что вы не заметили, но он выдает LoginLoadingState () перед выходом либо LoginLoggedInState (результат) или LoginFailureState (ошибка: "loginUsernamePasswordError")

  1. Текущее состояние - LoginInitialState ()
  2. Событие Emit
  3. Выход LoginLoadingState ()
  4. Состояние изменяется с LoginInitialState () на LoginLoadingState ()
  5. Выход либо LoginLoggedInState (), либо LoginFailurestate ()
  6. State изменяется с LoginLoadingState () на LoginLoggedInState () или LoginFailurestate ()
  7. Вернуться к шагу 2 для каждого события
...