Design patterns are a set of solutions to common software development problems that have been proven to be effective through years of experience. In the world of Android development, these patterns can be especially useful when working with the Kotlin programming language. This article will explore some of the most popular design patterns used in Android development with Kotlin. We will also explore how they can be implemented to improve the structure and organization of your code. From the SOLID principles to the Gang of Four patterns, we will delve into the best practices for designing your Android apps using Kotlin and the power of design patterns.
These patterns are divided into categories: Creational, Structural, and Behavioral.
How can they be useful?
In general, creational design patterns provide solutions for creating objects in a structured and organized manner. They can be used to promote loose coupling, allow for easy modification, allow for code reuse, simplify object creation and improve performance. Each pattern has its own strengths and weaknesses and should be chosen based on the project’s specific requirements. It’s important to understand each pattern’s trade-offs and potential drawbacks before choosing one to use in your application.
Creational patterns
Creational patterns focus on object-creation mechanisms and aim to create objects in a manner that is suitable to the situation. In this article, we will explore the Creational design patterns and how they can be used to improve the structure and organization of our code. By understanding these patterns and their implementation, developers can make more informed decisions when designing and building their software projects.
Factory Method
The Factory Method pattern is a creational design pattern that provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. It defines a method for creating objects, which subclasses can then override to change the type of objects that will be created.
Complexity: 🟢 ⚪ ⚪
Popularity: 🟢 🟢 🟢
The structure of the Factory Method pattern typically includes the:
- Product: An interface or abstract class that defines the type of objects that will be created. This interface or class defines the methods that all objects created by the factory method must implement.
interface Product { | |
fun use() | |
} |
- ConcreteProduct: Classes that implement the Product interface or inherit from the abstract Product class. These classes define the specific type of objects that will be created by the factory method.
class ConcreteProductA : Product { | |
override fun use() { | |
// implementation | |
} | |
} | |
class ConcreteProductB : Product { | |
override fun use() { | |
// implementation | |
} | |
} |
- Creator: An abstract class or interface that defines the factory method. This class or interface defines the method that subclasses will use to create objects.
abstract class Creator { | |
abstract fun factoryMethod(): Product | |
fun someOperation() { | |
val product = factoryMethod() | |
product.use() | |
} | |
} |
- ConcreteCreator: Classes that implement the Creator interface or inherit from the abstract Creator class. These classes provide the implementation of the factory method and determine the specific type of object that will be created.
class ConcreteCreatorA : Creator() { | |
override fun factoryMethod(): Product { | |
return ConcreteProductA() | |
} | |
} | |
class ConcreteCreatorB : Creator() { | |
override fun factoryMethod(): Product { | |
return ConcreteProductB() | |
} | |
} |
In this example, Product
is the interface that defines the methods that all created objects must implement, ConcreteProductA
and ConcreteProductB
are classes that implement the Product interface, Creator
is the abstract class that defines the factoryMethod and ConcreteCreatorA
and ConcreteCreatorB
are classes that provide the implementation of factoryMethod.
Here is a real-world implementation of the Factory Method pattern in Android:
interface RetrofitApi { | |
fun create(): Retrofit | |
} | |
class ProductionRetrofitApi : RetrofitApi { | |
override fun create(): Retrofit { | |
val retrofit = Retrofit.Builder() | |
.baseUrl("https://production-api.com") | |
.addConverterFactory(GsonConverterFactory.create()) | |
.build() | |
return retrofit | |
} | |
} | |
class StagingRetrofitApi : RetrofitApi { | |
override fun create(): Retrofit { | |
val retrofit = Retrofit.Builder() | |
.baseUrl("https://staging-api.com") | |
.addConverterFactory(GsonConverterFactory.create()) | |
.build() | |
return retrofit | |
} | |
} | |
class RetrofitApiFactory { | |
companion object { | |
fun createRetrofitApi(apiType: String): RetrofitApi? { | |
return when (apiType) { | |
"production" -> ProductionRetrofitApi() | |
"staging" -> StagingRetrofitApi() | |
else -> null | |
} | |
} | |
} | |
} | |
val productionApi = RetrofitApiFactory.createRetrofitApi("production")?.create() | |
val stagingApi = RetrofitApiFactory.createRetrofitApi("staging")?.create() |
The Factory Method pattern is good for several reasons:
- It promotes loose coupling: By encapsulating the process of creating objects in a separate factory class, it allows the client code to remain unaware of the specific classes that are being instantiated, promoting loose coupling between the client code and the objects it uses.
- It allows for easy modification: Because the factory method is defined in an interface or abstract class, it can be easily overridden by subclasses to change the type of objects that are created. This makes it easy to modify the code to create different types of objects without affecting the client code.
- It allows for code reuse: By encapsulating the process of creating objects in a separate factory class, it allows for code reuse across multiple classes. This can lead to a more organized and efficient codebase.
- It simplifies object creation: By encapsulating the process of creating objects in a factory class, it can simplify the creation of complex objects, making it easier to create and manage them throughout the lifetime of the application.
- It supports the Open-Closed Principle: The Factory Method pattern allows new objects to be added to the system without modifying existing client code, it supports the open-closed principle which states that classes should be open for extension but closed for modification.
Abstract Factory
The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. It uses a set of factory methods, each of which creates a related object of a specific class.
Complexity: 🟢 🟢 ⚪
Popularity: 🟢 🟢 🟢
The structure of the Abstract Factory pattern typically includes:
- AbstractFactory: An interface that defines the factory methods for creating objects. These methods return objects of the Product interface.
interface AbstractFactory { | |
fun createProductA(): ProductA | |
fun createProductB(): ProductB | |
} |
- ConcreteFactory: Classes that implement the AbstractFactory interface. These classes provide the implementation of the factory methods, creating objects of the ConcreteProduct classes.
class ConcreteProductA1 : ProductA { | |
override fun use() { | |
// implementation | |
} | |
} | |
class ConcreteProductA2 : ProductA { | |
override fun use() { | |
// implementation | |
} | |
} | |
class ConcreteProductB1 : ProductB { | |
override fun use() { | |
// implementation | |
} | |
} | |
class ConcreteProductB2 : ProductB { | |
override fun use() { | |
// implementation | |
} | |
} |
Job Offers
- Product: An interface that defines the type of objects that will be created by the factory methods.
interface ProductA { | |
fun use() | |
} | |
interface ProductB { | |
fun use() | |
} |
- ConcreteProduct: Classes that implement the Product interface. These classes define the specific type of objects that will be created by the ConcreteFactory classes.
class ConcreteFactory1 : AbstractFactory { | |
override fun createProductA(): ProductA { | |
return ConcreteProductA1() | |
} | |
override fun createProductB(): ProductB { | |
return ConcreteProductB1() | |
} | |
} | |
class ConcreteFactory2 : AbstractFactory { | |
override fun createProductA(): ProductA { | |
return ConcreteProductA2() | |
} | |
override fun createProductB(): ProductB { | |
return ConcreteProductB2() | |
} | |
} |
In this example, AbstractFactory
is the interface that defines the factory methods, ConcreteFactory1
and ConcreteFactory2
are classes that implement the AbstractFactory interface, ProductA
and ProductB
are interfaces that define the methods that all created objects must implement, ConcreteProductA1
, ConcreteProductA2
, ConcreteProductB1
and ConcreteProductB2
are classes that implement the ProductA and ProductB interfaces.
The Abstract factory pattern is closely related to the factory method pattern. The main difference is that the factory method creates objects of a single class, while the abstract factory creates objects of multiple classes. The abstract factory pattern provides a way to encapsulate a group of individual factories that have a common theme without specifying their concrete classes.
Here is an example of a real-world implementation of the Abstract Factory pattern in Kotlin Android:
interface AlertDialog { | |
fun show() | |
} | |
class MaterialAlertDialog : AlertDialog { | |
override fun show() { | |
println("Showing Material Design Alert Dialog") | |
} | |
} | |
class CupertinoAlertDialog : AlertDialog { | |
override fun show() { | |
println("Showing Cupertino Alert Dialog") | |
} | |
} | |
interface DialogFactory { | |
fun createAlertDialog(): AlertDialog | |
} | |
class MaterialDialogFactory : DialogFactory { | |
override fun createAlertDialog(): AlertDialog { | |
return MaterialAlertDialog() | |
} | |
} | |
class CupertinoDialogFactory : DialogFactory { | |
override fun createAlertDialog(): AlertDialog { | |
return CupertinoAlertDialog() | |
} | |
} | |
class DialogFactoryProvider { | |
companion object { | |
fun getDialogFactory(factoryType: String): DialogFactory? { | |
return when (factoryType) { | |
"material" -> MaterialDialogFactory() | |
"cupertino" -> CupertinoDialogFactory() | |
else -> null | |
} | |
} | |
} | |
} | |
val materialDialogFactory = DialogFactoryProvider.getDialogFactory("material") | |
val materialAlertDialog = materialDialogFactory?.createAlertDialog() | |
materialAlertDialog?.show() // Output: "Showing Material Design Alert Dialog" | |
val cupertinoDialogFactory = DialogFactoryProvider.getDialogFactory("cupertino") | |
val cupertinoAlertDialog = cupertinoDialogFactory?.createAlertDialog() | |
cupertinoAlertDialog?.show() // Output: "Showing Cupertino Alert Dialog" |
Both Factory Method and Abstract Factory patterns are good for creating objects in a structured and organized manner, but they are tailored for different needs. The Factory Method pattern is good for creating individual objects, while the Abstract Factory pattern is good for creating families of related or dependent objects. Both patterns have benefits and trade-offs and should be chosen based on the specific requirements of the project.
Builder
The Builder pattern is a creational design pattern that separates the construction of a complex object from its representation so that the same construction process can create different representations. It is used when an object has too many fields or its construction process is complex.
Complexity: 🟢 🟢 ⚪
Popularity: 🟢 🟢 🟢
The structure of the Builder pattern typically includes:
- Builder: An interface that defines the methods for building the parts of the complex object.
- ConcreteBuilder: A class that implements the Builder interface. It provides the implementation of the methods for building the parts of the complex object and also keeps track of the constructed object.
- Director: A class that uses the Builder interface to construct an object. It defines the construction process but does not know the concrete classes of the Builder or the object being constructed.
- Product: The complex object that is being constructed.
Here is an example of the structure of the Builder pattern in Kotlin:
class Product { | |
var partA: String = "" | |
var partB: String = "" | |
var partC: String = "" | |
} | |
interface Builder { | |
fun buildPartA(part: String) | |
fun buildPartB(part: String) | |
fun buildPartC(part: String) | |
fun getResult(): Product | |
} | |
class ConcreteBuilder : Builder { | |
private val product = Product() | |
override fun buildPartA(part: String) { | |
product.partA = part | |
} | |
override fun buildPartB(part: String) { | |
product.partB = part | |
} | |
override fun buildPartC(part: String) { | |
product.partC = part | |
} | |
override fun getResult(): Product { | |
return product | |
} | |
} | |
class Director { | |
private lateinit var builder: Builder | |
fun setBuilder(builder: Builder) { | |
this.builder = builder | |
} | |
fun buildMinimalViableProduct() { | |
builder.buildPartA("part A") | |
} | |
fun buildFullFeaturedProduct() { | |
builder.buildPartA("part A") | |
builder.buildPartB("part B") | |
builder.buildPartC("part C") | |
} | |
} |
In this example, Product
is the complex object that is being constructed, Builder
is the interface that defines the methods for building the parts of the complex object, ConcreteBuilder
is a class that implements the Builder interface and provide the implementation of the methods for building the parts of the complex object, Director
is a class that uses the Builder interface to construct an object and defines the construction process.
Here is an example of a real-world implementation of the Builder pattern in Kotlin Android:
class User { | |
var name: String = "" | |
var email: String = "" | |
var age: Int = 0 | |
var address: String = "" | |
override fun toString(): String { | |
return "User(name='$name', email='$email', age=$age, address='$address')" | |
} | |
} | |
class UserBuilder { | |
private var user = User() | |
fun name(name: String): UserBuilder { | |
user.name = name | |
return this | |
} | |
fun email(email: String): UserBuilder { | |
user.email = email | |
return this | |
} | |
fun age(age: Int): UserBuilder { | |
user.age = age | |
return this | |
} | |
fun address(address: String): UserBuilder { | |
user.address = address | |
return this | |
} | |
fun build(): User { | |
return user | |
} | |
} | |
val user = UserBuilder() | |
.name("John Smith") | |
.email("john.smith@example.com") | |
.age(30) | |
.address("123 Main St") | |
.build() | |
println(user) | |
// Output: User(name='John Smith', email='john.smith@example.com', age=30, address='123 Main St') |
The Builder pattern is good for several reasons:
- It allows to create different representations of the same object: The Builder pattern allows to create different representations of the same object, this can be very useful in situations where the same object needs to be constructed in different ways.
- It supports the Single Responsibility Principle: The Builder pattern separates the construction process of an object from its representation, this way the class that defines the object only has to worry about the representation, and the Builder class only has to worry about the construction process. This helps to adhere to the Single Responsibility Principle.
Prototype
The Prototype pattern is a creational design pattern that allows objects to be created by copying existing objects, rather than creating new ones from scratch. It is used when creating a new object is either expensive or impossible.
Complexity: 🟢 ⚪ ⚪
Popularity: 🟢 🟢 ⚪
The structure of the Prototype pattern typically includes:
- Prototype: An interface that defines the
clone()
method, which creates a copy of the object.
interface Prototype { | |
fun clone(): Prototype | |
} |
- ConcretePrototype: A class that implements the Prototype interface. It provides the implementation of the
clone()
method, which creates a copy of the object.
class ConcretePrototype : Prototype { | |
var attribute: String = "" | |
override fun clone(): Prototype { | |
val clone = ConcretePrototype() | |
clone.attribute = this.attribute | |
return clone | |
} | |
} |
- Client: A class that uses the Prototype interface to create new objects.
class Client { | |
fun operation(prototype: Prototype) { | |
val prototypeClone = prototype.clone() | |
// use the clone | |
} | |
} |
The Prototype pattern is good for several reasons:
- It reduces the need for subclasses: When creating new objects based on existing ones, the Prototype pattern reduces the need for creating subclasses to instantiate new objects, this can simplify the codebase.
- It improves performance: The Prototype pattern improves performance by avoiding the overhead of creating a new object from scratch. Instead, it creates a copy of an existing object, which can be faster and less memory-intensive.
- It allows for dynamic object creation: The Prototype pattern allows for dynamic object creation, this can be very useful when the type of object that needs to be created is not known until runtime.
Singleton
The Singleton pattern is a creational design pattern that ensures that a class has only one instance and provides a global access point to this instance. It is used when a single instance of a class needs to coordinate actions across the system.
Complexity: 🟢 ⚪ ⚪
Popularity: 🟢 🟢 ⚪
The structure of the Singleton pattern typically includes:
- Singleton: A class that implements the Singleton pattern. It has a private constructor and a static instance variable that holds the single instance of the class. It also has a static method (often called
getInstance()
) that returns the single instance of the class.
Here is an example of the structure of the Singleton pattern in Kotlin:
class Singleton private constructor() { | |
companion object { | |
private var instance: Singleton? = null | |
fun getInstance(): Singleton { | |
if (instance == null) { | |
instance = Singleton() | |
} | |
return instance!! | |
} | |
} | |
} |
In this example, Singleton
is a class that implements the Singleton pattern, it has a private constructor and a companion object that holds the single instance of the class and a static method getInstance()
to return the single instance of the class.
This is the most basic implementation of a singleton pattern, but it’s not thread-safe. It is also possible to use other thread-safe methods of implementing a singleton such as using synchronized
keywords, double-checked locking, using an enum, and using a static block.
Here is a more real-world implementation of the Singleton pattern in Kotlin Android:
class DatabaseHelper private constructor(context: Context) { | |
companion object { | |
private var instance: DatabaseHelper? = null | |
fun getInstance(context: Context): DatabaseHelper { | |
if (instance == null) { | |
instance = DatabaseHelper(context) | |
} | |
return instance!! | |
} | |
} | |
private val database: SQLiteDatabase = context.openOrCreateDatabase("app_db", Context.MODE_PRIVATE, null) | |
fun executeQuery(query: String) { | |
database.execSQL(query) | |
} | |
fun getData(query: String): Cursor { | |
return database.rawQuery(query, null) | |
} | |
} | |
val databaseHelper = DatabaseHelper.getInstance(applicationContext) | |
val cursor = databaseHelper.getData("SELECT * FROM users") |
The Singleton pattern is good for several reasons:
- It improves performance: By reusing the same instance of a class, the Singleton pattern can improve performance by avoiding the overhead of creating new instances of a class.
- It controls concurrent access: The singleton pattern can be used to control concurrent access to a shared resource, this is useful in situations where multiple threads are trying to access the same resource simultaneously.
- It helps to maintain the state of an object: By ensuring that there’s only one instance of an object, it helps to maintain the state of an object across multiple requests.
Happy coding!
This article covers the five most important Creational Design Patterns in Android Kotlin: the Factory Method, Abstract Factory, Builder, Prototype, and Singleton patterns. Each of these patterns has its own unique purpose and can be applied to different scenarios in Android development. By understanding and using these patterns, you can improve the flexibility, maintainability, and reusability of your code. With a solid understanding of these design patterns, you will be well on your way to becoming a Zero To Hero Android developer. Remember that the best way to become proficient in using these patterns is by practicing and experimenting with them in your own projects. Happy coding!
Learn more
If you want to learn more about design patterns in general, I recommend visiting refactoring.guru. This website provides a comprehensive catalog of design patterns, including creational, structural, and behavioral patterns with clear and concise explanations, diagrams, and code examples in multiple programming languages including Kotlin. It’s a great resource for understanding design patterns and how they can be applied in real-world scenarios. Check it out and start exploring the world of design patterns!
This article was originally published on proandroiddev.com