{"id":429639,"date":"2024-08-21T09:00:14","date_gmt":"2024-08-21T09:00:14","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=429639"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=429639","title":{"rendered":"<span>\u0412\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0444\u043e\u0440\u043c \u0432\u043e Flutter \u043a\u0430\u043a PRO. \u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043b\u0430\u0441\u0441 \u043f\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044e form_model \u0438 BLoC<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/506\/986\/a5f\/506986a5f275b803ba91aa88b422a34e.png\" width=\"2419\" height=\"1389\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/506\/986\/a5f\/506986a5f275b803ba91aa88b422a34e.png\"\/><\/figure>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u043a\u043e\u043b\u043b\u0435\u0433\u0438! <\/p>\n<p>\u0425\u043e\u0447\u0443 \u043f\u043e\u0434\u0435\u043b\u0438\u0442\u044c\u0441\u044f \u0441\u0432\u043e\u0438\u043c \u043e\u043f\u044b\u0442\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 \u0432\u043e Flutter. \u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0430\u0441 \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043b\u0441\u044f  \u0441 \u0437\u0430\u0434\u0430\u0447\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0441\u043b\u043e\u0436\u043d\u044b\u0445 \u0444\u043e\u0440\u043c \u0438 \u0445\u043e\u0447\u0443 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u0430\u043a\u0435\u0442\u0430 <a href=\"https:\/\/pub.dev\/packages\/form_model\" rel=\"noopener noreferrer nofollow\">form_model<\/a>.<\/p>\n<p><strong>\u041f\u043e\u0447\u0435\u043c\u0443 form_model?<\/strong><\/p>\n<ol>\n<li>\n<p>\u041e\u043d \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u043e\u0442 UI, \u0447\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u043a\u043e\u0434\u0430.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0433\u0438\u0431\u043a\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0445 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 BLoC (\u0445\u043e\u0442\u044f, \u0434\u0443\u043c\u0430\u044e, \u0438 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u0430\u043c\u0438 \u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u0442\u043e\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c).<\/p>\n<\/li>\n<li>\n<p>\u0421\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u043e \u0441\u043b\u043e\u0436\u043d\u044b\u043c\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u043c\u0438 \u0444\u043e\u0440\u043c \u0431\u0435\u0437 \u043e\u0441\u043e\u0431\u044b\u0445 \u0443\u0441\u0438\u043b\u0438\u0439.<\/p>\n<\/li>\n<\/ol>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 pubspec.yaml<\/p>\n<ul>\n<li>\n<p>form_model<\/p>\n<\/li>\n<li>\n<p>flutter_bloc<\/p>\n<\/li>\n<li>\n<p>freezed<\/p>\n<\/li>\n<\/ul>\n<p><strong>\u041a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b<\/strong><\/p>\n<p>\u0414\u043b\u044f \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0441\u043b\u043e\u0436\u043d\u044b\u043c\u0438 \u0442\u0438\u043f\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u043a\u043b\u0430\u0441\u0441 Address:<\/p>\n<pre><code class=\"dart\">@freezed class Address with _$Address {   const factory Address({     required String street,     required String city,     required String country,   }) = _Address; }<\/code><\/pre>\n<p><strong>State Management<\/strong><\/p>\n<p>\u0414\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u044f \u043e\u0431\u044b\u0447\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u043f\u043e\u0434\u0445\u043e\u0434 \u0441 \u0435\u0434\u0438\u043d\u044b\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c (single-state approach). \u0412\u043e\u0442 \u043a\u0430\u043a \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043c\u043e\u0439 \u043a\u043b\u0430\u0441\u0441 StateStatus:<\/p>\n<pre><code class=\"dart\">@freezed class StateStatus with _$StateStatus {   const factory StateStatus() = PureStatus;   const factory StateStatus.loading() = LoadingStatus;   const factory StateStatus.success([dynamic data]) = SuccessStatus;   const factory StateStatus.error([String? message]) = ErrorStatus; }<\/code><\/pre>\n<p>\u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0444\u043e\u0440\u043c\u044b: \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0435, \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430, \u0443\u0441\u043f\u0435\u0445 \u0438 \u043e\u0448\u0438\u0431\u043a\u0430.<\/p>\n<hr\/>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u0435\u043c \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442\u044c \u043a \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u043c\u0443, \u043a \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0430\u043c\u043e\u0433\u043e \u0431\u043b\u043e\u043a\u0430<\/p>\n<p><strong>SignUpState<\/strong><\/p>\n<pre><code class=\"dart\">@freezed class SignUpState with _$SignUpState {   const factory SignUpState({     @Default(StateStatus()) StateStatus status,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       EmailValidator(),     ]))     FormModel email,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       PasswordLengthValidator(minLength: 8),       PasswordLowercaseValidator(),       PasswordUppercaseValidator(),       PasswordSpecialCharValidator(),     ]))     FormModel password,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       StringConfirmPasswordMatchValidator(),     ]))     FormModel confirmPassword,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       StringMinLengthValidator(minLength: 6),       CustomValidator(validator: _validateUsername),     ]))     FormModel&lt;String&gt; username,     @Default(FormModel&lt;Address&gt;(validators: [       RequiredValidator(),       CustomValidator(validator: _validateStreet),       CustomValidator(validator: _validateCity),       CustomValidator(validator: _validateCountry),     ]))     FormModel address,     @Default(FormModel&lt;bool&gt;(validators: [       BoolAgreeToTermsAndConditionsValidator(),     ]))     FormModel&lt;bool&gt; agreeToTerms,   }) = _SignUpState; }  String? _validateUsername(String? value) {   if (value == null) return null;   if (!value.startsWith('@')) {     return 'Username should start with @';   }   return null; }  String? _validateStreet(Address? value) {   if (value == null) return null;   if (value.street.isEmpty) {     return 'Street is required';   }   return null; }  String? _validateCity(Address? value) {   if (value == null) return null;   if (value.city.isEmpty) {     return 'City is required';   }   return null; }  String? _validateCountry(Address? value) {   if (value == null) return null;   if (value.country.isEmpty) {     return 'Country is required';   }   return null; } <\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c FormModel \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0444\u043e\u0440\u043c\u044b. \u0418\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442: form_model \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u044b, \u0447\u0442\u043e \u0434\u0430\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438, \u043e\u0441\u0442\u0430\u0432\u0430\u044f\u0441\u044c \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430 \u0435\u0434\u0438\u043d\u043e\u0439 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<p>\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0434\u043b\u044f \u043f\u0430\u0440\u043e\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u0432.<\/p>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043e\u0434\u043d\u0443 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443, \u0447\u0442\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u0434\u0430.<\/p>\n<p><strong>SignUpEvent<\/strong><\/p>\n<pre><code class=\"dart\">@freezed class SignUpEvent with _$SignUpEvent {   const factory SignUpEvent.emailChanged(String value) = _EmailChanged;   const factory SignUpEvent.passwordChanged(String value) = _PasswordChanged;   const factory SignUpEvent.confirmPasswordChanged(String value) = _ConfirmPasswordChanged;   const factory SignUpEvent.usernameChanged(String value) = _UsernameChanged;   const factory SignUpEvent.addressChanged(String value) = _AddressChanged;   const factory SignUpEvent.agreeToTermsChanged(bool value) = _AgreeToTermsChanged;   const factory SignUpEvent.submitted() = _Submitted; }<\/code><\/pre>\n<p><strong>SignUpBloc<\/strong><\/p>\n<pre><code class=\"dart\">class SignUpBloc extends Bloc&lt;SignUpEvent, SignUpState&gt; {   SignUpBloc() : super(const SignUpState()) {     on&lt;_EmailChanged&gt;(_onEmailChanged);     on&lt;_PasswordChanged&gt;(_onPasswordChanged);     on&lt;_ConfirmPasswordChanged&gt;(_onConfirmPasswordChanged);     on&lt;_UsernameChanged&gt;(_onUsernameChanged);     on&lt;_AddressChanged&gt;(_onAddressChanged);     on&lt;_AgreeToTermsChanged&gt;(_onAgreeToTermsChanged);     on&lt;_Submitted&gt;(_onSubmitted);   }    void _onEmailChanged(_EmailChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(email: state.email.setValue(event.value)));   }    void _onPasswordChanged(_PasswordChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(       password: state.password.setValue(event.value),       confirmPassword: state.confirmPassword.replaceValidator(         predicate: (validator) =&gt; validator is StringConfirmPasswordMatchValidator,         newValidator: StringConfirmPasswordMatchValidator(matchingValue: event.value),       ),     ));   }    void _onConfirmPasswordChanged(_ConfirmPasswordChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(confirmPassword: state.confirmPassword.setValue(event.value)));   }    void _onUsernameChanged(_UsernameChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(username: state.username.setValue(event.value)));   }    void _onAddressChanged(_AddressChanged event, Emitter&lt;SignUpState&gt; emit) {     final parts = event.value         .split(',')         .map(           (e) =&gt; e.trim(),         )         .toList();     final address =         Address(street: parts[0], city: parts.length &gt; 1 ? parts[1] : '', country: parts.length &gt; 2 ? parts[2] : '');     emit(state.copyWith(address: state.address.setValue(address)));   }    void _onAgreeToTermsChanged(_AgreeToTermsChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(agreeToTerms: state.agreeToTerms.setValue(event.value)));   }    void _onSubmitted(_Submitted event, Emitter&lt;SignUpState&gt; emit) async {     emit(state.copyWith(       email: state.email.validate(),       password: state.password.validate(),       confirmPassword: state.confirmPassword.validate(),       username: state.username.validate(),       address: state.address.validate(),       agreeToTerms: state.agreeToTerms.validate(),     ));      if (areAllFormModelsValid([       state.email,       state.password,       state.confirmPassword,       state.username,       state.address,       state.agreeToTerms,     ])) {       emit(state.copyWith(status: const LoadingStatus()));        \/\/do some logic here       await Future.delayed(const Duration(seconds: 2));        emit(state.copyWith(status: const SuccessStatus()));     }   } }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0435\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u043c\u043e\u043c\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 FormModel \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u043c\u043c\u0443\u0442\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043f\u0430\u0440\u043e\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0442\u0430\u043a\u0436\u0435 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440 \u043f\u043e\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0437\u0431\u0438\u0432\u0430\u0435\u043c \u0432\u0445\u043e\u0434\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443 \u043d\u0430 \u0447\u0430\u0441\u0442\u0438, \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u044f \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 Address.<\/p>\n<\/li>\n<\/ol>\n<p><strong>UI<\/strong><\/p>\n<pre><code class=\"dart\">class SignUpPage extends StatelessWidget {   const SignUpPage({super.key});    @override   Widget build(BuildContext context) {     return BlocProvider(       create: (context) =&gt; SignUpBloc(),       child: Builder(         builder: (context) {           final bloc = context.read&lt;SignUpBloc&gt;();           return BlocConsumer&lt;SignUpBloc, SignUpState&gt;(             listener: (context, state) {               \/\/ do something on success status             },             builder: (BuildContext context, SignUpState state) {               return Scaffold(                 body: Padding(                   padding: const EdgeInsets.all(20),                   child: SingleChildScrollView(                     child: Column(                       children: [                         const SizedBox(height: 40),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.emailChanged(value)),                           decoration: InputDecoration(                             labelText: 'Email',                             errorText: state.email.error?.translatedMessage,                           ),                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.passwordChanged(value)),                           decoration: InputDecoration(                             labelText: 'Password',                             errorText: state.password.error?.translatedMessage,                           ),                           obscureText: true,                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.confirmPasswordChanged(value)),                           decoration: InputDecoration(                             labelText: 'Confirm Password',                             errorText: state.confirmPassword.error?.translatedMessage,                           ),                           obscureText: true,                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.usernameChanged(value)),                           decoration: InputDecoration(                             labelText: 'Username @',                             errorText: state.username.error?.translatedMessage,                           ),                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.addressChanged(value)),                           decoration: InputDecoration(                             labelText: 'Address (street, city, country)',                             errorText: state.address.error?.translatedMessage,                           ),                         ),                         const SizedBox(height: 16),                         CheckboxListTile(                           value: state.agreeToTerms.value ?? false,                           onChanged: (value) =&gt; bloc.add(                             SignUpEvent.agreeToTermsChanged(value ?? false),                           ),                         ),                         if (state.agreeToTerms.error != null)                           Text(                             state.agreeToTerms.error!.translatedMessage ?? '',                             style: TextStyle(color: Theme.of(context).colorScheme.error),                           ),                         const SizedBox(height: 32),                         ElevatedButton(                           onPressed: () =&gt; bloc.add(const SignUpEvent.submitted()),                           child: state.status is LoadingStatus                               ? const SizedBox(                                   width: 20,                                   height: 20,                                   child: CircularProgressIndicator(),                                 )                               : const Text('Submit'),                         )                       ],                     ),                   ),                 ),               );             },           );         },       ),     );   } }<\/code><\/pre>\n<p>\u0417\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u043c \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044e \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0444\u043e\u0440\u043c\u044b, \u0430 \u043d\u0435 \u043f\u0440\u0438 \u043a\u0430\u0436\u0434\u043e\u043c \u0432\u0432\u043e\u0434\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041c\u043d\u0435 \u043a\u0430\u0436\u0435\u0442\u0441\u044f, \u044d\u0442\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u043b\u0443\u0447\u0448\u0438\u0439 UX, \u043d\u043e, \u043a\u043e\u043d\u0435\u0447\u043d\u043e, \u044d\u0442\u043e \u0434\u0435\u043b\u043e \u0432\u043a\u0443\u0441\u0430.<\/p>\n<hr\/>\n<p><strong>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/strong><\/p>\n<p>\u042d\u0442\u043e\u0442 \u043f\u043e\u0434\u0445\u043e\u0434 \u043f\u043e\u0437\u0432\u043e\u043b\u0438\u0442 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0434\u043e\u0432\u043e\u043b\u044c\u043d\u043e \u0441\u043b\u043e\u0436\u043d\u0443\u044e \u0444\u043e\u0440\u043c\u0443, \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0432 \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u043a\u043e\u0434 \u0447\u0438\u0441\u0442\u044b\u043c \u0438 \u043b\u0435\u0433\u043a\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u043c. \u041f\u0440\u0438\u044f\u0442\u043d\u044b\u043c \u0431\u043e\u043d\u0443\u0441\u043e\u043c \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0430\u044f \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 form_model. \u042d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043f\u0435\u0440\u0435\u0432\u043e\u0434\u043e\u0432 \u0434\u043b\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439 \u043e\u0431 \u043e\u0448\u0438\u0431\u043a\u0430\u0445 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438.<\/p>\n<p>\u041a\u043e\u043d\u0435\u0447\u043d\u043e, \u044d\u0442\u043e \u043d\u0435 \u0435\u0434\u0438\u043d\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u043f\u043e\u0441\u043e\u0431 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 \u0432\u043e Flutter, \u043d\u043e \u043e\u043d \u0432\u0435\u0441\u044c\u043c\u0430 \u0443\u0434\u043e\u0431\u043d\u044b\u0439. \u041a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f form_model \u0438 BLoC \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0442 \u0433\u0438\u0431\u043a\u043e\u0441\u0442\u044c \u0432 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0438 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c, \u0430 \u0442\u0430\u043a\u0436\u0435 \u043e\u0431\u043b\u0435\u0433\u0447\u0438\u0442 \u0438\u043d\u0442\u0435\u0440\u043d\u0430\u0446\u0438\u043e\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e.<\/p>\n<p><a href=\"https:\/\/medium.com\/@codingfriday.dev\/flutter-form-validation-like-a-pro-23d83d368096\" rel=\"noopener noreferrer nofollow\">\u0421\u0442\u0430\u0442\u044c\u044f \u043d\u0430 \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u043e\u043c.<\/a><\/p>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/66c10fbc4c5bea9d8a893ef1\" data-style=\"\" id=\"66c10fbc4c5bea9d8a893ef1\" width=\"\"><\/div>\n<p>\u0411\u0443\u0434\u0443 \u0440\u0430\u0434 \u0443\u0441\u043b\u044b\u0448\u0430\u0442\u044c \u0432\u0430\u0448\u0435 \u043c\u043d\u0435\u043d\u0438\u0435 \u0438 \u043e\u043f\u044b\u0442 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 \u0432\u043e Flutter. \u041a\u0430\u043a\u0438\u0435 \u043f\u043e\u0434\u0445\u043e\u0434\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 \u0432\u044b? \u0421\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043b\u0438\u0441\u044c \u043b\u0438 \u0432\u044b \u0441 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430\u043c\u0438 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0439, \u0438 \u043a\u0430\u043a \u0438\u0445 \u0440\u0435\u0448\u0430\u043b\u0438?<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/837444\/\"> https:\/\/habr.com\/ru\/articles\/837444\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<figure class=\"full-width\"><\/figure>\n<p>\u041f\u0440\u0438\u0432\u0435\u0442, \u043a\u043e\u043b\u043b\u0435\u0433\u0438! <\/p>\n<p>\u0425\u043e\u0447\u0443 \u043f\u043e\u0434\u0435\u043b\u0438\u0442\u044c\u0441\u044f \u0441\u0432\u043e\u0438\u043c \u043e\u043f\u044b\u0442\u043e\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 \u0432\u043e Flutter. \u041a\u0430\u0436\u0434\u044b\u0439 \u0438\u0437 \u043d\u0430\u0441 \u0441\u0442\u0430\u043b\u043a\u0438\u0432\u0430\u043b\u0441\u044f  \u0441 \u0437\u0430\u0434\u0430\u0447\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0441\u043b\u043e\u0436\u043d\u044b\u0445 \u0444\u043e\u0440\u043c \u0438 \u0445\u043e\u0447\u0443 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u043e \u043f\u043e\u0434\u0445\u043e\u0434\u0435 \u0441 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435\u043c \u043d\u043e\u0432\u043e\u0433\u043e \u043f\u0430\u043a\u0435\u0442\u0430 <a href=\"https:\/\/pub.dev\/packages\/form_model\" rel=\"noopener noreferrer nofollow\">form_model<\/a>.<\/p>\n<p><strong>\u041f\u043e\u0447\u0435\u043c\u0443 form_model?<\/strong><\/p>\n<ol>\n<li>\n<p>\u041e\u043d \u043f\u043e\u043c\u043e\u0433\u0430\u0435\u0442 \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u044c \u043b\u043e\u0433\u0438\u043a\u0443 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u043e\u0442 UI, \u0447\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0443 \u043a\u043e\u0434\u0430.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0433\u0438\u0431\u043a\u0443\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438 \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c\u044e \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0445 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u0432.<\/p>\n<\/li>\n<li>\n<p>\u0425\u043e\u0440\u043e\u0448\u043e \u0438\u043d\u0442\u0435\u0433\u0440\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 BLoC (\u0445\u043e\u0442\u044f, \u0434\u0443\u043c\u0430\u044e, \u0438 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c\u0438 \u043f\u043e\u0434\u0445\u043e\u0434\u0430\u043c\u0438 \u043a \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044e \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u0442\u043e\u0436\u0435 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c).<\/p>\n<\/li>\n<li>\n<p>\u0421\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0441\u043e \u0441\u043b\u043e\u0436\u043d\u044b\u043c\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0430\u043c\u0438 \u0444\u043e\u0440\u043c \u0431\u0435\u0437 \u043e\u0441\u043e\u0431\u044b\u0445 \u0443\u0441\u0438\u043b\u0438\u0439.<\/p>\n<\/li>\n<\/ol>\n<p>\u0414\u043b\u044f \u043d\u0430\u0447\u0430\u043b\u0430 \u0434\u043e\u0431\u0430\u0432\u0438\u043c \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u0432 pubspec.yaml<\/p>\n<ul>\n<li>\n<p>form_model<\/p>\n<\/li>\n<li>\n<p>flutter_bloc<\/p>\n<\/li>\n<li>\n<p>freezed<\/p>\n<\/li>\n<\/ul>\n<p><strong>\u041a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b<\/strong><\/p>\n<p>\u0414\u043b\u044f \u0434\u0435\u043c\u043e\u043d\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441\u043e \u0441\u043b\u043e\u0436\u043d\u044b\u043c\u0438 \u0442\u0438\u043f\u0430\u043c\u0438 \u0434\u0430\u043d\u043d\u044b\u0445 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u043a\u043b\u0430\u0441\u0441 Address:<\/p>\n<pre><code class=\"dart\">@freezed class Address with _$Address {   const factory Address({     required String street,     required String city,     required String country,   }) = _Address; }<\/code><\/pre>\n<p><strong>State Management<\/strong><\/p>\n<p>\u0414\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c \u044f \u043e\u0431\u044b\u0447\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e \u043f\u043e\u0434\u0445\u043e\u0434 \u0441 \u0435\u0434\u0438\u043d\u044b\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435\u043c (single-state approach). \u0412\u043e\u0442 \u043a\u0430\u043a \u0432\u044b\u0433\u043b\u044f\u0434\u0438\u0442 \u043c\u043e\u0439 \u043a\u043b\u0430\u0441\u0441 StateStatus:<\/p>\n<pre><code class=\"dart\">@freezed class StateStatus with _$StateStatus {   const factory StateStatus() = PureStatus;   const factory StateStatus.loading() = LoadingStatus;   const factory StateStatus.success([dynamic data]) = SuccessStatus;   const factory StateStatus.error([String? message]) = ErrorStatus; }<\/code><\/pre>\n<p>\u042d\u0442\u043e \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0445 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0444\u043e\u0440\u043c\u044b: \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0435, \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0430, \u0443\u0441\u043f\u0435\u0445 \u0438 \u043e\u0448\u0438\u0431\u043a\u0430.<\/p>\n<hr\/>\n<p>\u0422\u0435\u043f\u0435\u0440\u044c \u043c\u043e\u0436\u0435\u043c \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0438\u0442\u044c \u043a \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u043c\u0443, \u043a \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0441\u0430\u043c\u043e\u0433\u043e \u0431\u043b\u043e\u043a\u0430<\/p>\n<p><strong>SignUpState<\/strong><\/p>\n<pre><code class=\"dart\">@freezed class SignUpState with _$SignUpState {   const factory SignUpState({     @Default(StateStatus()) StateStatus status,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       EmailValidator(),     ]))     FormModel email,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       PasswordLengthValidator(minLength: 8),       PasswordLowercaseValidator(),       PasswordUppercaseValidator(),       PasswordSpecialCharValidator(),     ]))     FormModel password,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       StringConfirmPasswordMatchValidator(),     ]))     FormModel confirmPassword,     @Default(FormModel&lt;String&gt;(validators: [       RequiredValidator(),       StringMinLengthValidator(minLength: 6),       CustomValidator(validator: _validateUsername),     ]))     FormModel&lt;String&gt; username,     @Default(FormModel&lt;Address&gt;(validators: [       RequiredValidator(),       CustomValidator(validator: _validateStreet),       CustomValidator(validator: _validateCity),       CustomValidator(validator: _validateCountry),     ]))     FormModel address,     @Default(FormModel&lt;bool&gt;(validators: [       BoolAgreeToTermsAndConditionsValidator(),     ]))     FormModel&lt;bool&gt; agreeToTerms,   }) = _SignUpState; }  String? _validateUsername(String? value) {   if (value == null) return null;   if (!value.startsWith('@')) {     return 'Username should start with @';   }   return null; }  String? _validateStreet(Address? value) {   if (value == null) return null;   if (value.street.isEmpty) {     return 'Street is required';   }   return null; }  String? _validateCity(Address? value) {   if (value == null) return null;   if (value.city.isEmpty) {     return 'City is required';   }   return null; }  String? _validateCountry(Address? value) {   if (value == null) return null;   if (value.country.isEmpty) {     return 'Country is required';   }   return null; } <\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0435\u043c FormModel \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043f\u043e\u043b\u044f \u0444\u043e\u0440\u043c\u044b. \u0418\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0439 \u043c\u043e\u043c\u0435\u043d\u0442: form_model \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043a\u043e\u043c\u0431\u0438\u043d\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u044b, \u0447\u0442\u043e \u0434\u0430\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0441\u043b\u043e\u0436\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u0438, \u043e\u0441\u0442\u0430\u0432\u0430\u044f\u0441\u044c \u043f\u0440\u0438 \u044d\u0442\u043e\u043c \u0432 \u0440\u0430\u043c\u043a\u0430\u0445 \u043f\u0440\u0438\u043d\u0446\u0438\u043f\u0430 \u0435\u0434\u0438\u043d\u043e\u0439 \u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043d\u043d\u043e\u0441\u0442\u0438.<\/p>\n<p>\u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440, \u0434\u043b\u044f \u043f\u0430\u0440\u043e\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440\u043e\u0432.<\/p>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440 \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430 \u043e\u0434\u043d\u0443 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0443, \u0447\u0442\u043e \u0443\u043f\u0440\u043e\u0449\u0430\u0435\u0442 \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u0434\u0430.<\/p>\n<p><strong>SignUpEvent<\/strong><\/p>\n<pre><code class=\"dart\">@freezed class SignUpEvent with _$SignUpEvent {   const factory SignUpEvent.emailChanged(String value) = _EmailChanged;   const factory SignUpEvent.passwordChanged(String value) = _PasswordChanged;   const factory SignUpEvent.confirmPasswordChanged(String value) = _ConfirmPasswordChanged;   const factory SignUpEvent.usernameChanged(String value) = _UsernameChanged;   const factory SignUpEvent.addressChanged(String value) = _AddressChanged;   const factory SignUpEvent.agreeToTermsChanged(bool value) = _AgreeToTermsChanged;   const factory SignUpEvent.submitted() = _Submitted; }<\/code><\/pre>\n<p><strong>SignUpBloc<\/strong><\/p>\n<pre><code class=\"dart\">class SignUpBloc extends Bloc&lt;SignUpEvent, SignUpState&gt; {   SignUpBloc() : super(const SignUpState()) {     on&lt;_EmailChanged&gt;(_onEmailChanged);     on&lt;_PasswordChanged&gt;(_onPasswordChanged);     on&lt;_ConfirmPasswordChanged&gt;(_onConfirmPasswordChanged);     on&lt;_UsernameChanged&gt;(_onUsernameChanged);     on&lt;_AddressChanged&gt;(_onAddressChanged);     on&lt;_AgreeToTermsChanged&gt;(_onAgreeToTermsChanged);     on&lt;_Submitted&gt;(_onSubmitted);   }    void _onEmailChanged(_EmailChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(email: state.email.setValue(event.value)));   }    void _onPasswordChanged(_PasswordChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(       password: state.password.setValue(event.value),       confirmPassword: state.confirmPassword.replaceValidator(         predicate: (validator) =&gt; validator is StringConfirmPasswordMatchValidator,         newValidator: StringConfirmPasswordMatchValidator(matchingValue: event.value),       ),     ));   }    void _onConfirmPasswordChanged(_ConfirmPasswordChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(confirmPassword: state.confirmPassword.setValue(event.value)));   }    void _onUsernameChanged(_UsernameChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(username: state.username.setValue(event.value)));   }    void _onAddressChanged(_AddressChanged event, Emitter&lt;SignUpState&gt; emit) {     final parts = event.value         .split(',')         .map(           (e) =&gt; e.trim(),         )         .toList();     final address =         Address(street: parts[0], city: parts.length &gt; 1 ? parts[1] : '', country: parts.length &gt; 2 ? parts[2] : '');     emit(state.copyWith(address: state.address.setValue(address)));   }    void _onAgreeToTermsChanged(_AgreeToTermsChanged event, Emitter&lt;SignUpState&gt; emit) {     emit(state.copyWith(agreeToTerms: state.agreeToTerms.setValue(event.value)));   }    void _onSubmitted(_Submitted event, Emitter&lt;SignUpState&gt; emit) async {     emit(state.copyWith(       email: state.email.validate(),       password: state.password.validate(),       confirmPassword: state.confirmPassword.validate(),       username: state.username.validate(),       address: state.address.validate(),       agreeToTerms: state.agreeToTerms.validate(),     ));      if (areAllFormModelsValid([       state.email,       state.password,       state.confirmPassword,       state.username,       state.address,       state.agreeToTerms,     ])) {       emit(state.copyWith(status: const LoadingStatus()));        \/\/do some logic here       await Future.delayed(const Duration(seconds: 2));        emit(state.copyWith(status: const SuccessStatus()));     }   } }<\/code><\/pre>\n<p>\u0417\u0434\u0435\u0441\u044c \u0435\u0441\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u043d\u0442\u0435\u0440\u0435\u0441\u043d\u044b\u0445 \u043c\u043e\u043c\u0435\u043d\u0442\u043e\u0432:<\/p>\n<ol>\n<li>\n<p>\u041a\u0430\u0436\u0434\u044b\u0439 FormModel \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u043e\u0432\u044b\u0439 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438, \u0447\u0442\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0438\u0432\u0430\u0435\u0442 \u0438\u043c\u043c\u0443\u0442\u0430\u0431\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c.<\/p>\n<\/li>\n<li>\n<p>\u041f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043f\u0430\u0440\u043e\u043b\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0442\u0430\u043a\u0436\u0435 \u0432\u0430\u043b\u0438\u0434\u0430\u0442\u043e\u0440 \u043f\u043e\u043b\u044f \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f \u043f\u0430\u0440\u043e\u043b\u044f.<\/p>\n<\/li>\n<li>\n<p>\u0414\u043b\u044f \u0430\u0434\u0440\u0435\u0441\u0430 \u0440\u0430\u0437\u0431\u0438\u0432\u0430\u0435\u043c \u0432\u0445\u043e\u0434\u043d\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443 \u043d\u0430 \u0447\u0430\u0441\u0442\u0438, \u0441\u043e\u0437\u0434\u0430\u0432\u0430\u044f \u043d\u043e\u0432\u044b\u0439 \u043e\u0431\u044a\u0435\u043a\u0442 Address.<\/p>\n<\/li>\n<\/ol>\n<p><strong>UI<\/strong><\/p>\n<pre><code class=\"dart\">class SignUpPage extends StatelessWidget {   const SignUpPage({super.key});    @override   Widget build(BuildContext context) {     return BlocProvider(       create: (context) =&gt; SignUpBloc(),       child: Builder(         builder: (context) {           final bloc = context.read&lt;SignUpBloc&gt;();           return BlocConsumer&lt;SignUpBloc, SignUpState&gt;(             listener: (context, state) {               \/\/ do something on success status             },             builder: (BuildContext context, SignUpState state) {               return Scaffold(                 body: Padding(                   padding: const EdgeInsets.all(20),                   child: SingleChildScrollView(                     child: Column(                       children: [                         const SizedBox(height: 40),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.emailChanged(value)),                           decoration: InputDecoration(                             labelText: 'Email',                             errorText: state.email.error?.translatedMessage,                           ),                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.passwordChanged(value)),                           decoration: InputDecoration(                             labelText: 'Password',                             errorText: state.password.error?.translatedMessage,                           ),                           obscureText: true,                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.confirmPasswordChanged(value)),                           decoration: InputDecoration(                             labelText: 'Confirm Password',                             errorText: state.confirmPassword.error?.translatedMessage,                           ),                           obscureText: true,                         ),                         const SizedBox(height: 16),                         TextField(                           onChanged: (value) =&gt; bloc.add(SignUpEvent.usernameChanged(value)),                           decoration: InputDecoration(                             labelText: 'Username @',                             errorText: state.username.error?.translatedMessage,                           ),                       <\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-429639","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/429639","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=429639"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/429639\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=429639"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=429639"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=429639"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}