Hasan YATAR
10 min readOct 29, 2023

Reactive Form stands as a preferred solution in modern application development processes. However, the focus of this article is not an exhaustive exploration of Reactive Form but rather to exemplify why it serves as an excellent showcase for the Builder Pattern. Builder Pattern, one of the fundamental principles in software development, finds its strength in the context of Reactive Form, allowing for the creation of cleaner, more organized, and highly scalable code. This article aims to illustrate how Reactive Form serves as a prime example of the Builder Pattern in action and elucidates how this pairing can enhance code cleanliness and organization.

What is the Builder Pattern

The Builder Pattern holds a significant place among the foundational design patterns in software design. In essence, this design pattern is aimed at separating the construction process of complex objects from their representation, ultimately allowing developers to create objects with multiple configurations while ensuring clarity, readability, and manageability.

In scenarios where traditional constructor methods tend to be burdened with lengthy parameter lists, the Builder Pattern comes to the rescue. This design pattern dissects the object construction process into logical, sequential steps, with each step representing the assignment of a specific attribute or configuration during object creation. These discrete steps effectively reduce complexity and render the object’s construction manageable. Furthermore, this approach simplifies the prevention of errors during object creation and guarantees the object’s construction in a valid state.

The Builder Pattern proves to be of immense advantage, particularly when dealing with the construction of large and complex objects or when there is a need to create various variations of a fundamental object. For example, when creating a game character object, there might be a requirement to assign different attributes, appearance, and abilities to the character. In such cases, the Builder Pattern alleviates complexity and provides developers with a clear roadmap for constructing the object.

Applications of the Builder Pattern

  1. Dynamic Object Creation: The Builder Pattern thrives in scenarios where objects possess a multitude of optional parameters. It serves as a structured conduit for creating objects with diverse configurations, all while maintaining simplicity and comprehensibility.
  2. Complex Data Structures: When confronted with the challenge of constructing intricate data structures, such as documents or reports, the Builder Pattern simplifies the process, rendering it more intuitive and organized.
  3. Immutable Objects: For the creation of immutable objects, which inherently ensure thread-safety, the Builder Pattern emerges as the preferred choice. It solemnly ensures that all object attributes are set during construction, preserving their state throughout the object’s lifecycle.

Pros:

  1. Clarity and Readability: By explicit delineation of object creation steps, the Builder Pattern substantially augments code readability. It expedites the comprehension of code intent, particularly when juxtaposed with protracted parameter lists in constructors.
  2. Flexibility: Developers are empowered to create objects with diverse configurations through the utilization of various builders, thereby diminishing the necessity for multiple constructors or constructor overloads.
  3. Safety: The Builder Pattern guarantees that an object emerges in a valid state upon construction, effectively preventing the creation of partially initialized or inconsistent objects.

Cons:

  1. Overhead: The implementation of the Builder Pattern often necessitates the authoring of additional code, which might be perceived as a drawback, particularly when confronted with the creation of straightforward objects.
  2. Complexity: In instances where objects feature a scant number of parameters, the employment of the Builder Pattern might be construed as excessive, potentially leading to code verbosity.

Reactive Forms Without Builder Pattern

First of all, I will show you how to create a reactive form normally (without Builder pattern).

As seen in the code below, we have a class named ClassicFormMethod. We have a setForm function here. At the end of the day, we have a dispose method to remove our form object and its features. We can say that it is a very simple structure.

import 'package:reactive_forms/reactive_forms.dart';

class ClassicFormMethod {
FormGroup form = FormGroup(
{
'name': FormControl<String>(
validators: [Validators.required],
),
'surname': FormControl<String>(
validators: [Validators.required],
),
'email': FormControl<String>(
validators: [Validators.required, Validators.email]),
'password': FormControl<String>(
validators: [
Validators.required,
Validators.minLength(8),
],
),
},
);

void setForm(
String name,
String surname,
String email,
String password,
) {
form.reset(
value: {
'name': name,
'surname': surname,
'email': email,
'password': password,
},
);
}

void dispose() {
form.dispose();
}
}

