Closed marcmacias96 closed 1 year ago
here is a code
CredentialsFormBuilder(
// model: state.form,
builder: (
BuildContext context,
CredentialsForm formModel,
Widget? child,
) {
return Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ReactiveTextField<String>(
formControl: formModel.emailControl,
keyboardType: TextInputType.emailAddress,
style: Theme.of(context).textTheme.bodyLarge,
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.03,
),
ReactiveTextField<String>(
formControl: formModel.passwordControl,
keyboardType: TextInputType.text,
style: Theme.of(context).textTheme.bodyLarge,
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.033,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ReactiveSwitch.adaptive(
formControl: formModel.rememberMeControl,
),
Text(
'LoginStrings.recuerdame',
style: Theme.of(context)
.textTheme
.labelLarge!
.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
TextButton(
onPressed: () {},
child: Text(
'LoginStrings.recuperarSesion',
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
// Iniciar sesión button
SizedBox(
height: MediaQuery.of(context).size.height * 0.066,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ReactiveCredentialsFormConsumer(
builder: (context, formModel, child) {
return FilledButton(
style: ButtonStyle(
backgroundColor: MaterialStateColor.resolveWith(
(states) =>
Theme.of(context).colorScheme.tertiaryContainer,
),
),
// onPressed: (formModel.form.valid ||
// state.status is ContentState)
// ? () {
// context.read<LoginCubit>().startSession();
// }
// : () {
// formModel.form.markAllAsTouched();
// },
onPressed: () {
formModel.form.markAllAsTouched();
print(formModel.model);
},
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.66,
height: 40,
child: Center(
child: Text('asdfasdf'),
),
),
);
}),
],
),
],
),
);
},
)
@vasilich6107 It is correct what you show me, but what I don't understand is why it only mutates the formModel instance inside the constructor and why it doesn't mutate the instance I have in my Cubit, it is supposed to be connected with my Credentials model, as it happens when I declare my fb.group without using reactive_forms_generator.
cubit-state | builder |
---|---|
It should not mutate your cubit. It is designed to work as immutable
You have form.
You init the form
You enter some values
At the end you are getting result fromModel.model
You have to mutate your cubit by yourself
@vasilich6107 But doesn't this kind of break one of the goals of reactive_forms ? which is to keep my form connected to my state manager? Or can you explain to me the context of why this changes, it doesn't seem logical to me.
// Using ReactiveForm in a StatelessWidget and resolve the FormGroup from a provider
class SignInForm extends StatelessWidget {
@override
Widget build(BuildContext context) {
final viewModel = Provider.of<SignInViewModel>(context, listen: false);
return ReactiveForm(
formGroup: viewModel.form,
child: ReactiveTextField(
formControlName: 'email',
),
);
}
}
That being the case I would rather stick with pure reactive_forms and just do a Credentials.fromJson()
and solve what this package does, but in my Cubit in one line, I hope you get my point.
I didn't hear about this goal of RF Anyway - magical object mutation considered as antipattern Could you clarify why do you need this kind of featyure?
and solve what this package does
This is not the goal of the package. This package provides typed entities for the forms.
If you are comfortable working with untyped objects - you do not need this package
RF will also create controls from you Map<String, dynamic> and will maintain it's own structure I'm not sure that you'll be able to get the result you are trying to reach
which is to keep my form connected to my state manager?
RF is a state manager of it's own state. It is not meant to be connected to third party state management
I explain the context
I tried reactive_forms
without reactive_forms_builder
and I was thrilled, but once I made my solution I realized its shortcomings and is that in the end the model I get is a Map<String,dynamic>
so the generation of typed seemed to me spectacular, I proceeded to migrate all the code to reactive_forms_builder
and happened what I showed you, I stopped mutating the instance that I had in the state of the cubit, and what you tell me is that now the class that generates my FormGrup is immutable and this changes as I had structured my code.
@freezed
class LoginState with _$LoginState {
const factory LoginState({
FlowState? status,
String? errorMessage,
required FormGroup form,
}) = _LoginState;
factory LoginState.initial() => LoginState(
form: fb.group({
'email': [ Validators.required, Validators.email],
'password': [Validators.required, Validators.minLength(6)],
'remember': true,
}));
}
Future<void> startSession() async {
emit(state.copyWith(status: LoadingState()));
var response = await _repository.signInWithEmailAndPassword(
email: state.form.value['email'] as String,
password: state.form.value['password'] as String,
);
........
BlocProvider(
create: (context) => getIt<LoginCubit>(),
child: BlocConsumer<LoginCubit, LoginState>(
listener: (_, state) {
if (state.status is ContentState) {
Future.delayed(const Duration(milliseconds: 200), () {
GoRouter.of(context).replace(menu);
});
} else if (state.status is ErrorState) {
BotToast.showText(
text: state.status?.getMessage() ?? "Ocurrió un error inesperado",
contentColor: Colors.red,
textStyle: const TextStyle(color: Colors.white, fontSize: 18),
);
}
},
builder: (_, state) => ReactiveForm(
formGroup: state.form,
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ReactiveTextField(
formControlName: 'email',
keyboardType: TextInputType.emailAddress,
style: Theme.of(context).textTheme.bodyLarge,
decoration: decoration(context),
validationMessages: {
'required': (_) => LoginStrings.requiredField,
'email': (_) => LoginStrings.invalidEmail,
},
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.03,
),
ReactiveTextField(
formControlName: 'password',
keyboardType: TextInputType.text,
obscureText: _passwordConfVisible,
style: Theme.of(context).textTheme.bodyLarge,
decoration: passDecorator(context),
validationMessages: {
'required': (_) => LoginStrings.requiredField,
'minLength': (_) => LoginStrings.invalidPasswordLength,
},
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.033,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ReactiveSwitch(
formControlName: 'remember',
activeColor: Theme.of(context).colorScheme.primary,
),
Text(
LoginStrings.recuerdame,
style: Theme.of(context)
.textTheme
.labelLarge!
.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
],
),
TextButton(
onPressed: () {
GoRouter.of(context).push(forgotPasswordRoute);
},
child: Text(
LoginStrings.recuperarSesion,
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
// Iniciar sesión button
SizedBox(
height: MediaQuery.of(context).size.height * 0.066,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ReactiveFormConsumer(builder: (context, form, child) {
return FilledButton(
style: ButtonStyle(
backgroundColor: MaterialStateColor.resolveWith(
(states) => Theme.of(context)
.colorScheme
.tertiaryContainer)),
onPressed: form.valid && state.status is ContentState
? () {
context.read<LoginCubit>().startSession();
}
: () => form.markAllAsTouched(),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.66,
height: 40,
child: Center(
child: Text(
state.status is LoadingState
? LoginStrings.loading
: LoginStrings.iniciarSesion,
style: Theme.of(context).textTheme.labelLarge,
),
),
),
);
}),
],
),
],
),
),
),
),
);
Once I migrated to reactive_forms_generator
my login method stopped working because my form model stopped mutating.
What I don't understand is why, now I have to send my mutated object to the block when in my block is the original instance and it is supposed to be connected to it as it happens in reactive_forms
.
I understand that the easy solution is to send the model as a parameter of my envet and mutate it manually, and delete the Credentials instance of my state and just create it in the Widget and that solves everything but I don't know, it's quite strange, it makes me feel that the bilding of properties was lost and became a typed state manager.
RF is a state manager of it's own state. It is not meant to be connected to third party state management
Reactive Forms + Provider plugin 💪 # Although Reactive Forms can be used with any state management library or even without any one at all, Reactive Forms gets its maximum potential when is used in combination with a state management library like the Provider plugin.
This way you can separate UI logic from business logic and you can define the FormGroup inside a business logic class and then exposes that class to widgets with mechanism like the one Provider plugin brings.
I understand what you are doing. The overall approach looks like antipattern.
your state should not depend on form.
You have *Consumer
widget which is updated on each form change.
You can mutate your cubit state in Consumer widget
I understand that the easy solution is to send the model as a parameter of my envet and mutate it manually, and delete the Credentials instance of my state and just create it in the Widget and that solves everything but I don't know, it's quite strange, it makes me feel that the bilding of properties was lost and became a typed state manager.
So the right thing to do is that?
I do not know your full flow. Form has it's own state so no need for +1 state management.
When your form is valid you can get model
and submit it.
Let's see what we have - RF - manages state and Riverpod manages the state. 2 state manages for one form)
Reactive Forms can be used with any state management library or even without any one at all
or even without any one at all is a key to success)
I understand the point, thank you for explaining, I close it.
Hi @marcmacias96! Your issue has been closed. If we were helpful don't forget to star the repo.
Please check our reactive_forms widget section https://github.com/artflutter/reactive_forms_widgets
We would appreciate sponsorship subscription or one time donation https://github.com/sponsors/artflutter
reactive_forms now has two seperate widgets: https://pub.dev/packages/reactive_forms#reactiveform-vs-reactiveformbuilder-which-one
@vasilich6107 With the generator we are forced to use the stateful widget which keeps the formgroup as state. Is there any way we can get two-way binding & type-safety with our own state management (bloc / provider) ?
I don't consider this anti-pattern especially in a clean architecture where the bloc handles all the presentation logic. In complex forms where the validators often change or there are conditional values (one field value, changes another field value), it would be amazing to handle them through the bloc for example instead of polluting the widgets.
Reactive Forms can be used with any state management library or even without any one at all
or even without any one at all is a key to success)
The new documentation now prefers using a provider / bloc.
Although Reactive Forms can be used with any state management library or even without any one at all, Reactive Forms gets its maximum potential when is used in combination with a state management library like the Provider plugin.
I've been doing this from my business logic class (Riverpod standard Provider, not a Notifier):
@override
void initState(BuildContext context, SignInForm formModel) {
if (this.formModel == null) {
// formModel.form.valueChanges.listen((_) {
this.formModel = formModel;
// });
}
}
I don't need the valueChanges.listen
line of code, that's why I commented it out, but depending on your state management package and situation, you may. formModel
in initState
will always have the latest updates as it is a reference to a location in memory, not a value type. This code is called from the widget FormBuilder.initState.
Eg:
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.watch(signInVmProvider);
return SignInFormBuilder(
initState: vm.initState,
builder: (context, formModel, child) {
return YourFormControlsHere....
Screenshot
Model
State
Widget
Note
Don't work without frezzed too