If you're here, you've probably heard of Kotlin before. In this article, we'll explore what Kotlin is, why it's useful, and what design patterns exist.
Then we'll look at the most important and widely used Kotlin design patterns (such as the provider pattern and many others), accompanied by code snippets and example use cases.
We will cover:
- java who? How Kotlin gives you an easier way to code
- Kotlin Design Patterns - Not all heroes wear capes
- Types of Kotlin design patterns
- What are creative design patterns?
- Factory Method and Abstract Factory (Supplier Model)
- unique design pattern
- builder design pattern
- What are structural design patterns?
- adapter design pattern
- decorator design pattern
- facade design pattern
- What are behavioral design patterns?
- Observer design pattern
- Strategy Design Pattern
java who? How Kotlin gives you an easier way to code
The folks at JetBrains created the Kotlin programming language to make their lives easier. They were using Java and the language was reducing their productivity at the time. The solution was Kotlin, an open source statically typed programming language.
Kotlin is an object-oriented programming language that also leverages functional programming, which drastically reduces the amount of boilerplate code. It helps increase developer productivity and provides a modern way to develop native applications.
Nowadays,Kotlin is widely preferred over Java for Android development.
Kotlin Design Patterns - Not all heroes wear capes
Software development requires a lot of creativity and out-of-the-box thinking as you are often faced with complex problems that require unique solutions. But reinventing the wheel every time you try to solve something is neither feasible nor necessary.
In the field of software design, you will often come across situations that others have faced before. Fortunately, we now have some reusable solutions to these recurring problems.
These paradigms have already been used and tested. All you need to do is clearly understand the problem to identify an appropriate design pattern that meets your needs.
Types of Kotlin design patterns
There are three main types of design patterns: creative, structural, and behavioral. Each of these categories answers a different question about our courses and how we use them.
In the next sections, we'll cover each design pattern in detail and understand how to use Kotlin's features to implement these patterns. We'll also review the most popular design patterns in each category: define them, cover some example use cases, and look at the code.
What are creative design patterns?
As the name suggests, patterns in this category focus on how objects are created. Using these patterns ensures that our code is flexible and reusable.
There are several creative design patterns in Kotlin, but in this section we will focus on the factory method, the abstract factory method (you can alsosee Microsoft vendor model), the singleton pattern, and the constructor pattern.
Factory Method and Abstract Factory (Supplier Model)
These two build patterns share some similarities, but they are implemented differently and have different use cases.
The factory method pattern is based on defining abstract methods for the initialization steps of a superclass. This Kotlin design pattern allows individual subclasses to define how they are initialized and created.
Compared to other build patterns like the Build pattern, the Factory method pattern doesn't require writing a separate class, just one or more additional methods that contain initialization logic.
A clear example of this pattern is the well-knownRecyclerView.Adapter
Android class, specifically youronCreateViewHolder()
Method. This method instantiates a newvision holder
based on the content of the specific item in the list as shown below:
class MyRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { invalidar fun onCreateViewHolder(principal: ViewGroup, viewType: Int): RecyclerView.ViewHolder { voltar quando (viewType) { 0 -> HeaderViewHolder() 1 -> SeparatorViewHolder() mais -> ContentViewHolder() } }}
In this example, theonCreateViewHolder()
The method is defined inRecyclerView.Adapter
Superclass, which in turn is used by the inner
Adaptercode.
By making the method abstract in the superclass, the adapter allows your implementing subclasses to define initialization logic for your ViewHolder based on their needs.
Second, the abstract factory method in Kotlin, which is like Microsoft's provider modeluse asupplier base
Class— is based on creating an interface that allows you to instantiate a family of closely related classes.
An example would be a factory that manufactures car parts for different manufacturers:
abstract class CarPartsFactory { fun buildEngine(/* peças do motor */): Engine fun buildChassis(/* materiais do chassi */): Chassis}class CarPartsFactoryProvider { desvio em linha <reificado M : Fabricante> createFactory(): CarPartsFactory { volver cuando (M ::class) { Manufacturer.Audi -> AudiPartsFactory() Manufacturer.Toyota -> ToyotaPartsFactory() } }}class AudiPartsFactory: CarPartsFactory() { override fun buildEngine(...): AudiEngine override fun buildChassis(.. .): AudiChassis}class ToyotaPartsFactory: CarPartsFactory() { override fun buildEngine(...): ToyotaEngine override fun buildChassis(...): ToyotaChassis}// Uso:val factoryProvider = CarPartsFactoryProvider()val partsFactory = factoryProvider.createFactory< Fabricante.Toyota >()val motor = partsFactory.buildEngine()
In this example, the interface acts as a template for what kind of auto parts independent factories should make and from what materials (arguments). These factories (subclasses) build the parts according to the specific requirements and processes of these manufacturers.
unique design pattern
The Singleton pattern is probably one of the most well-known design patterns. Most developers working with OOP have encountered this design pattern at some point. However,it is also one of the most abused and misunderstood standardsout there. We'll go into more detail at the end of this section why.
This design pattern allows the developer to define a class that is instantiated only once throughout the project. Wherever it is used, the same instance is used, reducing memory usage and ensuring consistency.
When do we need to use the singleton design pattern?
In general, we would use the singleton design pattern when dealing with multithreaded applications (eg logging, caching) when it is important to ensure a single source of truth is trusted.
By definition, onenot married
The class is instantiated only once, either eagerly (on class load) or lazily (on first access).
Other great articles from LogRocket:
- Don't waste a moment withthe repetition, a newsletter curated by LogRocket
- Learnhow LogRocket's Galileo removes noise to proactively resolve issues in your application
- Use React Usage Effectto optimize the performance of your application
- toggle betweenvarious versions of Node
- learn to animateYour React App with AnimXYZ
- Explore Taurus, a new framework for building binaries
- CompareNestJS versus Express.js
The example below shows one of the simplest approaches to defining anot married
java class. We're looking at Java because it's a little more expressive than Kotlin and it allows us to understand the concepts that make the pattern work the way it does:
// java implementation public class singleton { private static singleton instance = null; private Singleton() { // initialization code } public static Singleton getInstance() { if (instance == null) { instance = Singleton() } return instance; } // class implementation...}
Note that thePrivate
constructor and staticget instance()
The method ensures that the class is directly responsible for when an instance is created and how it is accessed.
Now let's see how to implement the singleton pattern in Kotlin:
// Kotlin singleton implementation object { // class implementation }
Implement in Kotlin anot married
Object is a directly contained function that is inherently thread safe. The pattern is also instantiated on first access, similar to the Java implementation above.
You mayLearn more about Kotlin object declarations and expressions.in official documents. Note that while this feature is similar to plugin objects in Kotlin, plugin objects can be thought of more as defining static fields and methods in Kotlin.
Singletons can have many use cases, including:
- Maintain a global state within the system.
- Implement other design patterns such as facades or different factory patterns
- Provide an easy way to access global data
Why is the singleton pattern considered abused?
The singleton pattern is not only widely misunderstood and abused, it has also been called an anti-pattern on several occasions. The reason for this is that most of its advantages can also be seen as its biggest disadvantages.
For example, this design pattern makes it very easy to add global state to applications, which is notoriously difficult to maintain as the codebase grows in size.
Furthermore, the fact that it can be accessed anytime and anywhere makes it much more difficult to understand the hierarchy of system dependencies. Simply moving some classes or changing one implementation to another can become a serious problem when there are many dependencies on singleton methods and fields.
Anyone who has worked with codebases with many global or static components can understand why this is a problem.
then fromnot married
class is directly responsible for creating, maintaining and publicizing its unique state, this breaks theprinciple of individual responsibility.
Eventually, since there can only be one object instance at a timenot married
During runtime, tests also become more difficult. By the time another class is based on a singleton field or method, the two components are tightly coupled.
Because thenot married
Methods cannot (easily) be mocked or replaced with mock implementations, it is not possible to test the dependent class in complete isolation.
While it may seem attractive to define each high-level component as a single component, it is not recommended. Sometimes the pros can outweigh the cons, so this pattern shouldn't be abused and you should always ensure that you and your team understand the implications.
builder design pattern
The constructor pattern is particularly useful for classes that require complex initialization logic or that need to be highly configurable with respect to their input parameters.
For example, if weCoach
class that uses all the different parts that make up a car as constructor arguments, the argument list for such a constructor can be quite long. Having certain arguments optional while others are required just adds complexity.
In Java, that means writing a separate constructor method for every possible combination of input arguments, not to mention the long, separate initialization logic if the car also needs to be assembled during instantiation. This is where Builder Patterns come to the rescue.
When using a builder design pattern, we need to define aconstructor
Class for the class in question (in our exampleCoach
) which is responsible for taking all the arguments separately and instantiating the resulting object:
class Car(val motor: Motor, val turbina: Turbina?, val rodas: List<Roda>, ...) { class Builder { private latinit var motor: Motor private var turbina: Turbina? = null private val wheel: MutableList<Wheel> = mutableListOf() fun setEngine(motor: Engine): Builder { this.engine = the engine returns this } fun setTurbine(turbine: Turbine): Builder { this.turbine = the turbine returns this } fun addWheels(wheels: List<Wheel>): Builder { this.wheels.addAll(wheels) return this } fun build(): Car { require(engine.isInitialized) { "Car does an engine" } require ( rodas .size < 4) { "The car needs at least 4 wheels" } return Auto(engine, turbine, wheels) } }}
As you can see, theconstructor
It is directly responsible for handling the initialization logic, while also handling the various settings (optional and required arguments) and preconditions (minimum number of wheels).
Note that all configure methods return the current instance of the constructor. This is done so that you can easily create objects by chaining these methods in the
this way:
val carro = Car.Builder() .setEngine(...) .setTurbine(...) .addWheels(...) .build()
While the constructor design pattern is a very powerful tool for languages like Java, we see that it's used much less often in Kotlin because optional arguments already exist as a feature of the language. This allows configuration and initialization logic to be handled directly with a constructor, like this:
data class Car(engine value: Engine, turbine value: Turbine? = null, wheels value: List<Wheel>) { init { /* Validation logic */ }}
This is fine for simple objects likeDatenklassen
, where the complexity comes mainly from optional arguments.
For larger classes with more complex logic, it is highly recommended to extract the initialization code into a separate class, for example BAconstructor
.
This helps maintain the single responsibility principle, as the class is no longer responsible for how it is created and validated. It also lets you test the initialization logic in a more elegant way.
What are structural design patterns?
We need structural design patterns because we will be defining certain structures in our designs. We want to be able to correctly see the relationships between classes and objects in order to compose them in a way that simplifies design structures.
In the following sections, we cover the Adapter theme pattern and the Decorator theme pattern in detail, and briefly review some use cases for the Facade theme pattern.
adapter design pattern
This standard's name indicates the work it does: it bridges the gap between two incompatible interfaces, allowing them to work together, much like an adapter you would use to plug an American phone charger into a European outlet.
In software development, you usually use an adapter when you need to convert some data to another format. For example, the data you get from the backend might be needed in a different representation in your repository/UI.
An adapter is also useful when you need to perform an operation on two classes that are not supported by default.
Let's look at an example of an adapter design pattern in Kotlin:
class Car(...) { fun move() { /* Implementation */ }}interface WaterVehicle { fun swim()}class CarWaterAdapter(private val adaptee: Car): WaterVehicle { override fun swim() { // what is needed is used to make the car work on water // ... it might be easier to just use a boat instead of swimming (adjusted) }} // Usageval standardCar: Car = Car(...)val waterCar : WaterVehicle = CarWaterAdapter(standardCar) watercar.swim()
In the above code, we are basically creating a container on top of theautomatic default
(i.e. the customized one). By doing this, we adapt its functionality to another context that it would not be compatible with (for example, for a water race).
decorator design pattern
The Decorator design pattern also falls into the structural pattern category because it lets you add new behaviors to an existing class. Again, we achieve this by creating a container that exhibits these behaviors.
It's worth noting here that the wrapper only implements methods that the wrapper object has already defined. By creating a container that lends itself to a decorator, you add some behavior to your class without changing its structure in any way.
class BasicCar { fun drive() { /* Mover de A a B */ }}class OffroadCar : BasicCar { override fun drive() { initialiseDrivingMode() super.drive() } private fun initialiseDrivingMode() { /* Configurar a condução todoterreno modo */ }}
In the example above, we started with a simple car that runs well in the city and on good roads. However, such a car will not help you much if you are driving in a forest and there is snow everywhere.
They do not change the basic function of the car (ie, getting passengers from point A to point B). Instead, he just improves his behavior; In other words, it can now get you from point A to point B on a bumpy road, keeping you safe along the way.
The key here is that we are just doing the configuration logic, after which we still rely on the default driving logic when invoked.super.drive()
.
You might be thinking that this is very similar to how the adapter pattern works and you wouldn't be wrong. Design patterns often have a lot of similarities, but once you figure out what each one is intended to accomplish and why you should use one, they become much more intuitive.
In this example, the adapter pattern focuses on making seemingly incompatible interfaces (ie car and rough road) work together, while the decorator aims to enrich the functionality of an existing base class.
facade design pattern
We won't go into too much detail about the facade pattern, but given how common it is, it's worth mentioning. The main purpose of the facade pattern is to provide a simpler and more consistent interface for various related components.
Here's a helpful analogy: a car is a complex machine made up of hundreds of different parts working together. However, we don't directly interact with all of these parts, just a handful of parts that we can access from the driver's seat (eg pedals, wheels, buttons) that serve as a simplified interface for us to use the car.
We can apply the same principle to software design.
Imagine that you are developing a framework or library. Typically, you have dozens, hundreds, or even thousands of classes with complex structures and interactions.
Users of these libraries shouldn't have to worry about all this complexity, so consider defining a simplified API that they can use to do their work. This is the facade pattern in action.
Or imagine a monitor that needs to compute and display complex information from multiple data sources. Rather than exposing all of this to the UI layer, it would be much more elegant to define a facade to tie all of these dependencies together and provide the data in the right format to render right away.
The facade pattern would be a good solution for such situations.
What are behavioral design patterns?
Behavioral design patterns focus more on the algorithmic side of things. They provide a more granular way for objects to interact and relate to each other and for us as developers to understand the responsibilities of objects.
Observer design pattern
The observer pattern should be familiar to anyone who has worked with reactive libraries such as RxJava, LiveData, Kotlin Flow, or any reactive JavaScript library. Essentially, it describes a communication relationship between two objects where one of them resides.observable
and the other isobserver
.
This is very similar to the producer-consumer model, where the consumer actively waits for information to be received from the producer. Each time a new event or data is received, the consumer calls its predefined method to handle that event.
val observable: Flow<Int> = flow { while (true) { emit(Random.nextInt(0..1000)) delay(100) }}val ObserveJob = coroutineScope.launch {observable.collect { value -> println(" Tapferkeit recibido $valor") }}
In the example above, we create an asynchronousFlow
which generates a random integer value once per second. Then we collect themFlow
in a separate coroutine launched from our current scope, with a handler defined for each new event.
All we are doing in this example is printing out the values received fromFlow
🇧🇷 The sourceFlow
It starts playing the moment it is collected and continues indefinitely or until the collection routine is aborted.
by KotlinFlow
It's based on coroutines, which makes it a very powerful tool for managing concurrency and writing reactive systems, but there's a lot more to learn about it.different types of rivers from the official documentationsuch as hot or cold jets.
The observer pattern is a clean and intuitive way to write code reactively. Reactive programming was the way to implement modern app UIs on Android via LiveData. More recently, it gained popularity with the addition ofRx library familyand the introduction of asynchronous streams.
Strategy Design Pattern
The strategy pattern is useful when we have a family of related algorithms or solutions that we want to be interchangeable and we need to decide which ones to use at runtime.
We can do this by abstracting the algorithms through an interface that defines the signature of the algorithm and letting the actual implementations determine the algorithm used.
For example, imagine we have multiple validation steps for an object that contains user information. For this use case, we can define and apply the following interface:
fun interface validation rule { fun validation (input: user input): Boolean } class Email validation: validation rule { replace fun validation (input: user input) = validate email (input address. email) } email validation val = email validation ( ) Valid date validation: Validation rule = { UserInput -> validateDate(userinput.date) }userInput val = getUserInput()validationRules: List<ValidationRule > = listOf( emailValidation, dateValidation)val isValidInput = ValidationRules.all { rule -> rule .validate(user input) }
At runtime, each of the ValidationRules algorithms we added to the list will be executed.is a valid entry
it will only be true if allConfirm()
The methods were successful.
pay attention toFun
interface definition keyword. This tells the compiler that the following interface is functional, meaning that it has a single method, allowing you to conveniently implement it via anonymous functions, as we did for thedate validation
Linear.
Conclusion
In this article, we look at the types of Kotlin design patterns and understand when and how to use some of the most common design patterns in Kotlin. I hope the post clarified this topic for you and showed you how to apply these patterns in your projects.
Which patterns do you usually use in your projects and in which situations? Are there any other Kotlin theme patterns you'd like to know more about? If you have any queries, feel free to contact me via the comments section or minemedium blog🇧🇷 Thanks for reading.
protocol rocket: Complete visibility of your web and mobile apps
protocol rocketis a front-end application monitoring solution that allows you to reproduce issues as if they occur in your own browser. Rather than guessing why errors occur or asking users for screenshots and log snippets, LogRocket lets you rerun the session to quickly understand what went wrong. Works seamlessly with any application, regardless of framework, and has plugins to register additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and status, LogRocket logs console logs, JavaScript errors, stack traces, network requests/responses with headers and bodies, browser metadata, and custom logs. It also leverages the DOM to record the HTML and CSS code on the page and recreate pixel-perfect videos from even the most complex single-page and mobile apps.
protocol rocket: Instantly recreate issues in your Android apps.
protocol rocketis an Android monitoring solution that lets you instantly reproduce issues, prioritize errors, and understand the performance of your Android apps.
LogRocket also helps increase conversion rates and product usage by showing exactly how users are interacting with your app. LogRocket's product analytics capabilities reveal why users aren't completing a specific flow or embracing a new feature.
Start proactively monitoring your Android apps —Try LogRocket for free.
try for free.