Skip to main content

Form Validation

How to write a Form Validation Flutter application with UtopiaHooks.

To visit Form Validation repository press HERE.

Project Structure​

|- form_validation.dart
|- validation
| |- form_validation_page.dart - Coordinator between state & view layers
| |- state
| | |- form_validation_page_state.dart - Layer that definies State and Hook responsible for business-logic
| |- view
| | |- form_validation_page_view.dart
|- util
| |- app_validators.dart
| |- app_regex_patterns.dart

For more info about our recommended directory structure visit our Guide.

FormValidationPageState πŸ”—β€‹

form_validation_page_state.dart consists of two segments: FormValidationPageState object and useFormValidationPageState Hook responsible for state management.

FormValidationPageState contains everything necessary for View, including variables, functions and getters.

useFormValidationPageState serves as a wrapper for all the necessary hooks for FormValidationPage's business-logic. In this use-case it's consisted of two hooks:

HooksDescription
useFieldStateHook responsible for TextField state management
useSubmitStateHook responsible for handling async queries

Whole validation is wrapped in a SubmitState. In this case it simulates synchronous validation combined with an asynchronous API query. shouldSubmit is made of two segments. First checking whether SubmitState is already in progress and second validating FieldStates.

Field validation is created through FieldState's built-in error handling mechanism. It implements a validate function, which accepts nullable Validator object.

If the ValidatorResult returns null a validate function will return true, meaning that there are no problems. Otherwise, the ValidationResult will be assigned to the FieldState errorMessage and a validate function will return false, meaning that a problem has occurred.

ValidatorResult is a builder of an error message, that can be used in the View layer of the application. Underneath it is a simple String Function(BuildContext) function.


import 'package:utopia_hooks/utopia_hooks_flutter.dart';
import 'package:utopia_hooks_example/form_validation/util/app_validators.dart';
import 'package:utopia_validation/utopia_validation.dart';

class FormValidationPageState {
final FieldState emailState, passwordState, repeatPasswordState;
final bool isInProgress;
final void Function() onSubmitPressed;

const FormValidationPageState({
required this.emailState,
required this.passwordState,
required this.repeatPasswordState,
required this.isInProgress,
required this.onSubmitPressed,
});
}

FormValidationPageState useFormValidationPageState() {
//declaration of FieldStates
final emailState = useFieldState();
final passwordState = useFieldState();
final repeatPasswordState = useFieldState();

//declaration of SubmitState
final submitState = useSubmitState();

//extracted FieldStates validation
bool validate() {
return [
emailState.validate(AppValidators.emailValidator),
passwordState.validate(AppValidators.passwordValidator),
repeatPasswordState.validate(AppValidators.repeatPasswordValidator(passwordState.value)),
].every((e) => e);
}

// mock async query for demonstration purposes
Future<void> mockQuery() async => Future.delayed(const Duration(seconds: 1));

// submit function implementing SubmitState's run function
Future<void> submit() async {
await submitState.runSimple<void, Never>(
shouldSubmit: () => !submitState.inProgress && validate(),
submit: mockQuery,
);
}

return FormValidationPageState(
emailState: emailState,
passwordState: passwordState,
repeatPasswordState: repeatPasswordState,
isInProgress: submitState.inProgress,
onSubmitPressed: submit,
);
}

AppValidators πŸ”—β€‹

AppValidators file is a helper extracting all static validations that are used in the FormValidatorPageState. To get more info consider checking utopia_validation package.

import 'package:utopia_hooks_example/form_validation/util/app_regex_patterns.dart';
import 'package:utopia_validation/utopia_validation.dart';

class AppValidators {
AppValidators._();

static Validator<String> passwordValidator = Validators.combine<String>([
notEmpty,
Validators.conditional<String>(
(value) => value.length < 8 || !RegexPattern.passwordExp.hasMatch(value),
onFalse: (context) =>
"Password needs to be 8 characters long and consist of small letters, big letters, a number and a special character",
),
]);

static Validator<String> repeatPasswordValidator(String password) {
return Validators.combine([
notEmpty,
Validators.conditional(
(value) => value != password,
onFalse: (context) => "Passwords do not match",
),
]);
}

static Validator<String> emailValidator = Validators.combine([
notEmpty,
Validators.conditional(
(value) => !RegexPattern.emailExp.hasMatch(value),
onFalse: (context) => "Incorrect email structure",
),
]);

static final notEmpty = Validators.notEmpty(onEmpty: (context) => "Field cannot be empty");
}

FormValidationPage - Coordinator πŸ”—β€‹

We start with a simple wrapper that serves as a bridge between our state and view. In this simple case it's only responsible for initializing FormValidationPageState.

In more complex structures, it also provides Flutter-related functions, such as Navigator.of(context).push to useFormValidationPageState Hook.

class FormValidationPage extends StatelessWidget {
const FormValidationPage();


Widget build(BuildContext context) {
return const HookCoordinator(
use: useFormValidationPageState,
builder: FormValidationPageView.new,
);
}
}

FormValidationPageView πŸ”—β€‹

FormValidationPageView is responsible for displaying TextFields and a SubmitButton It uses the FormValidationPageState passed by FormValidationPage.

import 'package:flutter/material.dart';
import 'package:utopia_hooks/utopia_hooks_flutter.dart';
import 'package:utopia_hooks_example/form_validation/validation/state/form_validation_page_state.dart';

class FormValidationPageView extends StatelessWidget {
final FormValidationPageState state;

const FormValidationPageView(this.state);


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Flutter demo form validation page"),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildTextField(state.emailState, "E-mail"),
_buildTextField(state.passwordState, "Password"),
_buildTextField(state.repeatPasswordState, "Repeat password"),
const Spacer(),
_buildButton(),
],
),
);
}

// handle submit and loading state
Widget _buildButton() {
return ElevatedButton(
onPressed: state.onSubmitPressed,
child: state.isInProgress ? const CircularProgressIndicator() : const Text("Validate"),
);
}

Widget _buildTextField(FieldState state, String label) {
return Builder(
builder: (context) {
final errorMessage = state.errorMessage;
// StatelessTextEditingControllerWrapper is a necessary Widget
// for FieldState's TextEditingController to work properly
return StatelessTextEditingControllerWrapper(
text: state,
builder: (controller) => TextField(
controller: controller,
decoration: InputDecoration(
label: Text(label),
// display error message if not null
error: errorMessage != null ? Text(errorMessage(context)) : null,
),
),
);
},
);
}
}

AppRegexPatterns​

AppRegexPatterns file contains necessary regex patterns for the validation. In this case it consists of password and e-mail patterns.

class RegexPattern {
static final RegExp emailExp = RegExp(
r"^([a-zA-Z0-9_\-])([a-zA-Z0-9_\-.]*)@(\[((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}|((([a-zA-Z0-9\-]+)\.)+))([a-zA-Z]{2,}|(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])])$",
);
static final RegExp passwordExp = RegExp(r"(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*\W)");
}

Final words​

Keep in mind that this is only our proposed combination of a state implementing both error handling and TextField state management. If this does not look clear, do not hesitate to play around and separate error handling with a simple useState<String> Hook.



Crafted specially for you by UtopiaSoftware πŸ‘½