Introduction
The Abstract Factory 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 first article on Introduction to Design Patterns in Flutter. In this article, I will be covering the following topics.
- What is the Abstract Factory?
- An Example of Abstract Factory In Flutter
- When to use an Abstract Factory?
- Pitfalls of Abstract Factory
- Conclusion
So before going to make any delay, let’s start.
What is the Abstract Factory?
According to the book Design Patterns,Elements of Reusable Object-Oriented Software, Abstract Factory provides an interface for creating families of related or dependent objects without specifying their concrete classes.
I am sure you didn't understand anything in the preceding introduction, so let me explain it in simpler terms. However, if you haven't read my previous article, please check out Factory Method Design Pattern In Flutter because this one is connected to it.
In our previous article, we looked at how to display different UI widgets based on the platform using factory method design patterns. You may recall that we only used this approach on a single widget. But what if we have to manage numerous widgets? This is where the Abstract Factory Method comes in. This design pattern allows us to handle the construction of several factory widgets at the same time, making the process more efficient and orderly.
Note. Abstract Factory, also known as Kit
An Example of Abstract Factory In Flutter
Let's look at how Flutter implements the abstract factory design pattern. In this example, we will create a button widget that changes its style depending on the operating system. Similarly, we'll construct an alert dialogue box class that varies depending on the platform (iOS or Android). Now, let's get into the code.
Step 1. AppButton
We are going to create an AppButton class that will dynamically provide a specific button widget based on the platform in use. For instance, if the platform is Android, it will instantiate the AndroidButton, and if it's iOS, it will instantiate the IOSButton class. This approach leverages the Factory Method design pattern, which we previously explored in our last article.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Abstract class AppButton serves as a blueprint for creating platform-specific buttons
abstract class AppButton {
// Factory constructor that returns an instance of a concrete implementation of AppButton based on the platform
factory AppButton(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android:
// Returns AndroidButton for Android platform
return AndroidButton();
case TargetPlatform.iOS:
// Returns IOSButton for iOS platform
return IOSButton();
default:
// Returns AndroidButton as a default button
return AndroidButton();
}
}
// Abstract method button which will be implemented in concrete classes
// It takes context, child widget and onPressed callback as parameters
Widget button({required BuildContext context, required Widget child, required VoidCallback onPressed});
}
// AndroidButton is a concrete implementation of AppButton for Android
class AndroidButton implements AppButton {
@override
// Implementation of button method for Android platform
// It returns an ElevatedButton widget
Widget button({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
// Implementation of button method for iOS platform
// It returns a CupertinoButton widget
Widget button({required BuildContext context, required Widget child, required VoidCallback onPressed}) {
return CupertinoButton(
onPressed: onPressed,
child: child,
);
}
}
Step 2. AppAlertBox
Now we will create an AppAlertBox that will dynamically generate a specific alert widget based on the platform in use. For instance, if the platform is Android, it will instantiate the AndroidAlertBox, and if it's iOS, it will instantiate the IOSAlertBox class.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
// Abstract class AppAlertBox serves as a blueprint for creating platform-specific alert boxes
abstract class AppAlertBox {
// Factory constructor that returns an instance of a concrete implementation of AppAlertBox based on the platform
factory AppAlertBox(TargetPlatform platform) {
switch (platform) {
case TargetPlatform.android:
// Returns AndroidAlertBox for Android platform
return AndroidAlertBox();
case TargetPlatform.iOS:
// Returns IOSAlertBox for iOS platform
return IOSAlertBox();
default:
// Returns AndroidAlertBox as a default alert box
return AndroidAlertBox();
}
}
// Abstract method show which will be implemented in concrete classes
// It takes context as a parameter
void show({required BuildContext context});
}
// AndroidAlertBox is a concrete implementation of AppAlertBox for Android
class AndroidAlertBox implements AppAlertBox {
@override
// Implementation of show method for Android platform
// It shows a Material Design AlertDialog
void show({required BuildContext context}) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Alert'),
content: const Text('This is an Android alert box.'),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
}
// IOSAlertBox is a concrete implementation of AppAlertBox for iOS
class IOSAlertBox implements AppAlertBox {
@override
// Implementation of show method for iOS platform
// It shows a Cupertino-style AlertDialog
void show({required BuildContext context}) {
showCupertinoDialog(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: const Text('Alert'),
content: const Text('This is an iOS alert box.'),
actions: <Widget>[
CupertinoDialogAction(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
);
},
);
}
}
Step 3. AppAbstractFactoryImpl
We have two factory methods, AppButton and AppAlertBox. If we add more factory methods in our project, managing and working with them might get difficult. This is when the abstract factory design pattern comes into picture. It centralizes all factory methods in one location, serving as a 'factory of factories'. Simply put, it's a one-stop shop for creating a variety of objects.
import 'package:abstract_factory/app_alert_box.dart';
import 'package:abstract_factory/app_button.dart';
import 'package:flutter/material.dart';
// Class AppAbstractFactoryImpl provides static methods to create platform-specific UI elements
class AppAbstractFactoryImpl {
// Static method platformAppButton creates a platform-specific button
// It uses the AppButton factory to create a button based on the platform
// The button is created with the provided context, child widget, and onPressed callback
static Widget platformAppButton({required BuildContext context, required Widget child, required VoidCallback onPressed}) {
return AppButton(Theme.of(context).platform)
.button(context: context, child: child, onPressed: onPressed);
}
// Static method platformAppAlertBox shows a platform-specific alert box
// It uses the AppAlertBox factory to create an alert box based on the platform
// The alert box is shown in the provided context
static void platformAppAlertBox({required BuildContext context}) {
AppAlertBox(Theme.of(context).platform).show(context: context);
}
}
Step 4. Home Page (main.dart)
On the Home Page, we will use the AbstractFactoryImpl class method. To display the button and alertbox.
import 'package:abstract_factory/app_abstract_factory.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@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(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Design Patterns'),
),
body: Center(
// Uses the AppAbstractFactoryImpl to create a platform-specific button
child: AppAbstractFactoryImpl.platformAppButton(
context: context,
child: const Text('Show Alert Box'),
onPressed: () {
// Uses the AppAbstractFactoryImpl to show a platform-specific alert box
AppAbstractFactoryImpl.platformAppAlertBox(context: context);
},
),
)
);
}
}
When To Use an Abstract Factory?
Here are some simple scenarios when you might want to use the Abstract Factory pattern
- When a system must be independent of how its products are created, composed, and represented.
- When a system should be set up using one of several product families.
- If a family of related products is designed to be used together, this constraint must be enforced.
- If you only want to expose the interfaces of products, not their implementations, in a class library.
Pitfalls of Abstract Factory
Here are some potential pitfalls of using the Abstract Factory pattern
- When you only need to create simple, similar objects, your code can become overly complicated.
- Code can become more difficult to maintain and navigate if there are a lot of small, separate classes.
- In order to add new types of products, you may need to extend the factory interface, which could break existing code.
- If your app only uses a single type of product family, it may not be necessary to use this.
Conclusion
In Flutter, the Abstract Factory design pattern is an important tool for creating platform-specific widgets. This centralizes the creation of related objects, making it easier to manage and work with multiple factory methods. This pattern is particularly useful when you want to enforce the use of related products together, swap product families without changing client code, or hide product creation code from the client. However, it's important to be mindful of potential pitfalls such as overcomplication of code, difficulty in navigating and maintaining numerous small classes, challenges in adding new product types, and unnecessary implementation for apps that only use one type of product family. If you like my articles, you can connect with me on LinkedIn and say hi.
Resources
If you want to read more about Design Patterns in Flutter, the next article is listed below in this series.