The Builder Pattern is a popular design pattern that helps manage the construction of complex objects. This pattern is well-known from the classic Gang of Four design patterns book, but Kotlin provides some unique features that can modernize this approach. In this article, we will explore how to implement the Builder Pattern traditionally and how Kotlin’s language features can make it more efficient. If you’re new to Kotlin patterns, you might find it useful to read about Kotlin Annotations as well.
Why Use the Builder Pattern?
The Builder Pattern allows you to construct complex objects step-by-step, giving you the flexibility to add conditional statements, loops, and modify properties during the object creation process. This is especially useful when dealing with a large set of parameters, as it can prevent the need to pass a long list of arguments into a constructor. You can learn more about how Kotlin simplifies concurrent programming, which complements the usage of patterns like this.
Traditional Implementation of the Builder Pattern
Let’s start with a more engaging example. Suppose we have a class that represents a TravelPackage for an application, allowing users to customize their vacation packages:
class TravelPackage(
val destination: String,
val flights: List<Flight>,
val hotels: List<Hotel>,
val activities: List<Activity>,
val specialRequests: List<String>
)
class Flight(val airline: String, val departure: String, val arrival: String)
class Hotel(val name: String, val stars: Int)
class Activity(val name: String, val duration: Int)
In the traditional implementation, you might construct an instance of TravelPackage
directly by passing all the required parameters:
val travelPackage = TravelPackage(
destination = "Paris",
flights = listOf(Flight("Berlin", "New York", "Istanbul")),
hotels = listOf(Hotel("Pera Palas", 5)),
activities = listOf(Activity("Hagia Sophia Tour", 5)),
specialRequests = listOf("Vegan meals")
)
However, this approach can become unwieldy when dealing with many parameters or when you need to add conditions to the setup. This is where the Builder Pattern shines. To dive deeper into understanding mobile app architecture, you can explore this guide on mobile app architecture.
Implementing the Builder Pattern in Kotlin
We can refactor the above setup to use the Builder Pattern, which will let us build the TravelPackage
object in a more flexible manner:
class TravelPackageBuilder {
var destination: String = "Unknown"
private val flights = mutableListOf<Flight>()
private val hotels = mutableListOf<Hotel>()
private val activities = mutableListOf<Activity>()
private val specialRequests = mutableListOf<String>()
fun addFlight(flight: Flight) = apply { flights.add(flight) }
fun addHotel(hotel: Hotel) = apply { hotels.add(hotel) }
fun addActivity(activity: Activity) = apply { activities.add(activity) }
fun addSpecialRequest(request: String) = apply { specialRequests.add(request) }
fun build(): TravelPackage {
return TravelPackage(destination, flights, hotels, activities, specialRequests)
}
}
// Usage
val travelPackage = TravelPackageBuilder()
.apply { destination = "Istanbul" }
.addFlight(Flight("Berlin", "New York", "Istanbul"))
.addHotel(Hotel("Pera Palas", 5))
.addActivity(Activity("Hagia Sophia Tour", 2))
.addSpecialRequest("Vegan meals")
.build()
Benefits of Using the Builder Pattern
- Enables flexible configuration: You can add conditional logic, loops, and more to customize object creation.
- Keeps code readable and clean: Instead of passing many parameters directly to a constructor, you can configure them step-by-step.
Modernizing with Kotlin Features
Kotlin offers several features that can further enhance the traditional Builder Pattern, allowing for cleaner and more expressive code. If you’re exploring how to make your apps more scalable, check out this guide on scalable architecture in development.
Default and Named Arguments
If the goal is to provide default values or reorder arguments, Kotlin’s default arguments and named arguments can be a simpler alternative. More details on how these features work can be found in the official Kotlin documentation.
class TravelPackage(
val destination: String = "Unknown",
val flights: List<Flight> = emptyList(),
val hotels: List<Hotel> = emptyList(),
val activities: List<Activity> = emptyList(),
val specialRequests: List<String> = emptyList()
)
// Usage
val travelPackage = TravelPackage(
destination = "Istanbul",
flights = listOf(Flight("Berlin", "New York", "Istanbul")),
specialRequests = listOf("Vegan meals")
)
With named arguments, you can change the order at the call site without the need for a builder. This approach is also beneficial when creating configurations for other parts of your app, like advanced navigation in Jetpack Compose.
Scope Functions: run
and apply
Kotlin’s scope functions like run
, apply
, and also
enable you to configure objects flexibly. You can refer to this in-depth guide on Kotlin Coroutines for more on functional approaches.
val travelPackage = TravelPackage(
destination = "Istanbul",
flights = run {
val list = mutableListOf<Flight>()
if (userPrefersDirectFlights) list.add(Flight("Berlin", "New York", "Istanbul"))
list
}
)
Using Collection Builders
Kotlin’s standard library provides functions like buildList
that can replace manual list building. For a more extensive look at Kotlin collections, see the article on mastering Kotlin collections.
val travelPackage = TravelPackage(
destination = "Paris",
flights = buildList {
add(Flight("Berlin", "New York", "Paris"))
add(Flight("KLM", "Amsterdam", "Istanbul"))
},
specialRequests = buildList {
add("Vegan meals")
add("Extra luggage")
}
)
Leveraging DSLs with Lambdas and Receivers
Finally, you can make your Builder Pattern more Kotlin-esque by using lambdas with receivers to create a DSL-like structure. For more on Kotlin’s advanced features, read the Kotlin documentation on type-safe builders.
class TravelPackageBuilder {
var destination: String = "Unknown"
private val flights = mutableListOf<Flight>()
private val hotels = mutableListOf<Hotel>()
private val activities = mutableListOf<Activity>()
private val specialRequests = mutableListOf<String>()
fun addFlight(airline: String, departure: String, arrival: String) = apply {
flights.add(Flight(airline, departure, arrival))
}
fun addHotel(name: String, stars: Int) = apply { hotels.add(Hotel(name, stars)) }
fun addActivity(name: String, duration: Int) = apply { activities.add(Activity(name, duration)) }
fun addSpecialRequest(request: String) = apply { specialRequests.add(request) }
fun build(): TravelPackage = TravelPackage(destination, flights, hotels, activities, specialRequests)
}
fun travelPackage(block: TravelPackageBuilder.() -> Unit): TravelPackage {
return TravelPackageBuilder().apply(block).build()
}
// Usage
val travelPackage = travelPackage {
destination = "Istanbul"
addFlight("Berlin", "New York", "Istanbul")
addHotel("Pera Palas", 5)
addActivity("Hagia Sophia Tour", 2)
addSpecialRequest("Vegan meals")
}
This allows you to create instances of TravelPackage
using more readable and intuitive syntax. For additional details on crafting clean and expressive Kotlin code, refer to this guide on Kotlin’s null safety features.
In this article, we’ve explored how to implement and modernize the Builder Pattern in Kotlin. While the traditional pattern still has its place, Kotlin’s features like default arguments, scope functions, and lambdas with receivers can simplify or replace the need for a full builder class in many cases. The result is cleaner, more maintainable, and expressive code.
Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.