4

I am really struggling to convert my MVVM pattern to a bloc pattern with flutter_bloc for a very simple page. My most immediate concern is where do I place my functions that perform side effects?

I have this BlocListener to perform a side effect when a state is emitted from a bloc, and to do that side effect it calls a ViewModel:

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

Is it fine to just inject a ViewModel into the view, that contains functions which perform side effects so that I don't have to perform side effects directly in the view, and therefore I can separate the business logic from the view? Can I also add mutable state to that ViewModel that simply does not make sense to be immutable, and therefore does not make sense to be on the "bloc" states that are emitted from the bloc?

I have to say I do want to use bloc for a lot of things but I just don't want to use it for my form validation, it is just a personal preference because I am enjoying the built-in form validation of reactive_forms.

My ViewModel contains my side-effects and mutable form state like so:

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

Is it valid to use bloc for certain things that are mostly immutable state changes related, and inject my ViewModel into the same view to take care of the form validation and to hold my side-effect performing functions?

For fullness, I will add my bloc related code so you can critique if I am getting this "bloc" thing. Bear in mind this page has only one text field and a button, and I'm not using bloc for my forms, so other pages will gain much more benefit from bloc than this one:

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

bloc state:

part of 'logged_out_nickname_cubit.dart';

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

And my view:

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);
  }
}
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287

1 Answers1

3

The ViewModel is not really necessary because helper functions and anything else (non-bloc pattern stuff) can go on the bloc. As such, mutable data such as the form can go on the bloc. Which can also be passed into the immutable states (the form is passed to the bloc state in my case).

Thinking about where the fields from the ViewModel should go, it seems kinda like the fields that get used in the BlocBuilder after a state is emitted, should belong to that state. The other stuff can go anywhere you see fit, so I have added it to the cubit as not really part of the pattern, but just helper stuff for my view to consume.

So here is my conversion from MVVM to bloc for this page, currently. I will update as my understanding of the bloc pattern grows:

state:

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

cubit:

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

The View:

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);
  }
}
BeniaminoBaggins
  • 11,202
  • 41
  • 152
  • 287