在不可变状态不实用的情况下使用 Bloc 模式

Posted

技术标签:

【中文标题】在不可变状态不实用的情况下使用 Bloc 模式【英文标题】:Bloc pattern in situations where immutable state is just not practical 【发布时间】:2021-03-09 23:04:00 【问题描述】:

对于一个非常简单的页面,我真的很难将我的 MVVM 模式转换为带有 flutter_bloc 的 bloc 模式。我最直接关心的是我应该将执行副作用的函数放在哪里?

我有这个BlocListener 在从块发出状态时执行副作用,并调用 ViewModel:

  BlocListener<LoggedOutNicknameCubit, LoggedOutNicknameState>(
      listener: (context, state) 
    state.maybeWhen(
        submitted: () => loggedOutNickNameViewModel.submitPressed(context),
        orElse: () );
  )

是否可以只将 ViewModel 注入到视图中,其中包含执行副作用的函数,这样我就不必直接在视图中执行副作用,因此我可以将业务逻辑与视图分离?我是否也可以将可变状态添加到该 ViewModel 中,该状态根本不具有不可变的意义,因此处于从 bloc 发出的“bloc”状态上没有意义?

我不得不说我确实想在很多事情上使用 bloc 但我只是不想将它用于我的表单验证,这只是个人喜好,因为我喜欢内置的表单验证reactive_forms.

我的 ViewModel 包含我的副作用和可变的表单状态,如下所示:

class LoggedOutNickNameViewModel extends VpViewModel 
    LoggedOutNickNameViewModel(
  this._saveNickNameLocallyUseCase, this._getUserFieldFromLocalUseCase)
  : super(null);

  FormGroup get form => _form;
  String get nickNameKey => _nickNameKey;

  FormGroup _form;
  final ISaveNickNameLocallyUseCase _saveNickNameLocallyUseCase;
  final IGetUserFieldFromLocalUseCase _getUserFieldFromLocalUseCase;
  final String _nickNameKey = UserRegistrationFieldKeys.nickName;

  @override
  void onClose() 
    _saveNickNameLocallyUseCase
        .invoke(_form.control(_nickNameKey).value as String ?? '');
  

  void onCreate() 
    _form = FormGroup(
      UserRegistrationFieldKeys.nickName:
          FormControl<String>(validators: [Validators.required]),
    );

    _form.control(_nickNameKey).value =
        _getUserFieldFromLocalUseCase.invoke(_nickNameKey);
    _form.markAsDirty();
  

  void submitPressed(BuildContext context) 
    _saveNickNameLocallyUseCase
        .invoke(_form.control(_nickNameKey).value as String ?? '');
    Navigator.pushNamed(context, Routes.LOGGED_OUT_EMAIL);
  
    

将 bloc 用于大多数与不可变状态更改相关的某些事情,并将我的 ViewModel 注入同一视图以处理表单验证并保持我的副作用执行功能是否有效?

为了完整起见,我将添加与 bloc 相关的代码,这样您就可以批评我是否得到了这个“bloc”的东西。请记住,此页面只有一个文本字段和一个按钮,而且我的表单中没有使用bloc,因此其他页面将从bloc 中获得比这个更多的好处:

集团:

import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'logged_out_nickname_state.dart';
part 'logged_out_nickname_cubit.freezed.dart';

class LoggedOutNicknameCubit extends Cubit<LoggedOutNicknameState> 
  LoggedOutNicknameCubit() : super(const LoggedOutNicknameState.initialised());

  void submitPressed() => emit(const LoggedOutNicknameState.submitted());

集团状态:

part of 'logged_out_nickname_cubit.dart';

@freezed
abstract class LoggedOutNicknameState with _$LoggedOutNicknameState 
  const factory LoggedOutNicknameState.initialised() = _Initialised;
  const factory LoggedOutNicknameState.submitted() = _Submitted;

