Flutter Validation with Provider

Andy Julow

April 2, 2020

Validation is an important part of many applications. While BLoC and streams are often used for validation in this article we take a look at a way to accomplish the same task using only the ChangeNotifierProvider portion of the Provider package.

Start by getting the latest version of Provider from pub.dev and pasting it in pubspec.yaml

/* pubspec.yaml */

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
provider: ^4.0.4
}

directory We'll create a custom validation model to store the value and any errors coming from our view. Add an src folder to the lib directory, then add a validation folder inside of src. Inside the validation folder create validation_item.dart and enter the code below.

/* src/validation/validation_item.dart */

class ValidationItem {
final String value;
final String error;

ValidationItem(this.value, this.error);
}

Now it is time for the backend logic. Inside the validation folder create signup_validation.dart This class contains getters, setters, and validation for each field intended for use on the form. We use our custom validation item to store both a value and error for each field. ChangeNotifier is used to signal changes that can be listened to later by a ChangeNotifierProvider. An isValid field is used to check the validity of the form and a submitData function serves as a placeholder for submitting values to a database.

/* src/validation/signup_validation.dart */

import 'package:flutter/material.dart';
import './validation_item.dart';


class SignupValidation with ChangeNotifier {

ValidationItem _firstName = ValidationItem(null,null);
ValidationItem _lastName = ValidationItem(null,null);
ValidationItem _dob = ValidationItem(null,null);

//Getters
ValidationItem get firstName => _firstName;
ValidationItem get lastName => _lastName;
ValidationItem get dob => _dob;
bool get isValid {
if (_lastName.value != null && _firstName.value != null && dob.value != null){
  return true;
} else {
  return false;
}
}

//Setters
void changeFirstName(String value){
if (value.length >= 3){
  _firstName=ValidationItem(value,null);
} else {
  _firstName=ValidationItem(null, "Must be at least 3 characters");
}
notifyListeners();
}

void changeLastName(String value){
if (value.length >= 3){
  _lastName=ValidationItem(value,null);
} else {
  _lastName=ValidationItem(null, "Must be at least 3 characters");
}
notifyListeners();
}

void changeDOB(String value){
try {
  DateTime.parse(value);
  _dob=ValidationItem(value,null);
} catch(error){
  _dob =ValidationItem(null, "Invalid Format");
}
notifyListeners();
}

void submitData(){
print("FirstName: ${firstName.value}, LastName: ${lastName.value}, 
${DateTime.parse(dob.value)}");
}


}

With the backend complete, it's time to add a view, but first go into main.dart, clear out the demo application and replace with this code. The MaterialApp widget gets wrapped in a ChangeNotifierProvider so that the backend will be able to communicate with the widget tree.

/* main.dart */

import 'package:flutter/material.dart';
import 'package:validation_provider/src/screens/signup.dart';
import 'package:provider/provider.dart';
import 'package:validation_provider/src/validation/signup_validation.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
  create: (context) => SignupValidation(),
      child: MaterialApp(
    title: 'Flutter Demo',
    theme: ThemeData(
      primarySwatch: Colors.blue,
    ),
    home: Signup(),
  ),
);
}
}
    

Lastly we can add the form. Create a screens folder inside the src directory, then add a file named login.dart. Inside the build method we bring in our instance of the SignupValidation class with Provider, then insert a listview with three fields and a submit button. Each field is wired to the error property and the setter built in our SignupValidation class, and the submit button is attached to the isValid field. When all fields are valid, the button becomes enabled and the form can be submitted.

import 'package:provider/provider.dart';
import '../validation/signup_validation.dart';


class Signup extends StatelessWidget {
@override
Widget build(BuildContext context) {
final validationService = Provider.of(context);
return Scaffold(
  appBar: AppBar(
    title: Text('Signup'),
  ),
  body: Padding(
    padding: const EdgeInsets.all(8.0),
    child: ListView(
      children: [
        TextField(
          decoration: InputDecoration(
            labelText: "First Name",
            errorText: validationService.firstName.error,
          ),
          onChanged: (String value) {
            validationService.changeFirstName(value);
          },
        ),
        TextField(
          decoration: InputDecoration(
            labelText: "Last Name",
            errorText: validationService.lastName.error,
          ),
          onChanged: (String value) {
            validationService.changeLastName(value);
          },
        ),
        TextField(
          decoration: InputDecoration(
            labelText: "DOB",
            errorText: validationService.dob.error,
            hintText: "yyyy-mm-dd"
          ),
          onChanged: (String value) {
            validationService.changeDOB(value);
          },
        ),
        RaisedButton( 
          child: Text('Submit'),
          onPressed:  (!validationService.isValid) ? null : validationService.submitData,
        )
      ],
    ),
  ),
);
}
}

        
Application gif

At this point you should have a working form with a complete separation of view and business logic. Additional fields can be added to the SignupValidation class as needed and additional classes can be added to the validation folder for other validation needs in your application.