As seen in the code below, The code snippet features a Flutter page named ClassicMethodFormPage. Within this page, we employ an instance of the ClassicFormMethod class to establish a form. Upon page loading, initial values are assigned to the form in the initState function.

The form encompasses various fields, each represented by a ReactiveTextField. These fields are thoughtfully configured with appearance and functionality attributes, culminating in an end-user-friendly interface. To wrap up the form, we introduce a 'Submit' button, which is enabled or disabled based on the form's validation status.

import 'package:builder_pattern/classic_method/classic_method.dart';
import 'package:flutter/material.dart';
import 'package:reactive_forms/reactive_forms.dart';

class ClassicMethodFormPage extends StatefulWidget {
const ClassicMethodFormPage({super.key});

@override
State<ClassicMethodFormPage> createState() => _FormPageState();
}

class _FormPageState extends State<ClassicMethodFormPage> {
final classicForm = ClassicFormMethod();

@override
void initState() {
super.initState();
//
classicForm.setForm('Hasan', 'Yatar', 'abc123@gmail.com', '3232132134');
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Page'),
),
body: ReactiveForm(
formGroup: classicForm.form,
child: Container(
margin: const EdgeInsets.all(16),
child: Center(
child: Column(
children: [
// name
ReactiveTextField(
formControlName: 'name',
decoration: const InputDecoration(
labelText: 'Name',
hintText: 'Enter your name',
),
),
const SizedBox(height: 16),
// surname
ReactiveTextField(
formControlName: 'surname',
decoration: const InputDecoration(
labelText: 'Surname',
hintText: 'Enter your surname',
),
),
const SizedBox(height: 16),
// email
ReactiveTextField(
formControlName: 'email',
decoration: const InputDecoration(
labelText: 'Email',
hintText: 'Enter your email',
),
),
const SizedBox(height: 16),
// password
ReactiveTextField(
formControlName: 'password',
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
hintText: 'Enter your password',
),
),
const SizedBox(height: 16),
// submit button
ReactiveFormConsumer(
builder: (context, form, child) {
return ElevatedButton(
onPressed: form.valid
? () {
classicForm.dispose();
Navigator.pop(context);
}
: null,
child: const Text('Submit'),
);
},
),
],
),
),
),
),
);
}
}

What I have explained so far was to show you how to create a form without using the builder pattern. Now we will see how we can write more effective code using the builder pattern. So let’s start :)

Reactive Forms With Builder Pattern

The foundation of this approach lies in two key components: the FormModel and the FormBuilderSource.

The FormModel class is a simple data model that encapsulates the attributes of a form, including 'name', 'surname', 'email', and 'password'. These attributes will store the user's input.

class FormModel {
final String name;
final String surname;
final String email;
final String password;

FormModel(
this.name,
this.surname,
this.email,
this.password,
);
}

The IFormBuilder interface defines a set of methods that form builders should implement. These methods include reset, setName, setSurname, setEmail, setPassword, buildForm, and getFormModel.

abstract class IFormBuilder {
IFormBuilder reset();
IFormBuilder setName(String name);
IFormBuilder setSurname(String surname);
IFormBuilder setEmail(String email);
IFormBuilder setPassword(String password);

FormGroup buildForm();

FormModel getFormModel();
}

The FormBuilderSource class implements the IFormBuilder interface, creating a structure for building and managing forms. It utilizes the Reactive Forms package to define a FormGroup, which serves as the core structure of the form. The FormGroup contains four FormControl instances, one for each form field, with corresponding validators to ensure data integrity.

