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