Introduction

The Factory Method design pattern is one of the 23 design patterns. It is a creational design pattern. If you're unsure what a creational design pattern is, I suggest you check out my previous article on Introduction to Design Patterns in Flutter. In this article, I will be covering the following topics.

So before going to make any delay, let’s start.

What is the Factory Method?

According to the book Design Patterns, Elements of Reusable Object-Oriented Software, Factory Method defines an interface for creating an object but lets subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.

I am sure from the above introduction you didn't understand anything, so let me describe it in more simple terms.

Assume you're running an internet store. Your consumers can pay for their products in a variety of ways, including cards(credits/debits), PayPal, and bank transfers.

Now, you must include a method in your code to handle all of these distinct forms of payments. You could write separate pieces of code for each payment method, but this would be cluttered and difficult to manage.

This is where the factory method comes into the picture. Rather than constructing payment method objects directly in your code, you write a method (the "Factory Method") that does it for you.

You tell the Factory Method the form of payment you require (cards, PayPal, or bank transfer), and it returns an object of that type as you can see in the diagram below

Factory design pattern

I hope you understand now, but if you are still confused, don't worry. Let's look at the code problem, and then we will see how the Factory Method solves it.

Note. The factory method is sometimes called a virtual constructor as well.

Problem Statement

Above, we looked at an example where an online store needs to integrate a payment mechanism in their program, where a consumer can pay the amount by payment methods such as cards, PayPal, and bank transfer. Now, let's code that.

// Card class represents a payment method using a card
class Card{
  // Method to make payment using a card
  void makePayment() {
    print("Make your payment through card");
  }
}
// PayPal class represents a payment method using PayPal
class PayPal {
  // Method to make payment using PayPal
  void makePayment() {
    print("Make payment through paypal");
  }
}
// BankTransfer class represents a payment method using bank transfer
class BankTransfer {
  // Method to make payment using bank transfer
  void makePayment() {
    print("Make payment through bank transfer");
  }
}
// checkOut function takes a payment method as a string and makes payment using the appropriate method
void checkOut(String paymentMethod) {
  if(paymentMethod == 'card') {
    Card card = Card();
    card.makePayment();
  }else if(paymentMethod == 'paypal') {
    PayPal paypal = PayPal();
    paypal.makePayment();
  }else if(paymentMethod == 'bank_transfer') {
    BankTransfer bankTransfer = BankTransfer();
    bankTransfer.makePayment();
  }else {
    print("Invalid payment method");
  }
}
// Main function of the program, it calls the checkOut function with 'bank_transfer' as the payment method
void main() {
  checkOut('bank_transfer');
}

Some issues with this approach

Now, you might be thinking it’s simple, but wait, there are some issues with this approach, such as

Solution

An efficient solution to the above problem can be achieved through the Factory Method pattern; let’s see how.

// Enum representing different types of payment methods
enum PaymentType {
  card,
  paypal,
  bankTransfer
}
// Abstract class PaymentMethod with a factory constructor to create different types of payment methods
abstract class PaymentMethod {
  factory PaymentMethod(PaymentType type) {
    switch(type) {
      case PaymentType.card:
        return Card();
      case PaymentType.paypal:
        return PayPal();
      case PaymentType.bankTransfer:
        return BankTransfer();
      default: 
        return Card();
    }
  }
  // Abstract method to make payment
  void makePayment();
}
// Card class implements PaymentMethod and provides implementation for makePayment method
class Card implements PaymentMethod{
  @override
  void makePayment() {
    print("Make your payment through card");
  }
}

// PayPal class implements PaymentMethod and provides implementation for makePayment method
class PayPal implements PaymentMethod{
  @override
  void makePayment() {
    print("Make payment through paypal");
  }
}

// BankTransfer class implements PaymentMethod and provides implementation for makePayment method
class BankTransfer implements PaymentMethod{
  @override
  void makePayment() {
    print("Make payment through bank transfer");
  }
}

// checkOut function takes a PaymentType and makes payment using the appropriate method
void checkOut(PaymentType paymentType) {
  PaymentMethod paymentMethod = PaymentMethod(paymentType);
  paymentMethod.makePayment();
}

// Main function of the program, it calls the checkOut function with PaymentType.paypal as the payment method
void main() {
  checkOut(PaymentType.paypal);
}

Note: Implementation acts like an interface, while extends act like a base class in Dart.

What are the advantages of using a factory method over the old code?

You might be wondering what are the advantages of using a factory method over the old code, so here are some.

When To Use Factory Method Design Pattern?

The disadvantage of the Factory Method

An Example of Factory Method In Flutter

Let's learn how to implement the Factory Method design pattern in Flutter. In this example, we will create a button widget that will change its style depending on the platform it is running on. For instance, if the platform is Android, the widget will render an Android button. Similarly, if the platform is iOS, the widget will render an iOS button.

// Importing necessary Flutter packages
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

// Entry point of the Flutter application
void main() {
 runApp(const MyApp());
}
// MyApp is the root widget of the application
class MyApp extends StatelessWidget {
 const MyApp({super.key});
 // This method builds the widget tree for MyApp
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     debugShowCheckedModeBanner: false,
     title: 'Flutter Demo',
     theme: ThemeData(
       colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
       useMaterial3: true,
     ),
     home: const HomePage(),
   );
 }
}
// HomePage is the home screen of the application
class HomePage extends StatelessWidget {
 const HomePage({super.key});
 // This method builds the widget tree for HomePage
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Factory Method Example'),
     ),
     body: Center(
       child: AppButton(Theme.of(context).platform).build(
           context: context,
           child: const Text('Button'),
           onPressed: () {
             print("Button clicked");
           }),
     ),
   );
 }
}
// AppButton is an abstract class that defines a factory method for creating platform-specific buttons
abstract class AppButton {
 factory AppButton(TargetPlatform platform) {
   switch (platform) {
     case TargetPlatform.android:
       return AndroidButton();
     case TargetPlatform.iOS:
       return IOSButton();
     default:
       return AndroidButton();
   }
 }
 // This method builds the platform-specific button
 Widget build(
     {required BuildContext context,
     required Widget child,
     required VoidCallback onPressed});
}
// AndroidButton is a concrete implementation of AppButton for Android
class AndroidButton implements AppButton {
 @override
 Widget build(
     {required BuildContext context,
     required Widget child,
     required VoidCallback onPressed}) {
   return ElevatedButton(
     onPressed: onPressed,
     child: child,
   );
 }
}
// IOSButton is a concrete implementation of AppButton for iOS
class IOSButton implements AppButton {
 @override
 Widget build(
     {required BuildContext context,
     required Widget child,
     required VoidCallback onPressed}) {
   return CupertinoButton(
     onPressed: onPressed,
     child: child,
   );
 }
}

You can access the AppButton like this

AppButton(Theme.of(context).platform).build(
   context: context,
   child: const Text('Button'),
   onPressed: () {
     //onpress logic will go here
   })

Theme.of(context).platform dynamically detects the current platform

Conclusion

The Factory Method design pattern is an effective tool in a developer's arsenal. It encapsulates the complexity of object creation and improves code flexibility, maintainability, and scalability. It allows subclasses to choose which class to instantiate, encouraging loose coupling.

However, as with any tool, it should be handled with caution. Overuse can cause needless complexity and make the code more difficult to understand and debug. It's critical to understand the problem before using the Factory Method. If you like my articles, you can connect with me on LinkedIn and say hi.

If you want to read more about Design Patterns in Flutter, the next articles are listed below in this series.