If you’re here, then you’ve probably heard of Kotlin before. In this article, we will review what Kotlin is, why it’s useful, and what design patterns are.
Then, we will take a 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:
The people at JetBrains created the Kotlin programming language to make their lives easier. They had been 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 makes use of functional programming, drastically reducing the quantity of boilerplate code. It helps increase the productivity of programmers and provides a modern way of developing native apps.
Nowadays, Kotlin is widely preferred over Java for Android development.
Software development involves a lot of creativity and thinking outside the box, since you’re often faced with complex problems which require unique solutions. But reinventing the wheel every single time you’re trying to solve something is not feasible — and not necessary.
In the area of software design, you will often face situations that others have faced before. Luckily enough, we now have some reusable solutions for these recurring problems.
These paradigms have been used and tested before. All that’s left to do is to understand the problem clearly to identify a suitable design pattern that fits your needs.
There are three main types of design patterns: creational, structural, and behavioral. Each of these categories answers a different question about our classes and how we use them.
In the next few sections, we are going to cover each design pattern in depth and understand how to use Kotlin features to implement these patterns. We will also review the most popular design patterns from each category: we will define them, cover some example use cases, and look at some code.
As the name suggests, patterns from this category focus on how objects are being created. Using such patterns will ensure that our code is flexible and reusable.
There are multiple creational design patterns in Kotlin, but in this section, we will focus on covering the factory method, the abstract factory method (you can also refer to Microsoft’s provider model), the singleton pattern, and the builder pattern.
These two creational patterns have some similarities, but they are implemented differently and have distinct use cases.
The factory method pattern is based on defining abstract methods for the initialization steps of a superclass. This Kotlin design pattern allows the individual subclasses to define the way they are initialized and created.
In comparison to other creational patterns such as the builder pattern, the factory method pattern doesn’t require writing a separate class, but only one or more additional methods containing the initialization logic.
A clear example of this pattern is the well-known RecyclerView.Adapter
class from Android, specifically its onCreateViewHolder()
method. This method instantiates a new ViewHolder
based on the content of the specific list element, as demonstrated below:
class MyRecyclerViewAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { 0 -> HeaderViewHolder() 1 -> SeparatorViewHolder() else -> ContentViewHolder() } } }
In this example, the onCreateViewHolder()
method is defined on the RecyclerView.Adapter
superclass, which is in turn used by the inner
code of the adapter.
By making the method abstract in the superclass, the adapter allows its implementing subclasses to define the initialization logic for their ViewHolders based on their needs.
On the other hand, the abstract factory method in Kotlin — which, like Microsoft’s provider model, uses a ProviderBase
class — relies on creating an interface that allows a family of closely related classes to be instantiated.
An example would be a factory that makes car parts for different manufacturers:
abstract class CarPartsFactory { fun buildEngine(/* engine parts */): Engine fun buildChassis(/* chassis materials */): Chassis } class CarPartsFactoryProvider { inline fun <reified M : Manufacturer> createFactory(): CarPartsFactory { return when (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 } // Usage: val factoryProvider = CarPartsFactoryProvider() val partsFactory = factoryProvider.createFactory<Manufacturer.Toyota>() val engine = partsFactory.buildEngine()
In this example, the interface acts as a blueprint for what kind of car parts the independent factories should build and from what materials (arguments). These factories (subclasses) will then build the parts based on the requirements and processes specific to those manufacturers.
The singleton pattern is probably one of the best-known design patterns. Most developers that work with OOP have encountered this design pattern before. However, it’s also one of the most misused and misunderstood patterns out there. We’ll discuss why in more detail at the end of this section.
This design pattern enables the developer to define a class that will be instantiated only once in the entire project. Every single place where it is used will make use of the same instance, thus reducing memory usage and ensuring consistency.
Generally, we would use the singleton design pattern when dealing with multi-threaded applications (e.g. logging, caching), when it is important to ensure a reliable single source of truth.
By definition, a Singleton
class will only be instantiated once, either eagerly (when the class is loaded) or lazily (at the time it is first accessed).
The following example shows one of the simplest approaches to defining a Singleton
class in Java. We are looking at Java because it is quite a bit more expressive than Kotlin, which lets us understand the concepts that make the pattern work as 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 the private
constructor and the static getInstance()
method ensure that the class is directly responsible for when it’s instantiated and how it is accessed.
Now, let’s look at how to implement the singleton pattern in Kotlin:
// Kotlin implementation object Singleton { // class implementation }
In Kotlin, implementing a Singleton
object is a directly included feature that is thread-safe out of the box. The pattern is also instantiated at the time of the first access, similarly to the above Java implementation.
You can read more on Kotlin object expressions and declarations in the official docs. Note that while this feature seems similar to companion objects in Kotlin, companion objects can be thought of more as defining static fields and methods in Kotlin.
Singletons can have lots of use cases, including:
Not only is the singleton pattern widely misused and misunderstood, but on various occasions, it has even been called an anti-pattern. The reason for this is that most of its benefits can also be considered its biggest drawbacks.
For example, this design pattern makes it very easy to add a global state to applications, which is notoriously hard to maintain if the code base grows in size.
Additionally, the fact that it can be accessed anywhere, anytime makes it much harder to understand the dependency hierarchy of the system. Simply moving some classes around or swapping one implementation for another can become a serious hassle if there are lots of dependencies on singleton methods and fields.
Anyone that has worked with code bases with lots of global or static components understands why this is a problem.
Next, since the Singleton
class is directly responsible for creating, maintaining, and exposing its single state, this breaks the Single Responsibility Principle.
Finally, since there can only be one object instance of each Singleton
class during runtime, testing also becomes harder. The moment a different class relies on a field or method of the Singleton, the two components will be tightly coupled.
Because the Singleton
methods can’t be (easily) mocked or swapped for fake implementations, unit-testing the dependent class in complete isolation is not possible.
While it might seem attractive to define every high-level component as a singleton, this is highly discouraged. Sometimes the benefits can outweigh the drawbacks, so we should not overuse this pattern, and you should always make sure that both you and your team understand the implications.
The builder pattern is especially useful for classes that require complex initialization logic or have to be highly configurable in terms of their input parameters.
For example, if we were to implement a Car
class that takes all the different parts that make up a car as constructor arguments, the list of arguments for said constructor could be fairly long. Having certain arguments that are optional, while others are mandatory, just adds to the complexity.
In Java, this would mean writing a separate constructor method for each possible combination of input arguments, not to mention separate and tedious initialization logic if the car would also need to be assembled during instantiation. This is where the builder patterns come to the rescue.
When using a builder design pattern, we must define a Builder
class for the class in question (in our example, Car
), which will be responsible for taking all of the arguments separately and for actually instantiating the resulting object:
class Car(val engine: Engine, val turbine: Turbine?, val wheels: List<Wheel>, ...) { class Builder { private lateinit var engine: Engine private var turbine: Turbine? = null private val wheels: MutableList<Wheel> = mutableListOf() fun setEngine(engine: Engine): Builder { this.engine = engine return this } fun setTurbine(turbine: Turbine): Builder { this.turbine = turbine return this } fun addWheels(wheels: List<Wheel>): Builder { this.wheels.addAll(wheels) return this } fun build(): Car { require(engine.isInitialized) { "The car needs an engine" } require(wheels.size < 4) { "The car needs at least 4 wheels" } return Car(engine, turbine, wheels) } } }
As you can see, the Builder
is directly responsible for handling the initialization logic while also handling the different configurations (optional and mandatory arguments) and preconditions (minimum number of wheels).
Note that all configuration methods return the current instance of the builder. This is done to allow easily creating objects by chaining these methods in the
following manner:
val car = Car.Builder() .setEngine(...) .setTurbine(...) .addWheels(...) .build()
While the builder design pattern is a very powerful tool for languages such as Java, we see it used much less often in Kotlin because optional arguments already exist as a language feature. This way, the configurations and the initialization logic can all be handled directly with one constructor, like so:
data class Car(val engine: Engine, val turbine: Turbine? = null, val wheels: List<Wheel>) { init { /* Validation logic */ } }
This is fine for simple objects such as data classes
, where the complexity mostly comes from having optional arguments.
In the case of larger classes with more complex logic, it is highly encouraged to extract the initialization code into a separate class such as a Builder
.
This helps with maintaining the Single Responsibility Principle, as the class is no longer responsible for how it’s created and validated. It also allows the initialization logic to be unit tested more elegantly.
We need structural design patterns because we will be defining certain structures in our projects. We want to be able to correctly identify the relations between classes and objects to compose them in such a way as to simplify project structures.
In the sections below, we will cover the adapter design pattern and the decorator design pattern in detail, as well as briefly reviewing some use cases for the facade design pattern.
The name of this pattern hints at the very job that it accomplishes: it fills the gap between two incompatible interfaces and enables them to work together, much like an adapter that you’d use for using an American phone charger in a European outlet.
In software development, you’ll usually use an adapter when you need to convert some data into a different format . For example, the data that you receive from the backend may be needed in a different representation in your repository/for the UI.
An adapter is also useful when you need to accomplish an operation using two classes that are not compatible 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() { // whatever is needed to make the car work on water // ... might be easier to just use a boat instead startSwimming(adaptee) } } // Usage val standardCar: Car = Car(...) val waterCar: WaterVehicle = CarWaterAdapter(standardCar) waterCar.swim()
In the code above, we are essentially creating a wrapper over the standardCar
(i.e., the adaptee). By doing so, we adapt its functionality to a different context with which it would have been otherwise incompatible (for example, for a water race).
The decorator design pattern also falls into the category of structural patterns since it allows you to assign new behaviors to an existing class. Again, we accomplish this by creating a wrapper that will have the said behaviors.
It is worth noting here that the wrapper will only implement the methods that the wrapped object has already defined. When creating a wrapper that qualifies as a decorator you will add some behaviors to your class without altering its structure in any way.
class BasicCar { fun drive() { /* Move from A to B */ } } class OffroadCar : BasicCar { override fun drive() { initialiseDrivingMode() super.drive() } private fun initialiseDrivingMode() { /* Configure offroad driving mode */ } }
In the example above, we started with a basic car, which is fine in a city and on well-maintained roads. However, a car like this won’t help you too much when you’re in a forest and there’s snow everywhere.
You aren’t altering the basic function of the car (i.e., driving passengers from point A to point B). Instead, you are just enhancing its behavior — in other words, it can now take you from point A to point B on a difficult road while keeping you safe.
The key here is that we’re only doing some configuration logic, after which we’re still relying on the standard driving logic by calling super.drive()
.
You might be thinking that this is very similar to the way the adapter pattern worked — and you wouldn’t be wrong. Design patterns often have lots of similarities, but once you figure out what they each aim to achieve and why you would use any one of them, they become much more intuitive.
In this example, the emphasis of the adapter pattern is to make seemingly incompatible interfaces (i.e., the car and the difficult road) work together, while the decorator aims to enrich the functionality of an existing base class.
We’re not going to go into too much detail about the facade pattern, but given how common it is, it’s well worth mentioning it. The main goal of the facade pattern is to provide a simpler and more unified interface for a set of related components.
Here’s a helpful analogy: a car is a complex machine composed of hundreds of different parts that work together. However, we’re not directly interacting with all of those parts, but only with a handful of parts that we can access from the driver’s seat (e.g. pedals, wheels, buttons) that act as a simplified interface for us to use the car.
We can apply the same principle in software design.
Imagine you’re developing a framework or library. Usually, you’d have tens, hundreds, or even thousands of classes with complex structures and interactions.
The consumers of these libraries shouldn’t be concerned with all of this complexity, so you’d want to define a simplified API with which they can make use of your work. This is the facade pattern at work.
Or, imagine that a screen needs to calculate and display complex information coming from multiple data sources. Instead of exposing all of this to the UI layer, it would be much more elegant to define a facade to join all those dependencies and expose the data in the proper format for rendering it right away.
The facade pattern would be a good solution for such situations.
Behavioral design patterns are more focused on the algorithmic side of things. They provide a more precise way for objects to interact and relate to each other, as well as for us as developers to understand the objects’ responsibilities.
The observer pattern should be familiar to anyone that has worked with reactive-style libraries such as RxJava, LiveData, Kotlin Flow, or any reactive JavaScript libraries. It essentially describes a communication relationship between two objects where one of them is observable
and the other is the observer
.
This is very similar to the producer-consumer model, where the consumer is actively waiting for input to be received from the producer. Each time a new event or piece of data is received, the consumer will invoke its predefined method for handling the said event.
val observable: Flow<Int> = flow { while (true) { emit(Random.nextInt(0..1000)) delay(100) } } val observerJob = coroutineScope.launch { observable.collect { value -> println("Received value $value") } }
In the above example, we’ve created an asynchronous Flow
that emits a random integer value once every second. After this, we collected this Flow
in a separate coroutine launched from our current scope and defined a handler for each new event.
All we’re doing in this example is printing the values received by the Flow
. The source Flow
will start emitting the moment it’s collected and will continue indefinitely or until the collector’s coroutine is canceled.
Kotlin’s Flow
is based on coroutines, which make it a very powerful tool for concurrency handling and for writing reactive systems, but there’s much more to be learned about the different types of flows from the official documentation, such as hot or cold streams.
The observer pattern is a clean and intuitive way of writing code reactively. Reactive programming has been the go-to way to implement modern app UIs on Android via LiveData. More recently, it has gained popularity with the addition of the Rx family of libraries and the introduction of asynchronous flows.
The strategy pattern is useful when we have a family of related algorithms or solutions that should be interchangeable and we need to decide which one to use at runtime.
We can do this by abstracting the algorithms via an interface that defines the algorithm’s signature and allowing the actual implementations to determine the algorithm used.
For example, imagine that we have multiple validation steps for an object that contains user input. For this use case, we can define and apply the following interface:
fun interface ValidationRule { fun validate(input: UserInput): Boolean } class EmailValidation: ValidationRule { override fun validate(input: UserInput) = validateEmail(input.emailAddress) } val emailValidation = EmailValidation() val dateValidation: ValidationRule = { userInput -> validateDate(userInput.date) } val userInput = getUserInput() val validationRules: List<ValidationRule> = listOf(emailValidation, dateValidation) val isValidInput = validationRules.all { rule -> rule.validate(userInput) }
During runtime, each of the algorithms from the ValidationRules we’ve added to the list will be executed. isValidInput
will only be true if all the validate()
methods have been successful.
Take note of the fun
keyword from the interface’s definition. This tells the compiler that the following interface is a functional one, meaning that it has one and only one method, letting us conveniently implement it via anonymous functions as we did for the dateValidation
rule.
In this article, we went over types of Kotlin design patterns and understood when and how to use some of the most commonly used design patterns in Kotlin. I hope that the post cleared this topic up for you and showed you how to apply these patterns in your projects.
What patterns do you usually use in your projects, and in what situations? Are there any other Kotlin design patterns you’d like to learn more about? If you have any questions don’t be afraid to reach out in the comments section or on my Medium blog. Thank you for reading.
LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your Android apps — try LogRocket for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.