我的看法:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get/get.dart';
import 'package:vepo/presentation/widgets/display/buttons/elevated_buttons/submit_button_widget.dart';
import 'package:vepo/presentation/widgets/display/containers/form_container_widget.dart';
import 'package:vepo/presentation/widgets/display/text/caption_widget.dart';
import 'package:vepo/presentation/widgets/display/text/subtitle_1_widget.dart';
import 'package:vepo/presentation/widgets/forms/text_field/text_field_widget.dart';
import 'package:vepo/presentation/widgets/pages/form_page_scaffold_widget.dart';
import 'package:vepo/presentation/widgets/pages/logo_header_widget.dart';

import 'cubit/logged_out_nickname_cubit.dart';
import 'logged_out_nick_name_controller.dart';
import 'logged_out_nick_name_cubit.dart';

class LoggedOutNickNameView extends GetView<LoggedOutNickNameController> 
  @override
  Widget build(BuildContext context) 
    print('building loggedOUtNickName page');
    final loggedOutNickNameViewModel =
        BlocProvider.of<LoggedOutNickNameViewModel>(context);
    final loggedOutNicknameCubit =
        BlocProvider.of<LoggedOutNicknameCubit>(context);

return VpFormPageScaffold([
  const VpLogoHeader(),
  BlocConsumer<LoggedOutNickNameCubit, LoggedOutNickNameState>(
      builder: (context, state) 
    return state.when(
      initialised: () =>
          buildContent(loggedOutNickNameViewModel, loggedOutNicknameCubit),
      submitted: () =>
          buildContent(loggedOutNickNameViewModel, loggedOutNicknameCubit),
    );
  , listener: (context, state) 
    state.maybeWhen(
        submitted: () => loggedOutNickNameViewModel.submitPressed(context),
        orElse: () );
  ),
]);
  

  Widget buildContent(LoggedOutNickNameViewModel loggedOutNickNameViewModel,
      LoggedOutNicknameCubit loggedOutNicknameCubit) 
    return VpFormContainer([
      Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.start,
          children: const [
            VpSubtitle1('So nice to meet you! What do your friends call you?',
                TextAlign.center),
            VpCaption('You can change this any time in settings...',
                TextAlign.center),
          ]),
      VpTextField(
        loggedOutNickNameViewModel.nickNameKey,
        'Nickname...',
        validationMessages: (control) =>
            'required': 'Please enter your name / alter ego',
        textAlign: TextAlign.center,
        maxLength: 20,
      ),
      Center(
          child:
              VpSubmitButton('CONTINUE', loggedOutNicknameCubit.submitPressed))
    ], loggedOutNickNameViewModel.form);
  

【问题讨论】:

我已将 ViewModel 添加到 bloc 状态,这可能更惯用也可能不会更惯用。 【参考方案1】:

ViewModel 并不是真正需要的,因为辅助函数和其他任何东西(非 bloc 模式的东西)都可以在 bloc 上运行。因此,诸如表单之类的可变数据可以继续存在。也可以传递到不可变状态(在我的例子中,表单传递到 bloc 状态)。

考虑 ViewModel 中的字段应该去哪里,看起来有点像在发出状态后在 BlocBuilder 中使用的字段应该属于那个状态。其他的东西可以放在任何你认为合适的地方,所以我把它添加到了cubit中,作为模式的一部分,它只是我的视图使用的辅助材料。

所以这是我目前从 MVVM 转换为 bloc 的页面。随着我对 bloc 模式理解的加深,我会更新:

状态:

part of 'logged_out_nickname_cubit.dart';

@freezed
abstract class LoggedOutNickNameState with _$LoggedOutNickNameState 
  factory LoggedOutNickNameState.initialised(
      String nickNameKey, FormGroup form) = _Initialised;
  factory LoggedOutNickNameState.submitted(
      String nickNameKey, FormGroup form) = _Submitted;

肘:

part 'logged_out_nickname_state.dart';
part 'logged_out_nickname_cubit.freezed.dart';

