Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin on Android: Delegate with pleasure

Kotlin on Android: Delegate with pleasure

Slides from my talk at Android Christmas Meetup, 2017-12-26

Dmytro Zaitsev

December 26, 2017
Tweet

More Decks by Dmytro Zaitsev

Other Decks in Programming

Transcript

  1. External Can be implemented in any object-oriented language Two types

    of Delegation Internal Requires language support
  2. External Delegation class Rectangle(val width: Int, val height: Int) :

    Area { override fun area(): Int = width * height }
  3. External Delegation class Window(val rect: Rectangle) : Area { //

    Delegation override fun area(): Int = rect.area() }
  4. Translation rule public final class Window implements Area { //

    $FF: synthetic field private final Rectangle $$delegate_0 = new Rectangle(10, 10); public int area() { return this.$$delegate_0.area(); } }
  5. What properties would we like to have? ×Lazy (computed only

    upon first access) ×Observable (listeners get notified about changes) ×Storing in a map, not in separate field each ×<Whatever I want property semantics>
  6. When I should care? When you want to extend the

    behavior of classes that you cannot or don’t want to subclass because…
  7. Encapsulation, thou art a heartless bitch ×Classes are final (e.g.

    File) ×You offer a limited API, using 98% of the existing code ×You want to hide the implementation from calling code, so clients cannot cast to the superclass
  8. Val / Var: define an operator // For val operator

    fun getValue(thisRef: R, prop: KProperty<*>): T // For var operator fun getValue(thisRef: R, prop: KProperty<*>): T operator fun setValue(thisRef: R, prop: KProperty<*>, value: T) Property metadata Property owner
  9. Val / Var: or implement an interface // For val

    interface ReadOnlyProperty<in R, out T> { operator fun getValue(thisRef: R, prop: KProperty<*>): T } // For var interface ReadWriteProperty<in R, T> { operator fun getValue(thisRef: R, prop: KProperty<*>): T operator fun setValue(thisRef: R, prop: KProperty<*>, value: T) }
  10. Read Only Properties class Person { val name: String by

    NameVal() } class NameVal: ReadOnlyProperty<Person, String> { override fun getValue(thisRef: Person, prop: KProperty<*>): String { return "$thisRef, thank you for delegating '${prop.name}' to me!" } }
  11. Read-Write Properties class Person { var name: String by NameVar()

    } class NameVar: ReadWriteProperty<Person, String> { override fun getValue(…): { … } override fun setValue(thisRef: Person, prop: KProperty<*>, value: String) { println("$value has been assigned to '${prop.name} in $thisRef.'") } }
  12. This is generated by the compiler instead public final class

    Person { // $FF: synthetic field static final KProperty[] $$delegatedProperties = new KProperty[]{ /* Nightmare */ } @NotNull private final NameVar name$delegate = new NameVar(); @NotNull public final String getName() { return this.name$delegate.getValue(this, $$delegatedProperties[0]); } public final void setName(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.name$delegate.setValue(this, $$delegatedProperties[0], var1); } }
  13. observable class Person { var name by Delegates.observable("Dmytro") { prop:

    KProperty<*>, old, new -> println("$old -> $new") } }
  14. Map class Person(map: Map<String, Any?>) { val name: String by

    map val age: Int by map } class PersonWrapper(map: Map<String, Any?>) { val person: Person by map }
  15. class PersonActivity : Activity() { private lateinit var firstName: TextView

    private var lastName: TextView by Delegates.notNull() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) firstName = findViewById(R.id.first_name) as TextView lastName = findViewById(R.id.last_name) as TextView } }
  16. class PersonActivity : Activity() { private val firstName: TextView by

    bindView(R.id.first_name) private val lastName by bindView<TextView>(R.id.last_name) } fun <V : View> Activity.bindView(id: Int): V = lazy(LazyThreadSafetyMode.NONE) { findViewById(id) as V } Say `no` to annotation processing
  17. Just got bit by a Kotlin gotcha: using `by lazy`

    for view vals. It will still point to the old view instance after fragment view recreation. pic.twitter.com/gmjQwy0wjc — Chris Banes (@chrisbanes) August 14, 2017
  18. Butterknife/Kotterknife fun <V : View> bindView(id: Int): ReadOnlyProperty<Activity, V> {

    return object : ReadOnlyProperty<Activity, V> { private var value: Any? = EMPTY override fun getValue(thisRef: Activity, property: KProperty<*>): V { if (value == EMPTY) { value = thisRef.findViewById(id) as V? ?: throw IllegalStateException() } return value as V } } }
  19. Before class PersonApp : Application() { lateinit var component: AppComponent

    override fun onCreate() { super.onCreate() component = DaggerAppComponent.builder() .appModule(AppModule(this)) .build() } }
  20. After class PersonApp : Application() { val component by lazy(LazyThreadSafetyMode.NONE)

    { DaggerAppComponent.builder() .appModule(AppModule(this)) .build() } }
  21. class CircleView extends View { private long centerX = 200L;

    private long centerY = 300L; private int radius = 150; public void setCenterX(long centerX) { this.centerX = centerX; invalidate(); } public void setCenterY(long centerY) { this.centerY = centerY; invalidate(); } public void setRadius(int radius) { this.radius = radius; invalidate(); } }
  22. class CircleView(context: Context) : View(context) { var centerX: Long =

    200L set(value) { field = value invalidate() } var centerY: Long = 300L set(value) { field = value invalidate() } var radius: Int = 150 set(value) { field = value invalidate() } }
  23. class CircleView(context: Context) : View(context) { var centerX by bindValue(200L)

    var centerY by bindValue(300L) var radius by bindValue(150) } fun <T> View.bindValue(value: T) = Delegates.observable(value) { _, _, _ -> invalidate() }
  24. Type-safe stored values fun <T> SharedPreferences.bind(default: T? = null) =

    object : ReadOnlyProperty<Any, T?> { override fun getValue(thisRef: Any, prop: KProperty<*>): T? { return when (prop.returnType.jvmErasure) { String::class -> getString(prop.name, default as String) Int::class -> getInt(prop.name, default as Int) else -> TODO("Complete as you please...") } as T? } }
  25. fun <T> SharedPreferences.bind(default: T? = null) = object : ReadOnlyProperty<Any,

    T?> { override fun getValue(thisRef: Any, prop: KProperty<*>): T? { return when (prop.returnType.jvmErasure) { String::class -> getString(prop.name, default as String) Int::class -> getInt(prop.name, default as Int) else -> TODO("Complete as you please...") } as T? } } Kotlin-reflect REFLECTION!

  26. Easy fix inline fun <reified T> SharedPreferences.bind(default: T? = null)

    = object : ReadOnlyProperty<Any, T?> { override fun getValue(thisRef: Any, prop: KProperty<*>): T? { return when (T::class) { String::class -> getString(prop.name, default as String) Int::class -> getInt(prop.name, default as Int) else -> TODO("Complete as you please...") } as T? } }
  27. ×Read about Providing a delegate (since 1.1) ×Read about Local

    Delegated Properties (since 1.1) ×Build similar type-safe APIs on top of other unsafe APIs: Bundle, Intent, Cursor ×Bind resources: dimen, color, anim, drawable ×Take a look at DI frameworks without annotation processing: KOIN, injekt etc