Superpowers of Kotlin delegation
@marcinmoskala
@marcinmoskala
@marcinmoskala
https://www.pinterest.de/explore/alter-computer/
@marcinmoskala
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
@marcinmoskala
Inheritance
interface Player {
fun playGame()
}
class RpgGamePlayer(val enemy: String) : Player {
override fun playGame() {
println("Killing $enemy")
}
}
class WitcherPlayer(enemy: String) : RpgGamePlayer(enemy)
RpgGamePlayer("monsters").playGame() // Prints: Killing monsters
WitcherPlayer("monsters").playGame() // Prints: Killing monsters
@marcinmoskala
Delegation pattern
@marcinmoskala
Inheritance breaks encapsulation
public class CountingHashSet 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
@marcinmoskala
When should we use delegation instead of inheritance?
When classes is final, private or not designed for inheritance.
When there is no "is-a" relationship
When we need only some functionalities from class
@marcinmoskala
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
@marcinmoskala
Kotlin Class Delegation
class WitcherPlayer() : Player by RpgGamePlayer("monsters")
class WitcherPlayer(enemy: String) : Player by RpgGamePlayer(enemy)
class WitcherPlayer(player: Player) : Player by player
class WitcherPlayer(val player: Player = RpgGamePlayer("monsters")) : Player by player
val player = RpgGamePlayer(10)
class WitcherPlayer() : Player by player
@marcinmoskala
Decorator pattern
InputStream fis = new FileInputStream("/someFile.gz");
InputStream bis = new BufferedInputStream(fis);
InputStream gis = new ZipInputStream(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
@marcinmoskala
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.
@marcinmoskala
@marcinmoskala
public class MainActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
// ...
}
public class MainPresenter implements Presenter {
@Inject NetworkApi networkApi;
@Inject UserService userService;
// ...
}
@Entity
@Table(name = "EMPLOYEE")
public class Employee {
@Id @GeneratedValue
@Column(name = "id")
private int id;
@Column(name = "first_name")
private String firstName;
@Column(name = "last_name")
private String lastName;
@Column(name = "salary")
private int salary;
}
private UserRepository userRepo;
public UserRepository getUserRepo() {
if(userRepo == null) {
userRepo = new UserRepository();
}
return userRepo;
}
private List<String> observableList;
public void setObservableList(List<String> observableList) {
this.observableList = observableList;
onObservableListChanged()
}
Need for property delegates
@marcinmoskala
Field vs Property
Field
Property
// Java
public String description;
// Kotlin
var description: String? = null
@marcinmoskala
Property
Property
// Java
private String description;
String getDescription() {
return description;
}
void setDescription(String description) {
this.description = description;
}
// Kotlin
var description: String? = null
Property
@marcinmoskala
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
}
Property
@marcinmoskala
Property
Property
// Java
String getDescription() {
return "Super presentation";
}
// Kotlin
val description: String? = null
get() = "Super presentation"
Property
@marcinmoskala
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
@marcinmoskala
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
}
Property delegation
@marcinmoskala
val someProperty by object {
operator fun getValue(thisRef: Any?, property: KProperty<*>) = "Something"
}
println(someProperty) // Prints: Something
object as a property delegate
@marcinmoskala
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
@marcinmoskala
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
}
}
Property delegation
@marcinmoskala
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
@marcinmoskala
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.)
notNull vs lateinit
@marcinmoskala
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
@marcinmoskala
class MainActivity : Activity() {
lateinit var questionLabelView: TextView
lateinit var answerLabelView: EditText
lateinit var confirmButtonView: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
questionLabelView = findViewById(R.id.main_question_label) as TextView
answerLabelView = findViewById(R.id.main_answer_label) as EditText
confirmButtonView = findViewById(R.id.main_button_confirm) as Button
}
}
lazy
@marcinmoskala
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
@marcinmoskala
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
@marcinmoskala
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
@marcinmoskala
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
}
}
lazy
@marcinmoskala
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
@marcinmoskala
var list: List<LocalDate> by observable(list) { _, old, new ->
if(new != old) notifyDataSetChanged()
}
private var drawerOpen by Delegates.observable(false) { _, _, opened ->
if (opened) drawerLayout.openDrawer(GravityCompat.START)
else drawerLayout.closeDrawer(GravityCompat.START)
}
var presenters: List<Presenter> by observable(list) { _, old, new ->
(old - new).forEach { it.onDestroy() }
(new - old).forEach { it.onCreate() }
}
observable
@marcinmoskala
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
}
}
vetoable
@marcinmoskala
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
}
}
Map as a property delegate
@marcinmoskala
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
@marcinmoskala
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()
initialized = true
}
return value as T
}
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
synchronized(this) {
this.value = value
initialized = true
}
}
}
mutable lazy
@marcinmoskala
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()
}
mutable lazy
@marcinmoskala
Source: spindrift-racing.com/jules-verne/drupal/en/log-book/icebergs-en
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
@marcinmoskala
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
@marcinmoskala
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
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()
}
@marcinmoskala
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)
}
}
View binding
@marcinmoskala
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
@marcinmoskala
https://github.com/MarcinMoskala/KotlinPreference
Preference binding
var SharedPreferences.canEatPie: Boolean by bindToPreferenceField(true, "CanEatPieKey")
var SharedPreferences.experience: Float? by bindToPreferenceFieldNullable()
var SharedPreferences.allPieInTheWorld: Long by bindToPreferenceField(0, "AllPieKey")
var SharedPreferences.savedGame: Game? by bindToPreferenceFieldNullable()
// Usage
stopDietButton.setOnClickListener {
preferences.canEatPie = true
}
startDietButton.setOnClickListener {
preferences.canEatPie = false
}
eatPieButton.setOnClickListener {
if(preferences.canEatPie) {
toast("Heah, pie :D")
} else {
toast("I cannot? :(")
}
}
@marcinmoskala
https://github.com/MarcinMoskala/PreferenceHolder
Preference binding
object Pref: PreferenceHolder() {
var canEatPie: Boolean by bindToPreferenceField(true, "CanEatPieKey")
var longList: Map<Int, Long> by bindToPreferenceField(mapOf(0 to 12L, 10 to 143L))
var savedGame: Game? by bindToPreferenceFieldNullable("GameKey")
}
// Usage
stopDietButton.setOnClickListener {
Pref.canEatPie = true
}
startDietButton.setOnClickListener {
Pref.canEatPie = false
}
eatPieButton.setOnClickListener {
if(Pref.canEatPie) {
toast("Heah, pie :D")
} else {
toast("I cannot? :(")
}
}
@marcinmoskala
What property delegates really introduced?
@marcinmoskala
override var email by bindToText(R.id.emailView)
Property delegation vs functions
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 { UserRepository() }
val emailView by lazy { findViewById(R.is.email_view) }
override fun getEmail(): String
= emailView.text.toString()
override fun setEmail(text: String) {
emailView.text = text
}
vs
vs
@marcinmoskala
override var email: String by bindToText(R.id.emailView)
email = "marcinmoskala@gmail.com"
if(email.matches(EMAIL_REGEX)) { /* ... */ }
interface MainView {
var email: String
}
Property delegation vs classes
val userRepo: LazyProperty<UserRepository>
= LazyProperty { UserRepository() }
userRepo.getValue().getUser()
val userRepo: UserRepository by lazy { UserRepository() }
userRepo.getUser()
override var email: BindToText = BindToText(R.id.emailView)
email.setValue("marcinmoskala@gmail.com")
if(email.getValue().matches(EMAIL_REGEX)) { /* ... */ }
interface MainView {
var email: BindToText
}
vs
vs
@marcinmoskala
http://kotlinacademy.org/
https://goo.gl/WKEb5G
About me
Marcin Moskała
marcinmoskala.com
blog.kotlin-academy.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.
- 859