Superpowers of Kotlin delegation
Delegation pattern
interface Player {
fun playGame()
}
class RpgGamePlayer(val enemy: String) : Player {
override fun playGame() {
println("Killing $enemy")
}
}
class WitcherPlayer(enemy: String) : Player {
val player = RpgGamePlayer(enemy)
override fun playGame() {
player.playGame()
}
}
RpgGamePlayer("monsters").playGame() // Prints: Killing monsters
WitcherPlayer("monsters").playGame() // Prints: Killing monsters
WitcherPlayer class is delegating methods defined in Player interface to an instance of type RpgGamePlayer
Delegation pattern
interface Player {
fun playGame()
}
class RpgGamePlayer(val enemy: String) : Player {
override fun playGame() {
println("Killing $enemy")
}
}
class WitcherPlayer() : RpgGamePlayer()
RpgGamePlayer("monsters").playGame() // Prints: Killing monsters
WitcherPlayer("monsters").playGame() // Prints: Killing monsters
Delegation pattern
Inheritance breaks encapsulation
public class CountingHashMap extends HashSet {
private int addCount = 0;
@Override
public boolean add(Object o) {
addCount += 1;
return super.add(o);
}
@Override
public boolean addAll(Collection collection) {
addCount += collection.size();
return super.addAll(collection);
}
public int getAddCount() {
return addCount;
}
}
// Usage
CountingHashMap s = new CountingHashMap();
s.addAll(Arrays.asList(new String[]{"A", "B", "C"}));
System.out.println(s.getAddCount()); // Prints: 6
Pros:
- Most classes are final, private or not designed for inheritance.
- We can inherit only from one superclass.
- We are abstracting away delegate behind an interface.
Cons
-
We need to create interfaces that specify which methods should be delegated.
-
We don’t have access to protected methods and properties.
- It is much easier to use inheritance in Java.
Delegation pros and cons
Kotlin Class Delegation
interface Player {
fun playGame()
}
class RpgGamePlayer(val enemy: String) : Player {
override fun playGame() {
println("Killing $enemy")
}
}
class WitcherPlayer(enemy: String) : Player by RpgGamePlayer(enemy)
RpgGamePlayer("monsters").playGame() // Prints: Killing monsters
WitcherPlayer("monsters").playGame() // Prints: Killing monsters
Using by keyword, we are informing compiler to delegate all methods defined in Player interface from WitcherPlayer to RpgGamePlayer
Kotlin Class Delegation
interface Player {
fun playGame()
}
interface GameMaker {
fun developGame()
}
class WitcherPlayer(val enemy: String) : Player {
override fun playGame() {
print("Killin $enemy! ")
}
}
class WitcherCreator(val gameName: String) : GameMaker{
override fun developGame() {
println("Makin $gameName! ")
}
}
class WitcherPassionate :
Player by WitcherPlayer("monsters"),
GameMaker by WitcherCreator("Witcher 3") {
fun fulfillYourDestiny() {
playGame()
developGame()
}
}
WitcherPassionate().fulfillYourDestiny() // Prints: Killin monsters! Makin Witcher 3
Kotlin Class Delegation
class WitcherPlayer(enemy: String) : Player by RpgGamePlayer(enemy)
class WitcherPlayer() : Player by RpgGamePlayer("monsters")
class WitcherPlayer(player: Player) : Player by player
class WitcherPlayer(val player: Player) : Player by player
class WitcherPlayer(player: Player = RpgGamePlayer("monsters")) : Player by player
class WitcherPlayer(enemy: String, player: Player = RpgGamePlayer(enemy)) : Player by player
val player = RpgGamePlayer(10)
class WitcherPlayer() : Player by player
Decorator pattern
InputStream fis = new FileInputStream("/someFile.gz");
InputStream bis = new BufferedInputStream(fis);
InputStream gis = new GZIPInputStream(bis);
ObjectInputStream ois = new ObjectInputStream(gis);
SomeObject someObject = (SomeObject) ois.readObject();
class PlayHard(player: Player) : Player by player {
override fun playGame() {
println("Arrgggh! ")
player.playGame()
}
}
// Usage
val player = WitcherPlayer("monsters")
player.playGame() // Prints: Killing monsters
val hardcore = PlayHard(player)
hardcore.playGame() // Prints: Arrgggh! Killing monsters
Property delegation
class User(val name: String, val surname: String)
var user: User by UserDelegate()
// Usage
println(user.name)
user = User(“Marcin”,”Moskala”)
Property user is delegated to object of type UserDelegate.
Field vs Property
Field
Property
// Java
public String description;
// Kotlin
var description: String? = null
Field vs Property
Property
Property
// Java
private String description;
String getDescription() {
return description;
}
void setDescription(String description) {
this.description = description;
}
// Kotlin
var description: String? = null
Field vs Property
Property
Property
// Java
private String description;
String getDescription() {
return Strings.trim(description);
}
void setDescription(String description) {
if(Strings.isNotNullOrEmpty(description))
this.description = description;
}
// Kotlin
var description: String? = null
get() = field?.trim()
set(value) {
if (value != null
&& value.isNotEmpty())
field = value
}
Field vs Property
Property
Property
// Java
String getDescription() {
return "Super presentation";
}
// Kotlin
val description: String? = null
get() = "Super presentation"
Property delegation
class User(val name: String, val surname: String)
var user: User by UserDelegate()
// Usage
println(user.name)
user = User(“Marcin”,”Moskala”)
Property user is delegated to object of type UserDelegate.
var p$delegate = UserDelegate()
var user: User
get() = p$delegate.getValue(this, this::user)
set(value) {
p$delegate.setValue(this, this::user, value)
}
Property delegation
data class User(val name: String, val surname: String)
class UserDelegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): User
= User("Marcin", "Moskala")
operator fun setValue(thisRef: Any?, property: KProperty<*>, user: User) {
println("I am not setting new user")
}
}
fun main(args: Array<String>) {
var user: User by UserDelegate()
println(user) // Prints: User(name=Marcin, surname=Moskala)
user = User("Michal", "Michalski") // Prints: I am not setting new user
}
object as a property delegate
val someProperty by object {
operator fun getValue(thisRef: Any?, property: KProperty<*>)
= "Something"
}
println(someProperty) // Prints: Something
Property delegation
var user$delegate = UserDelegate()
var user: User
get() = user$delegate.getValue(this, this::user)
set(value) {
user$delegate.setValue(this, this::user, value)
}
public interface ReadOnlyProperty<in R, out T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
}
public interface ReadWriteProperty<in R, T> {
public operator fun getValue(thisRef: R, property: KProperty<*>): T
public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
Specify allowed location
Specify allowed
type
Property delegation
var a by SomeDelegate() // Top-level property with delegate
fun someTopLevelFun() {
var b by SomeDelegate() // Local variable (inside top-level function) with delegate
}
class SomeClass() {
var c by SomeDelegate() // Member property with delegate
fun someMethod() {
val d by SomeDelegate() // Local variable (inside method) with delegate
}
}
notNull
var name: String by Delegates.notNull()
private class NotNullVar<T: Any>() : ReadWriteProperty<Any?, T> {
private var value: T? = null
public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return value ?: throw IllegalStateException(
"Property ${property.name} should be initialized before get."
)
}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
}
}
name = "Marcin"
println(name) // Prints: Marcin
name = "Maja"
println(name) // Prints: Maja
println(name) // Error:
// Property name should be
// initialized before get.
notNull
var user: User by Delegates.notNull()
lateinit var user: User
Can be used everywhere
Can be used to all types
More efficient
Cannot be used at top level
Cannot be used to primitive (Int, Long, Double etc.)
lazy
val userRepo by lazy { UserRepository() }
private var _userRepo: UserRepository? = null
private val userRepoLock = Any()
val userRepo: UserRepository
get() {
synchronized(userRepoLock) {
if (_userRepo == null) {
_userRepo = UserRepository()
}
return _userRepo!!
}
}
val userRepo by lazy(LazyThreadSafetyMode.NONE) { UserRepository() }
lazy
public class MainActivity extends Activity {
TextView questionLabelView;
EditText answerLabelView;
Button confirmButtonView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
questionLabelView = findViewById(R.id.main_question_label);
answerLabelView = findViewById(R.id.main_answer_label);
confirmButtonView = findViewById(R.id.main_button_confirm);
}
}
lazy
class MainActivity : Activity() {
var questionLabelView: TextView by lazy { findViewById(R.id.main_question_label) }
var answerLabelView: TextView by lazy { findViewById(R.id.main_answer_label) }
var confirmButtonView: Button by lazy { findViewById(R.id.main_button_confirm) }
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}
}
- Property is declared and initialized in single place
- Properties are not lateinint and not nullable
- Properties are read only
- The value will be resolved only when the property is accessed for the first time.
- Not used property will be marked by the compiler
lazy
class MainActivity : Activity() {
var questionLabelView: TextView by bindView(R.id.main_question_label)
var answerLabelView: TextView by bindView(R.id.main_answer_label)
var confirmButtonView: Button by bindView(R.id.main_button_confirm)
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
}
}
fun <T: View> Activity.bindView(viewId: Int) = lazy { findViewById<T>(viewId) }
lazy
class SettingsActivity : Activity() {
private val titleString by bindString(R.string.title)
private val blueColor by bindColor(R.color.blue)
private val doctor by extra<Doctor>(DOCTOR_KEY)
private val title by extraString(TITLE_KEY)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
}
}
fun <T> Activity.bindString(@IdRes id: Int): Lazy<T> = lazy { getString(id) }
fun <T> Activity.bindColour(@IdRes id: Int): Lazy<T> = lazy { getColour(id) }
fun <T : Parcelable> Activity.extra(key: String)
= lazy { intent.extras.getParcelable<T>(key) }
fun Activity.extraString(key: String) = lazy { intent.extras.getString(key) }
lazy
class MainActivity : Activity(), MainView {
private val doctor by extra<Doctor>(DOCTOR_KEY)
private val title by extraString(TITLE_KEY)
private val titleView by bindView<TextView>(R.string.title_view)
private val presenter by lazy { MainPresenter(this, doctor) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.settings_activity)
titleView.text = title
}
}
observable
var name: String by Delegates.observable("Empty"){ property, oldValue, newValue ->
println("$oldValue -> $newValue")
}
// Usage
name = "Martin" // Prints: Empty -> Martin
name = "Igor" // Prints: Martin -> Igor
name = "Igor" // Prints: Igor -> Igor
observable
var list: List<Int> by observable(emptyList()) { _, old, new ->
println("List changed from $old to $new")
}
// Usage
list += 1 // Prints: List changed from [] to [1]
list += 2 // Prints: List changed from [1] to [1, 2]
list -= 2 // Prints: List changed from [1, 2] to [1]
var list: MutableList<Int> by observable(mutableListOf()) { _, old, new ->
println("List changed from $old to $new")
}
// Usage
list.add(1) // Does not print
list = mutableListOf(2, 3) // Prints: List changed from [1] to [2, 3]
observable
var list: List<LocalDate> by observable(list) { _, old, new ->
if(new != old) notifyDataSetChanged()
}
var list: List<String> by Delegates.observable(emptyList()) { _, old, new ->
if(old != new) updateListView(new)
}
var presenters: List<Presenter> by observable(list) { _, old, new ->
(old - new).forEach { it.onDestroy() }
(new - old).forEach { it.onCreate() }
}
vetoable
var list: List<String> by Delegates.vetoable(emptyList()) { prop, old, new ->
if(new.size < 3) return@vetoable false
println(new)
true
}
// Usage
listVetoable = listOf("A", "B", "C") // Update A, B, C
println(listVetoable) // Prints: [A, B, C]
listVetoable = listOf("A") // Nothing happens
println(listVetoable) // Prints: [A, B, C]
listVetoable = listOf("A", "B", "C", "D", "E") // Prints: [A, B, C, D, E]
var name: String by Delegates.vetoable("") { prop, old, new ->
if (isValid(new)) {
showNewName(new)
true
} else {
showNameError()
false
}
}
Map as a property delegate
class User(map: Map<String, Any>) {
val name: String by map
val kotlinProgrammer: Boolean by map
}
// Usage
val map: Map<String, Any> = mapOf(
"name" to "Marcin",
"kotlinProgrammer" to true
)
val user = User(map)
println(user.name) // Prints: Marcin
println(user.kotlinProgrammer) // Prints: true
operator fun <V, V1: V> Map<String, V>.getValue(
thisRef: Any?,
property: KProperty<*>
): V1 {
val key = property.name
val value = get(key)
if (value == null && !containsKey(key)) {
throw NoSuchElementException("Key ${property.name} is missing in the map.")
} else {
return value as V1
}
}
class User(val map: MutableMap<String, Any>) {
var name: String by map
}
// Usage
val map = mutableMapOf("name" to "Marcin")
val user = User(map)
println(user.name) // Prints: Marcin
user.map.put("name", "Igor")
println(user.name) // Prints: Igor
user.name = "Michal"
println(user.name) // Prints: Michal
MutableMap as a property delegate
class User(var map: MutableMap<String, Any>) {
var name: String by map
}
// Usage
val user = User(mutableMapOf("name" to "Marcin"))
println(user.name) // Prints: Marcin
user.map = mutableMapOf("name" to "Michal")
println(user) // Prints: Marcin
MutableMap as a property delegate
class User(var map: MutableMap<String, Any>) {
var name$delegate = map
var name: String
get() = name$delegate.getValue(this, this::user)
set(value) {
name$delegate.setValue(this, this::user, value)
}
}
mutable lazy
fun <T> mutableLazy(initializer: () -> T): ReadWriteProperty<Any?, T>
= MutableLazy<T>(initializer)
private class MutableLazy<T>(val initializer: () -> T) : ReadWriteProperty<Any?, T> {
private var value: T? = null
private var initialized = false
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
synchronized(this) {
if (!initialized) {
value = initializer()
}
return value as T
}
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
synchronized(this) {
this.value = value
initialized = true
}
}
}
mutable lazy
var gameMode : GameMode by mutableLazy {
getDefaultGameMode()
}
var mapConfiguration : MapConfiguration by mutableLazy {
getSavedMapConfiguration()
}
var screenResolution : ScreenResolution by mutableLazy {
getOptimalScreenResolutionForDevice()
}
public class MainActivity: BaseActivity() {
@get:Arg var userData: String by argExtra()
}
Function literal as delegate
val a: Int get() = 1
val b: String get() = "KOKO"
val c: Int get() = 1 + 100
val a by { 1 }
val b by { "KOKO" }
val c by { 1 + 100 }
inline operator fun <R> (() -> R).getValue(
thisRef: Any?,
property: KProperty<*>
): R = invoke()
private val `a$delegate` = { 1 }
val a: Int get() = `a$delegate`()
private val `b$delegate` = { "KOKO" }
val b: String get() = `b$delegate`()
private val `c$delegate` = { 1 + 100 }
val c: Int get() = `c$delegate`()
val a: Int get() = 1
val b: String get() = "KOKO"
val c: Int get() = 1 + 100
https://www.pinterest.de/explore/alter-computer/
View binding
interface LoginView {
fun showProgress(show: Boolean)
fun getEmail(): String
fun setEmail(text: String)
fun getPassword(): String
fun setPassword(text: String)
fun setEmailError(id: Int?)
fun setPasswordError(id: Int?)
fun requestEmailFocus()
}
View binding
override fun getEmail(): String = emailView.text.toString()
override fun setEmail(text: String) {
emailView.text = text
}
override fun getPassword(): String = passwordView.text.toString()
override fun setPassword(text: String) {
passwordView.text = text
}
override fun setEmailError(id: Int?) {
emailView.setErrorId(id)
}
override fun setPasswordError(id: Int?) {
passwordView.setErrorId(id)
}
override fun requestEmailFocus() {
emailView.requestFocus()
}
override fun requestPasswordFocus() {
passwordView.requestFocus()
}
override fun showProgress(show: Boolean) {
progressView.visibility = if (show) View.VISIBLE else View.GONE
loginFormView.visibility = if (show) View.GONE else View.VISIBLE
}
View binding
interface LoginView {
fun showProgress(show: Boolean)
fun getEmail(): String
fun setEmail(text: String)
fun getPassword(): String
fun setPassword(text: String)
fun setEmailError(id: Int?)
fun setPasswordError(id: Int?)
fun requestEmailFocus()
fun requestPasswordFocus()
}
View binding
override var progressVisible by bindToLoading(R.id.progressView, R.id.loginFormView)
override var email by bindToText(R.id.emailView)
override var password by bindToText(R.id.passwordView)
override var emailErrorId by bindToErrorId(R.id.emailView)
override var passwordErrorId by bindToErrorId(R.id.passwordView)
override val emailRequestFocus by bindToRequestFocus(R.id.emailView)
override val passwordRequestFocus by bindToRequestFocus(R.id.passwordView)
https://github.com/MarcinMoskala/KotlinAndroidViewBindings
interface LoginView {
var progressVisible: Boolean
var email: String
var password: String
var emailErrorId: Int?
var passwordErrorId: Int?
val emailRequestFocus: () -> Unit
val passwordRequestFocus: () -> Unit
}
View binding
https://github.com/MarcinMoskala/KotlinAndroidViewBindings
fun Activity.bindToEditText(@IdRes editTextId: Int): ReadWriteProperty<Any?, String>
= bindToEditText { findViewById(editTextId) as EditText }
private fun bindToEditText(viewProvider: () -> EditText): ReadWriteProperty<Any?, String>
= EditTextViewTextBinding(lazy(viewProvider))
private class EditTextViewTextBinding(
lazyViewProvider: Lazy<EditText>
) : ReadWriteProperty<Any?, String> {
val view by lazyViewProvider
override fun getValue(thisRef: Any?, property: KProperty<*>): String {
return view.text.toString()
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
view.setText(value)
}
}
Preference binding
val CAN_EAT_PIE_KEY = "canEatPieKey"
val editor = preferences.edit();
editor.putBoolean(CAN_EAT_PIE_KEY, true);
editor.commit();
if(preferences.getBoolean(CAN_EAT_PIE_KEY, false)) {
// Code
}
val Context.preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(this)
preferences.canEatPie = true
if(preferences.canEatPie) {
// Code
}
Preference binding
preferences.canEatPie = true
if(preferences.canEatPie) {
// Code
}
var SharedPreferences.canEatPie: Boolean by bindToPreferenceField(true)
var SharedPreferences.experience: Float? by bindToPreferenceFieldNullable()
var SharedPreferences.savedGame: Game? by bindToPreferenceFieldNullable()
var SharedPreferences.allPieInTheWorld: Long by bindToPreferenceField(0, "AllPieKey")
var SharedPreferences.monstersKilled: Int? by bindToPreferenceFieldNullable("MonstersKilledKey")
https://github.com/MarcinMoskala/KotlinPreference
Preference binding
https://github.com/MarcinMoskala/KotlinPreference
var SharedPreferences.canEatPie: Boolean by bindToPreferenceField(true)
// Usage
stopDietButton.setOnClickListener {
preferences.canEatPie = true
}
startDietButton.setOnClickListener {
preferences.canEatPie = false
}
eatPieButton.setOnClickListener {
if(preferences.canEatPie) {
toast("Heah, pie :D")
} else {
toast("I cannot? :(")
}
}
Preference binding
object UserPref: PreferenceHolder() {
var canEatPie: Boolean by bindToPreferenceField(true)
var isMonsterKiller: Boolean? by bindToPreferenceFieldNullable()
var experience: Float? by bindToPropertyWithBackup(-1.0F)
var className: String? by bindToPropertyWithBackupNullable()
var savedGame: Game? by bindToPreferenceFieldNullable()
var longList: Map<Int, Long> by bindToPreferenceField(mapOf(0 to 12L, 10 to 143L))
var propTest: List<Character>? by bindToPropertyWithBackupNullable()
var elemTest: Set<Elems> by bindToPreferenceField(setOf(Elems.Elem1, Elems.Elem3))
}
https://github.com/MarcinMoskala/PreferenceHolder
Preference binding
https://github.com/MarcinMoskala/PreferenceHolder
object Pref: PreferenceHolder() {
var canEatPie: Boolean by bindToPreferenceField(true)
}
// Usage
stopDietButton.setOnClickListener { Pref.canEatPie = true }
startDietButton.setOnClickListener { Pref.canEatPie = false }
eatPieButton.setOnClickListener {
if(Pref.canEatPie) {
toast("Heah, pie :D")
} else {
toast("I cannot? :(")
}
}
Provide delegate
class A(val i: Int) {
operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>)
= object: ReadOnlyProperty<Any?, Int> {
override fun getValue( thisRef: Any?, property: KProperty<*>) = i
override fun setValue( thisRef: Any?, property: KProperty<*>, v: Int) {
println("No way, I am not changing $i to any other value")
}
}
}
val a by A(1)
// Usage
println(a) // Prints: 1
a = 2 // Prints: No way, I am not changing 1 to any other value
private val a$delegate = A(1).provideDelegate(this, this::prop)
val a: Int
get() = a$delegate.getValue(this, this::prop)
set(value) {
a$delegate.setValue(this, this::prop)
}
Provide delegate
public class MainActivity extends BaseActivity {
@Arg(optional = true) String name;
@Arg(optional = true) long id = -1;
@Arg char grade;
@Arg boolean passing;
}
https://github.com/MarcinMoskala/ActivityStarter
public class MainActivity: BaseActivity() {
@Arg(optional = true) var name: String? = null;
@Arg(optional = true) var id: Long = -1;
@Arg lateinit var grade: Char;
@Arg lateinit var passing: Boolean;
}
public class MainActivity: BaseActivity() {
@get:Arg(optional = true) var name: String by argExtra(defaultName)
@get:Arg(optional = true) val id: Int by argExtra(defaultId)
@get:Arg var grade: Char by argExtra()
@get:Arg val passing: Boolean by argExtra()
}
MainActivityStarter.start(context, grade, passing);
MainActivityStarter.start(context, name, grade, passing);
MainActivityStarter.start(context, id, grade, passing);
MainActivityStarter.start(context, name, id, grade, passing);
Provide delegate
val a: A? by argExtra()
https://github.com/MarcinMoskala/ActivityStarter
@get:Arg(optional = true) val a: A by argExtra()
fun <T> Activity.argExtra(default: T? = null) = BoundToArgValueDelegateProvider(default)
class BoundToArgValueDelegateProvider<T>(val default: T? = null) {
operator fun provideDelegate(
thisRef: Any?,
prop: KProperty<*>
): ReadWriteProperty<Any, T> {
val annotation = prop.getter.findAnnotation<Arg>()
when {
annotation == null -> throw Error(ErrorMessages.noAnnotation)
annotation.optional &&
!prop.returnType.isMarkedNullable &&
default == null -> throw Error(ErrorMessages.optionalValueNeeded)
}
return BoundToValueDelegate(default)
}
}
Source: spindrift-racing.com/jules-verne/drupal/en/log-book/icebergs-en
About me
Marcin Moskała
marcinmoskala.com
github.com/MarcinMoskala
@marcinmoskala
Senior Android Developer in Gamekit, writing articles, open-source, freelancing and consulting.
marcinmoskala@gmail.com
Superpowers of Kotlin delegation
By Marcin Moskala
Superpowers of Kotlin delegation
Delegation is one of the most inconspicuous Kotlin features. Property delegation is still pretty new feature in the programming world and most developers still use only small amount of possibilities it provides. On the other hand, class delegation is known for decades and it was promoted by big authorities. All it needed was language support, which was introduced in Kotlin. On this session, we are going to dive into possibilities that Kotlin delegation gives. Let's discover this features and understand its superpowers.
- 847