class FormBuilderSource implements IFormBuilder {
FormBuilderSource({FormGroup? form}) {
this.form = form ??
FormGroup(
{
'name': FormControl<String>(
validators: [Validators.required],
),
'surname': FormControl<String>(
validators: [Validators.required],
),
'email': FormControl<String>(
validators: [Validators.required, Validators.email]),
'password': FormControl<String>(
validators: [
Validators.required,
Validators.minLength(8),
],
),
},
);
}

late FormGroup form;

...
... other codes

}

As seen in the code below, each set of methods returns IFormBuilder.
The Builder Pattern shines in this implementation. It allows for step-by-step form customization through methods like setName, setSurname, setEmail, and setPassword. These methods set the values for the form fields, providing flexibility in modifying the form's data. The reset method conveniently clears the form, ensuring a clean slate for the user.

class FormBuilderSource implements IFormBuilder {

... other codes
...

@override
IFormBuilder setEmail(String email) {
form.control('email').value = email;
return this;
}

@override
IFormBuilder setName(String name) {
form.control('name').value = name;
return this;
}

@override
IFormBuilder setPassword(String password) {
form.control('password').value = password;
return this;
}

@override
IFormBuilder setSurname(String surname) {
form.control('surname').value = surname;
return this;
}

@override
IFormBuilder reset() {
form.reset();
return this;
}

...
... other codes

}

The buildForm method finalizes the form construction and returns the complete FormGroup. Meanwhile, the getFormModel method converts the form data into a FormModel object, making it easy to work with the user's input.

class FormBuilderSource implements IFormBuilder {

... other codes
...

@override
FormGroup buildForm() => form;

@override
FormModel getFormModel() {
return FormModel(
form.control('name').value,
form.control('surname').value,
form.control('email').value,
form.control('password').value,
);
}

}

This approach is invaluable, particularly when dealing with complex forms, as it promotes code organization, maintainability, and readability. It separates form creation and management concerns, allowing developers to focus on enhancing the user experience while ensuring data consistency.

Within the code of the BuilderFormPage class, the initState method plays a significant role in utilizing the Builder Pattern for form initialization.

At the heart of this approach is the formBuilder object, which is of type IFormBuilder. This object is an instance of the FormBuilderSource class, which implements the IFormBuilder interface. The IFormBuilder interface defines a set of methods to manipulate and construct form objects.

class BuilderFormPage extends StatefulWidget {
const BuilderFormPage({super.key});

@override
State<BuilderFormPage> createState() => _FormPageState();
}