class LoggedOutNickNameCubit extends Cubit<LoggedOutNickNameState> 
  LoggedOutNickNameCubit(this.nickNameKey, this.form,
      this.getUserFieldFromLocalUseCase, this.saveNickNameLocallyUseCase)
      : super(LoggedOutNickNameState.initialised(
            nickNameKey: nickNameKey, form: form)) 
    form.control(nickNameKey).value =
        getUserFieldFromLocalUseCase.invoke(nickNameKey);

    form.markAsDirty();
  

  final String nickNameKey;
  final FormGroup form;
  final GetUserFieldFromLocalUseCase getUserFieldFromLocalUseCase;
  final SaveNickNameLocallyUseCase saveNickNameLocallyUseCase;

  // the cubit version of "mapEventToState"
  void submitPressed() => emit(
      LoggedOutNickNameState.submitted(nickNameKey: nickNameKey, form: form));

  void initialise() => emit(
      LoggedOutNickNameState.initialised(nickNameKey: nickNameKey, form: form));

  // just random helper functions for the view to consume
  void onSubmitPressed(BuildContext context) 
    saveNickNameLocallyUseCase
        .invoke(form.control(nickNameKey).value as String ?? '');

    Navigator.pushNamed(context, Routes.LOGGED_OUT_EMAIL,
            arguments: LoggedOutEmailPageArgs(
                form.control(nickNameKey).value as String ?? ''))
        .then((value) => initialise());
  

  void onBackPressed() 
    saveNickNameLocallyUseCase
        .invoke(form.control(nickNameKey).value as String ?? '');
    close();
  

观点:

import 'logged_out_nickname_cubit.dart';

class LoggedOutNickNamePage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    print('building loggedOUtNickName page');

    final loggedOutNicknameCubit =
        BlocProvider.of<LoggedOutNickNameCubit>(context);

    return VpFormPageScaffold([
      VpLogoHeader(loggedOutNicknameCubit.onBackPressed),
      BlocConsumer<LoggedOutNickNameCubit, LoggedOutNickNameState>(
          builder: (context, state) 
        return state.when(
          initialised: (nickNameKey, form) =>
              _buildContent(nickNameKey, form, loggedOutNicknameCubit),
          submitted: (nickNameKey, form) =>
              _buildContent(nickNameKey, form, loggedOutNicknameCubit),
        );
      , listener: (context, state) 
        state.maybeWhen(
            submitted: (nickNameKey, form) =>
                loggedOutNicknameCubit.onSubmitPressed(context),
            orElse: () );
      ),
    ]);
  

  Widget _buildContent(
      String nickNameKey, FormGroup form, LoggedOutNickNameCubit cubit) 
    return VpFormContainer([
      Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.start,
          children: const [
            VpSubtitle1('So nice to meet you! What do your friends call you?',
                TextAlign.center),
            VpCaption('You can change this any time in settings...',
                TextAlign.center),
          ]),
      VpTextField(
        nickNameKey,
        'Nickname...',
        validationMessages: (control) =>
            'required': 'Please enter your name / alter ego',
        textAlign: TextAlign.center,
        maxLength: 20,
      ),
      Center(child: VpSubmitButton('CONTINUE', cubit.submitPressed))
    ], form);
  

【讨论】:

以上是关于在不可变状态不实用的情况下使用 Bloc 模式的主要内容,如果未能解决你的问题,请参考以下文章

如何在不使用任何库的情况下在颤振中使用 bloc

在不使用 iostream 的情况下保存 c++11 随机生成器的状态

在不存储信用卡信息的情况下重复计费可变金额

为啥 Python 3.8.0 允许在不使用“非本地”变量的情况下从封闭函数范围更改可变类型?

在一个简单的 RxJS 示例中,如何在不使用主题或命令式操作的情况下管理状态?

如何在不改变绘图宽度的情况下使用 ggplot2 在 R 中添加可变大小的 y 轴标签?