By leveraging the Kotlin Language features of data classes, named parameters and default values, we can address the the majority of use cases that the Builder Pattern addresses for Java, but with much less code.

I saw a Tweet by Hannes Dorfmann asking if people still used the Builder Pattern in Kotlin. I realized that on the projects I’ve been working on lately, we don’t use it and I wanted to share why we don’t, and how you can do the same.

No more builder pattern here. With Kotlin we use named arguments and provide default values. So much cleaner 💯 https://t.co/vYoxdYTQZL — Sam Edwards (@HandstandSam) August 20, 2018

In this post we’ll be building a NetworkRequest object which holds the information needed to make an HTTP networking request.

Goals

Emulate the short, readable syntax of the Builder Pattern in Java, but without the boilerplate code.

Default values for optional arguments.

Validation of business rules during object creation.

We’ll walk through a few steps, each which leverages a different Kotlin language feature to help accomplish our goals.

Step 1 – Using a Kotlin Data Class

In Kotlin, required, non-null parameters can be achieved by requiring a val in the constructor. In our NetworkRequest object, all fields are required, except the body string which is nullable because of the ? we see in String? .

data class NetworkRequest( val url : String, val method: String, val headers : Map<String, String>, val body : String? )

Kotlin data classes give us a really clean, concise way to define objects that hold values and force immutable, non-null values. This simple data class is short, readable code, but it doesn’t provide default values or argument validation out of the box.

Step 2 – Providing Default Values

Kotlin allows us to provide default values for a parameter with the syntax you see below.

data class NetworkRequest( val url : String, val method: String = "GET", val headers : Map<String, String> = mapOf(), val body : String? = null )

All parameters have default values provided in the NetworkRequest object except for url . This means that the only parameter which is required is url . This means that if the default values are okay for us, then we don’t need to specify specific values. With this implementation, we can build instances of the NetworkRequest object, and only specify the required url parameter since default values are provided for the other parameters. Here is an example of creating a NetworkRequest object where the url is the only parameter specified:

NetworkRequest("http://localhost:8080")

As you have more arguments, it’s hard to tell which argument is which, especially if the fields are fairly similar. Some values could both be "" or null and you wouldn’t know which argument is what.

NetworkRequest( "http://localhost:8080", "GET", mapOf("Content-Type" to "application/json"), null )

Android Studio has tooling to show you parameter names, but these are not available in GitHub or other tools. The parameter name tooltips in Android Studio are AWESOME during development, but as soon as someone is doing a code review outside of Android Studio (like on GitHub), they won’t be able to tell which parameter is which. So, while this is nice during development, and slightly less code, I feel like it’s not a great experience for future developers on your project, so I recommend a coding style where you use named parameters when you can.

Step 3 – Using Named Parameters

Named parameters allow us to very easily see what each value is. Here are a couple of examples:

NetworkRequest(url = "http://localhost:8080") NetworkRequest( url = "http://localhost:8080", method = "GET", headers = mapOf("Content-Type" to "application/json"), body = null )

Named parameters are nice because they allow us to specify parameters in any order. These following two code examples create the exact same object even though url and headers are in a different order:

// `url` then `headers` NetworkRequest( url = "http://localhost:8080", headers = mapOf("Content-Type" to "application/json") ) // `headers` then `url` NetworkRequest( headers = mapOf("Content-Type" to "application/json"), url = "http://localhost:8080" )

Step 4 – Business Logic Validation

A major benefit of using something like the Builder Pattern is for argument and business logic validation. In our Kotlin Data Class, this can be done in the init {} block which is called immediately after the default constructor is invoked.

init { if (url.isEmpty()) { throw IllegalArgumentException("Invalid `url`") } if (method == "POST" && body == null) { throw IllegalArgumentException("`body` cannot be `null` for a `POST`") } }

Here is the full version of our data class with the init {} block which ensures we have a non-empty url and a body if the method is POST:

data class NetworkRequest( val url: String, val method: String = "GET", val headers: Map<String, String> = mapOf(), val body: String? = null ) { init { if (url.isEmpty()) { throw IllegalArgumentException("Invalid `url`") } if (method == "POST" && body == null) { throw IllegalArgumentException("`body` cannot be `null` for a `POST`") } } }

Decompiling Kotlin -> Java to See How it Works

If we decompile our Kotlin NetworkRequest data class to Java code, here are the resulting code blocks that are generated from the Kotlin compiler.

Variable definition

@NotNull private final String url; @NotNull private final String method; @NotNull private final Map headers; @Nullable private final String body;

Non-null checks and business logic validation.

public NetworkRequest(@NotNull String url, @NotNull String method, @NotNull Map headers, @Nullable String body) { Intrinsics.checkParameterIsNotNull(url, "url"); Intrinsics.checkParameterIsNotNull(method, "method"); Intrinsics.checkParameterIsNotNull(headers, "headers"); super(); this.url = url; this.method = method; this.headers = headers; this.body = body; CharSequence var5 = (CharSequence)this.url; if(var5.length() == 0) { throw (Throwable)(new IllegalArgumentException("Invalid `url`")); } else if(Intrinsics.areEqual(this.method, "POST") && this.body == null) { throw (Throwable)(new IllegalArgumentException("`body` cannot be `null` for a `POST`")); } }

Default Values of "GET" , an empty Map and a null body.

if((var5 & 2) != 0) { var2 = "GET"; } if((var5 & 4) != 0) { var3 = MapsKt.emptyMap(); } if((var5 & 8) != 0) { var4 = (String)null; }

Conclusion

Kotlin Data classes, named parameters and default values give us a concise way to build objects and perform validations without the boilerplate needed in Java. This allows us to meet all of our requirements, and keeps our code concise, and readable.

NOTE to Java Developers: If you still need to have Java code that will create these Kotlin objects that you are building, it can get ugly, so be aware of that if the person using your code is not using Kotlin. I’m assuming that you are 100% Kotlin, or the code that will call this is written in Kotlin.

I wanted to share this because coming from the Java world, these Kotlin language features are non-obvious to help in building objects. If you have any questions, feel free to reach out on Twitter at @HandstandSam.