class _FormPageState extends State<BuilderFormPage> {
late IFormBuilder formBuilder;
final List<FormModel> formList = [];

...
... other codes
...

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form Page'),
),
body: ReactiveForm(
formGroup: formBuilder.buildForm(), // build this form
child: Container(
margin: const EdgeInsets.all(16),
child: Center(
...
...
...
...
}

First and foremost, the reset method is invoked to ensure a clean slate for the form. This is particularly useful when initializing a new form or resetting an existing one.

Subsequently, the Builder Pattern comes into play. The setName, setSurname, and setEmail methods are used in a chained manner to set the initial values of the form fields. This chain of method calls simplifies form initialization and allows for flexibility in specifying the order and number of properties. Each of these methods corresponds to a field in the form: 'name', 'surname', and 'email'. You can see that the 'password' field is commented out, indicating that it's optional and can be set if needed.

class _FormPageState extends State<BuilderFormPage> {
late IFormBuilder formBuilder;
final List<FormModel> formList = [];

@override
void initState() {
super.initState();
//The Builder design pattern is used as a pattern that makes building an object more flexible.
//This pattern allows you to chain-call methods (set methods) that are used to set a set of
//properties (or parameters) on an object. This gives you a structure that you can use to change
//the order or number of properties and set optional properties.
formBuilder = FormBuilderSource()
.reset() // at first reset the form
.setName('Hasan') // then set the name (default value)
.setSurname('Yatar') // then set the surname (default value)
.setEmail('abc12345@gmail.com'); // then set the email (default value)
//.setPassword(password) // if you want to set the password, or you can skip it
}


...
... other codes

}

This Builder Pattern approach makes form initialization more structured and readable. It allows for concise and sequential configuration of form properties, making it ideal for initializing complex forms with multiple fields and options.

Using Director for Builder Pattern

The Director, when applied alongside the Builder Pattern, plays a crucial role in selecting the appropriate Builder and orchestrating the creation process of an object. The Director serves as the guide, directing Builders to craft the object according to the desired structure.

How to Utilize Builder and Director? In this article, we will walk through an example using Builder and Director Patterns to illustrate the step-by-step construction of a complex object. You’ll also gain insights into how these patterns not only streamline your code but also reduce redundancy, ultimately resulting in cleaner and more maintainable code.

Understanding the Components:

  • FormBuilderSource: We start with the implementation of the FormBuilderSource class, which serves as our concrete builder. This class defines a variety of methods for setting different properties of a form. It's worth noting that the FormBuilderSource adheres to the IFormBuilder interface, ensuring consistency in building forms.
  • FormDirector: The FormDirector class acts as our director, orchestrating the construction process. It takes an instance of the builder (FormBuilderSource) as a parameter. The FormDirector offers a range of methods, each dedicated to building a specific form for an individual. By calling these methods, the Director instructs the builder on how to construct the form step by step.
import 'package:builder_pattern/builder_pattern/form_builder.dart';
import 'package:reactive_forms/reactive_forms.dart';

class FormDirector {
IFormBuilder builder;

FormDirector(this.builder);

FormGroup get constructForJohn => builder
.reset()
.setName('John')
.setSurname('Doe')
.setEmail('john.doe@gmail.com')
.setPassword('12345678')
.buildForm();

FormGroup get constructForJane => builder
.reset()
.setName('Jane')
.setSurname('Smith')
.setEmail('jane.smith@gmail.com')
.buildForm();

/// ... according to your needs You can create more methods like this

/// For example: You want to create default form for Hasan Yatar which has name, surname

FormGroup get constructForHasan =>builder
.reset()
.setName('Hasan')
.setSurname('Yatar')
.buildForm();
}

void main(List<String> args) {
final director = FormDirector(FormBuilderSource());
final formForJohn = director.constructForJohn;
final formForJane = director.constructForJane;
}

In the main function, we create an instance of the FormDirector by providing it a FormBuilderSource. With the Director in place, we can now construct forms for different individuals with ease.

formForJohn: Using the Director, we construct a form for John Doe, setting his name, surname, email, and password. This form is built using the constructForJohn method, which internally calls the builder's methods to assemble the form.

formForJane: Similarly, we create a form for Jane Smith by invoking the constructForJane method. This demonstrates the flexibility and efficiency that the Director Pattern brings to the construction process.

By utilizing the Director Pattern, we have separated the process of building complex forms from the actual form structure, enhancing readability and maintainability. This approach enables us to easily extend and modify form creation as needed, making it a valuable tool in managing intricate object construction.

In this example, it may not be a good example for the default form, but in another example, it may sometimes be necessary for more than one different situation.

Resource: this link is all code sources in my GitHub

FINAL

Summarily, the Director and Builder Patterns provide a structured and organized way to build complex objects, as demonstrated in this example of constructing personalized forms. This understanding empowers you to streamline your code and simplify the creation of intricate objects. By harnessing the potential of design patterns like the Builder and Director Patterns, we’ve delved into the means to optimize software development processes. While we showcased Reactive Forms as just one example, the primary focus remains on illustrating the effective utilization of the Builder Pattern and Director Pattern. These patterns not only facilitate the construction of complex objects in an organized and comprehensible manner but also expedite development processes and enhance code quality. Especially in extensive and intricate projects, mastering these patterns can significantly elevate your software development endeavors.

I hope that the information I shared in this article will be useful to you. Continue on your journey and always remain open to learning! :)