-
Notifications
You must be signed in to change notification settings - Fork 99
4.) The Tricky Stuff
When you first see Kotlin properties in a class, your first reaction is probably: "I can get rid of all those private field/constructor/getters/setters".
Why did we need getters and setters? The idea was: encapsulation. The way Java made us implement this idea was verbose, but the idea itself is valuable.
Kotlin doesn't get rid of getters and setters. Instead, the compiler generates them for us!.
While in many cases we did not actually use them in any way, for the remaining cases that are complex, Kotlin has the following features:
- Custom getter methods
- Manipulating the backing field
- Built-in delegates
- Delegating to a map
- Custom delegates
A val
property can be backed by a getter method instead of private field.
val hello : String = "Hello at time ${System.currentTimeMillis()}!"
val hello: String
get() = "Hello at time ${System.currentTimeMillis()}!"
There is an important difference between those two properties.
The first is backed by a final field and will be initialized once and then always return the same value.
The second property is backed by a getter method and will deliver a different value each time.
It is also possible to define a custom setter
method where you manipulate the private field
generated by the compiler.
var name: String = ""
private set(value) {
field = value
println("Name was set to $value")
}
Calling a function when the field is changed is actually built-in in Delegates.observable
import kotlin.properties.Delegates
var name: String by Delegates.observable("") { property, oldValue, newValue ->
println("Property '${property.name}' was changed to $newValue")
}
fun main() {
name = "Jake"
//Prints: Property 'name' was changed to Jake
}
Delegates.vetoable
is another built-in
that allows to forbid (veto) to modify a property if some condition is not fulfilled.
lazy
lets you "defer" the instantiation of the class to first access. Meaning you define that "this will happen at some point", give it an initializer it will invoke, and the default behavior also uses a double-checked lock around the initializer for the sake of thread-safety.
val myView by lazy { view.findViewById(R.id.myView) }
Please note that this is actually not free because of the double-checked lock, so using it for view references like this would be fairly slow. An unsafe version would be preferred.
val myView by lazy(LazyThreadSafetyMode.NONE) {
view.findViewById(R.id.myView)
}
You are not limited to the built-in properties delegate.
Library authors can define their own functions that return a ReadOnlyProperty
or ReadWriteProperty
.
This allows you for example to abstract away how things are stored in your SharedPreferences.
interface CurrentUser {
var userId: String
var fullName: String
}
// Implementation on Android with the kotlin-jetpack library
// https://github.com/nsk-mironov/kotlin-jetpack#preferences-bindings
class CurrentUserSharedPrefs(
val preferences: SharedPreferences
): CurrentUser {
override var userId by preferences.bindPreference("", "user_id")
override var fullName by preferences.bindPreference("", "fullName")
}
Note that each comes at a cost because a ReadOnlyProperty
is used for each, so use it when it makes sense.
Once you are using a delegate, wrapping it with another delegate is tricky.
One common use case is storing the values of properties in a map. This comes up often in applications like parsing JSON or doing other “dynamic” things.
Delegating to a map avoid the annoying use of magic strings and casting and is optimized by the compiler!
class User(
val map: Map<String, Any?>
) {
val name: String by map
val age: Int by map
}
val user = User(convertSomeJsonToAMap())
println(user.name) // Prints "John Doe"
println(user.age) // Prints 25
// Instead of
println(user.map["name"] as String)
println(user.map["age"] as Int)
Beware though that in both case a map is being used behind the scene.
Your code will crash if your map contains no name
. You may want to wrap your map with map.withDefault { ... }
In Kotlin, it's possible to pass over implementations of a given interface to a value that is passed in to the class via its constructor.
Suppose you use an API to fetch a list of users. Your backend developer has wrapped the actual list in an object to be more flexible:
{
"users" : [
{ "id": 1, "name": "Jake"}
]
}
But really it's a list of users. Here is a possible trick you can use to use the inner list directly:
data class GetUsersResponse(
val users: List<UserDTO>
) : List<UserDTO> by users
data class UserDTO(val id: Int, val name: String)
// usage
val users: GetUsersResponse = api.fetchUsersSync()
println(users.first().name) // INSTEAD OF: users.users.first().name
JVM annotations: @JvmOverloads
, @JvmField
, @JvmStatic
, @JvmSuppressWildcards
, @get:
and @set:
and @field:
It's VERY important to know about the @Jvm*
annotations, which manipulates what kind of Java code will be available as a result of having written your Kotlin code.
For example, Dagger requires @JvmSuppressWildCards
to generate Map<? extends Class<?>, Provider<ViewModel>>
instead of Provider<? extends ViewModel>
-
Parcelable.CREATOR needs to be a public static final
field, so we must apply @JvmField
inside the companion object {
.
If we don't want to call MyObject.INSTANCE.someMethod()
to access a method inside an object
, we need to use @JvmStatic
. It's also required for binding adapters (data-binding) and static provider methods (Dagger modules).
@JvmOverloads
can be used to generate multiple constructors from a Kotlin constructor that has multiple arguments. However, it should generally NOT be used for Android View constructors (even if your IDE says so).
@get:
and @set:
and @field:
lets you define what an annotation is applied to when you put it on a property.
If you use Java's annotation processing with Kotlin on classes written in Kotlin, then you HAVE TO apply kotlin-kapt
plugin.
You don't really get any real errors beyond "things not working" if you forget.
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'