
class User(var name: String, var surname: String) val user = User("Marcin", "Moskała")
data class Student( val name: String, val surname: String, val age: Int )
class QuotationPresenter( private val view: QuotationView, private val repo: QuotationRepository ) { private var nextQuoteId = -1 fun onStart() { onNext() } fun onNext() { nextQuoteId = (nextQuoteId + 1) % repo.quotesNumber val quote = repo.getQuote(nextQuoteId) view.showQuote(quote) } }
QuotationPresenter has more properties than those declared in the primary constructor. Here, nextQuoteId is a property that is always initialized with the value -1. This is perfectly fine, especially when the initial state is set up using default values or primary constructor parameters.- the telescoping constructor pattern
- the builder pattern
class Pizza { val size: String val cheese: Int val olives: Int val bacon: Int constructor( size: String, cheese: Int, olives: Int, bacon: Int ) { this.size = size this.cheese = cheese this.olives = olives this.bacon = bacon } constructor( size: String, cheese: Int, olives: Int ) : this(size, cheese, olives, 0) constructor( size: String, cheese: Int ) : this(size, cheese, 0) constructor(size: String) : this(size, 0) }
class Pizza( val size: String, val cheese: Int = 0, val olives: Int = 0, val bacon: Int = 0 )
size and olives without mentioning other parameters:val myFavorite = Pizza("L", olives = 3)
val myFavorite = Pizza("L", olives = 3, cheese = 1)
- We can define a subset of parameters with the default arguments we want.
- We can provide arguments in any order.
- We can explicitly name arguments to make it clear what each value means.
val villagePizza = Pizza("L", 1, 2, 3)
val villagePizza = Pizza( size = "L", cheese = 1, olives = 2, bacon = 3 )
- name parameters,
- specify parameters in any order,
- have default values.
class Pizza private constructor( val size: String, val cheese: Int, val olives: Int, val bacon: Int ) { class Builder(private val size: String) { private var cheese: Int = 0 private var olives: Int = 0 private var bacon: Int = 0 fun setCheese(value: Int): Builder { cheese = value return this } fun setOlives(value: Int): Builder { olives = value return this } fun setBacon(value: Int): Builder { bacon = value return this } fun build() = Pizza(size, cheese, olives, bacon) } }
val myFavorite = Pizza.Builder("L").setOlives(3).build() val villagePizza = Pizza.Builder("L") .setCheese(1) .setOlives(2) .setBacon(3) .build()
val villagePizza = Pizza( size = "L", cheese = 1, olives = 2, bacon = 3 )
- Named parameters are shorter — a constructor or factory method with default arguments is much easier to implement than the builder pattern. It is a time-saver both for the developer who implements this code and for those who read it. This is a significant difference because implementation of the builder pattern can be time-consuming. Builder modifications are harder to apply. For instance, changing the name of a parameter requires not only changing the name of the function used to set it but also the name of the parameter in this function, the body of this function, the internal field used to keep it, the parameter name in the private constructor, etc.
- Named parameters are cleaner — when you want to see how an object is constructed, everything you need is in a single method instead of being scattered around a whole builder class. How are objects held? Do they interact? These are questions that are not so easy to answer when we have a big builder.
-
Named parameters offer simpler usage — the primary constructor is a built-in concept. The builder pattern is an artificial concept and therefore requires some knowledge about it. For instance, a developer can easily forget to call the
buildfunction (or, in other cases,create). - Named parameters have no problems with concurrence — this is a rare problem, but function parameters are always immutable in Kotlin, while properties in most builders are mutable. Therefore, it is harder to implement a thread-safe build function for a builder.
setPositiveButton, setNegativeButton, and addRoute), and they allow us to aggregate (addRoute):val dialog = AlertDialog.Builder(context) .setMessage(R.string.fire_missiles) .setPositiveButton(R.string.fire) { d, id -> // FIRE MISSILES! } .setNegativeButton(R.string.cancel) { d, id -> // User cancelled the dialog } .create() val router = Router.Builder() .addRoute(path = "/home", ::showHome) .addRoute(path = "/users", ::showUsers) .build()
val dialog = AlertDialog( context, message = R.string.fire_missiles, positiveButtonDescription = ButtonDescription(R.string.fire) { d, id -> // FIRE MISSILES! }, negativeButtonDescription = ButtonDescription(R.string.cancel) { d, id -> // User cancelled the dialog } ) val router = Router( routes = listOf( Route("/home", ::showHome), Route("/users", ::showUsers) ) )
val dialog = context.alert(R.string.fire_missiles) { positiveButton(R.string.fire) { // FIRE MISSILES! } negativeButton { // User cancelled the dialog } } val route = router { "/home" directsTo ::showHome "/users" directsTo ::showUsers }
fun Context.makeDefaultDialogBuilder() = AlertDialog.Builder(this) .setIcon(R.drawable.ic_dialog) .setTitle(R.string.dialog_title) .setOnCancelListener { it.cancel() }
copy to customize its properties, or creating this class using a function with optional parameters.data class DialogConfig( val icon: Int, val title: Int, val onCancelListener: (() -> Unit)?, //... ) val defaultDialogConfig = DialogConfig( icon = R.drawable.ic_dialog, title = R.string.dialog_title, onCancelListener = { it.cancel() }, //... ) // or fun defaultDialogConfig( val icon: Int = R.drawable.ic_dialog, val title: Int = R.string.dialog_title, val onCancelListener: (() -> Unit)? = { it.cancel() } ) = DialogConfig(icon, title, onCancelListener, /*...*/)
- to make code consistent with libraries written in other languages that used the Builder pattern,
- when we design an API to be easily used in other languages that do not support default arguments or DSLs.
[^34_2]: A presenter is a kind of object that is used in the Model View Presenter (MVP) architecture, which used to be popular in Android before its successor MVVM became popular.
[^34_3]: Dependency injection is a technique in which an object receives other objects that it depends on. By itself, it does not need any library (or framework) like Koin or Dagger, although I find them useful.