-
Notifications
You must be signed in to change notification settings - Fork 99
3.) The Cool Stuff
In Kotlin, you have a first-party support for "lambda types", which represent a method with a given set of arguments, and a given return type.
Of course, in Java, you could also attempt to mimic it with java.util.function.*
, but Kotlin's syntax is much more straightforward and easy to use. This might sound like a pitch, but just look at it:
class MyAdapter(
private var items: List<Item>,
private val clickListener: (Item) -> Unit
): RecyclerView.Adapter<MyAdapter.ViewHolder>() {
....
}
Where this is created as:
val adapter = MyAdapter(items) { item ->
doSomethingWith(item)
}
As the last "lambda" argument is passed "outside" the parentheses.
If we are passing multiple lambdas, it is preferred to use named arguments for both.
createDialog(title, description,
onPositiveClick = { yay() },
onNegativeClick = { nay() }
)
What is important to note is that interfaces defined in Java can be used with lambda types of Kotlin thanks to SAM conversion.
Another thing to know is that a single-argument lambda implicitly names its argument it
, but any name can be given to that variable.
If we have multiple variables but there are unused ones, we can use _
as a name for them.
someView.setOnClickListener { view -> // if unnamed, it is called `it`
// ...
}
someView.waitForMeasure { _, _, _ ->
// unused parameters can be named as `_`
}
Beware: Kotlin code can become very messy if it
is over-used. Pretend it is called x
. If it is hard to read without IDE support, don't use it
.
Also important: One more thing to note is that in a lambda expression in Kotlin, the last line's return value is returned as the return value, and return
is explicitly not required.
However if we still want to add a return (or we need to return
from a lambda above the last line), then we can use return@[caller]
, for example return@onClick
.
Any function of any class can be passed as a lambda using the function reference
operator.
fun acceptsLambda(lambda: () -> Unit) {
if(...) {
lambda() // same as `lambda.invoke()`
}
}
fun doSomething() {
}
fun execute() {
acceptsLambda(::doSomething)
}
One can use a typealias
to give a name to a given type.
What's nice is that one can also provide template parameters for typealiases.
For example, it can be used to name lambda parameters in case they are unclear.
typealias ItemClickListener = (Item) -> Unit
class MyAdapter(
private var items: List<Item>,
private val clickListener: ItemClickListener
): RecyclerView.Adapter<MyAdapter.ViewHolder>() {
....
}
Kotlin has a very powerful construct called a "lambda with receiver". This means that inside a given lambda, the parameter it's invoked on is seen as this
.
For example:
inline fun Realm.runTransaction(crossinline transaction: Realm.() -> Unit) {
val realm = this
realm.executeTransaction {
transaction(realm)
}
}
realm.runTransaction {
insertOrUpdate(Dog("Hello"))
insertOrUpdate(Cat("World"))
}
For more info on this, you can check out https://academy.realm.io/posts/kau-jake-wharton-testing-robots/
Extension functions are functions you define that lets you invoke functions on given classes as if they were functions of that given class.
This enables you with the ability to enhance classes without having to extend them or wrap them with custom classes - instead, define a function somewhere as a top-level extension function, and you can "magically" use this function anywhere (as long as you import it).
fun View.show() {
this.visibility = View.VISIBLE
}
fun View.hide() {
this.visibility = View.GONE
}
// myView.visibility = View.VISIBLE
myView.show()
You can also specify extension properties.
var View.isVisible: Boolean
get() = this.visibility == View.VISIBLE
set(value) {
if (value) {
show()
} else {
hide()
}
}
In Kotlin, this feature might be the most powerful feature, especially considering it respects generic bounds, too.
inline fun Array<Account>.forEachNonDemo(action: (Account) -> Unit) {
this.filter { !it.isDemo() }.forEach(action)
}
And it allows you to add numerous "utils" or "helper" functions to your class as if it was already part of it, without using inheritance to add new behavior.
fun View.objectAnimate() = ViewPropertyObjectAnimator.animate(this)
And just to show you a real example for what this looks like, this is what I use for the fade animation of views (this is actually what I was showing off as an example in this thread on Reddit about benefits of using Kotlin.)
fun View.animateFadeOut(duration: Long = 325, hideOnFinish: Boolean = true): Animator = run {
alpha = 1f
objectAnimate()
.alpha(0f)
.setDuration(duration)
.get()
.apply {
if (hideOnFinish) {
onFinish {
hide()
}
}
}
}
fun View.animateFadeIn(duration: Long = 325, showOnStart: Boolean = true): Animator = run {
alpha = 0f
objectAnimate()
.alpha(1f)
.setDuration(duration)
.get()
.apply {
if (showOnStart) {
onStart {
show()
}
}
}
}
So if you have any DateUtils
or StringUtils
or ContextUtils
class, it should probably be top-level extension functions.
Now that we know we can create extension functions and we can also apply generic bounds to them, and we also know about lambdas-with-receivers, now we can understand the standard library scoping functions and how they work.
First we have let
and run
, which can return a new type after performing the lambda:
inline fun <R, T> T.let(transform: (T) -> R): R = transform(this)
inline fun <R, T> T.run(transform: T.() -> R): R = transform(this)
Then we have also
and apply
, which return the current object once the lambda is executed:
inline fun <T> T.also(action: (T) -> Unit): T {
action(this)
return this
}
inline fun <T> T.apply(action: T.() -> Unit): T {
action(this)
return this
}
And we have one last odd-one-out called with
, which works like apply
except it is a top-level function instead of an extension function.
inline fun <T> with(t: T, action: T.() -> Unit) {
action(t)
}
If you've not really seen Kotlin before, then you might be thinking "wait WTF how are these useful", but trust me they make code much simpler once you know how to use them.
Here are a few noteworthy examples.
apply/also
apply
is often used when an object is being configured before it is assigned to a value (or returned from a function).
also
is used for the same thing, except it allows you to rename the variable you are operating on, instead of seeing it as this
.
For example,
fun okHttpClient() = OkHttpClient.Builder().apply {
setInterceptor(NetworkInterceptor())
baseUrl(BuildConfig.BASE_URL)
}.build()
Or
fragment.arguments = Bundle().also { bundle ->
bundle.putString("hello", "hello")
}
Imagine that you are writing Java. You need to create a local variable instead of writing the assignment directly.
Bundle bundle = new Bundle();
bundle.putString("hello", "hello");
fragment.setArguments(bundle);
With scoping functions, we can merge these operations together. This is especially nice if you're working with mutable lists that you are adding items into.
-
let
/run
let
lets you execute a block which can return a new type after its execution. run
lets you do the same thing, except you see the current object as this
.
This can be useful if you don't want to create a local variable but you're "mapping" your object into something else.
val domainObject = dao.findFirst<MyClass>().let { DomainObject(it) }
It is also fairly common to combine let
with the ?.
and ?:
safe-call / elvis operators.
run
is similar but when you want to use the current object as this
. Another nice trick with run
is that you can turn a multi-line function into a single-line expression, here is an example.
fun someFunction() {
val someValue = "beep" // some local variable you cannot avoid, or scoping functions would make hard to read
return result
}
We can turn this into:
fun someFunction() = run {
val someValue = "beep"
result
}
with
with
can be used for reducing the number of times we refer to the same variable over and over again.
For example,
with(recyclerView) {
adapter = MyAdapter()
layoutManager = LinearLayoutManager(this@MyActivity)
}
There are two more standard library functions that people actually often forget to mention, takeIf
and takeUnless
(in which case takeUnless
is actually just !=
for takeIf
).
inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? {
if(predicate(this)) {
return this
}
return null
}
Or with a when
statement (just to get used to using it):
inline fun <T> T.takeIf(predicate: (T) -> Boolean): T? = when {
predicate(this) -> this
else -> null
}
It might seem like an odd function, but it's actually very powerful in certain assignments, especially if you need to define a non-null default value that you'd prefer to ignore.
int someNumber = bundle.getInt("someNumber", -1);
if(someNumber == -1) {
// uninitialized!
}
As it is uninitialized, we might want to model it with null
instead.
Integer someNumber = bundle.getInt("someNumber", -1);
if(someNumber == -1) {
someNumber = null;
}
And we can turn this into a single-liner with Kotlin.
val someNumber = bundle.getInt("someNumber", -1).takeIf { it >= 0 }
And it gets better when we combine this with ?.let
.
val someText = bundle.getInt("someNumber", -1).takeIf { it >= 0 }?.let { "$it" }
And maybe even give it a default non-null value.
val someText = bundle.getInt("someNumber", -1).takeIf { it >= 0 }?.let { "$it" } ?: ""
Or throw an exception.
val someText = something.takeIf { condition } ?: throw IllegalArgumentException("$something is invalid")
In Kotlin, a recursive function that is "tail-recursive" can be forced to be optimized, in the sense that its real implementation will use a for-loop (iterative solution) instead of a recursive one. This means the function does not need to use the stack to store previous results and is therefore safe to use even for larger numbers.
When you write a recursive function, it's best to check if you can use tailrec
. If you can, then the compiler won't nag you about it.
tailrec fun <T: Activity> findActivity(context: Context): T {
if(context is Activity) {
return context as T
}
if(context is ContextWrapper) {
val baseContext = context.baseContext
return findActivity(baseContext)
}
throw IllegalArgumentException("The context does not contain an Activity reference!")
}
I've previously used inline
functions without explaining them; it means that the lambda we pass in will be inlined instead of creating an anonymous implementation for the given lambda type.
This pretty much means that inline lambdas don't have the cost of creating an anonymous instance for the interface, and that means it's cheaper as there is no actual object instance instantiation by calling a function with a lambda argument!
inline fun <T> doSomething(action: (T) -> Unit) {
// action is inlined instead of creating a new
// anonymous implementation / instance of the lambda interface
}
However, due to some trickery in how inline returns work, there are restrictions sometimes, which once we understand can actually still be quite handy.
1.) noinline
means that the argument won't be inlined even if it's an inline
function
2.) crossinline
means that the argument will be invoked inside another lambda that is not inlined.
Okay, you may ask "when are these useful?"
noinline
is useful if you need the lambda to be an instance, for example if it is actually being passed to a SAM-converted method.
crossinline
is typically useful if you're passing the lambdas as implementations to an object with multiple implementation. I'll show you my favorite example of this.
myEditText.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable) {
newValue = s.toString()
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
can be converted into
inline fun EditText.onTextChanged(crossinline eventHandler: (String) -> Unit) {
this.addTextChangedListener(object: TextWatcher {
override fun afterTextChanged(s: Editable) {
eventHandler(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
})
}
And now you can use it:
myEditText.onTextChanged {
newValue = it
}
So we had to add crossinline
to be able to call our lambda inside the object: TextWatcher
's functions. This actually happens quite often, as we can see in another example:
inline fun <reified T: ViewModel> AppCompatActivity.createViewModel(
crossinline factory: () -> T
): T = T::class.java.let { clazz ->
ViewModelProviders.of(this, object: ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if(modelClass == clazz) {
@Suppress("UNCHECKED_CAST")
return factory() as T
}
throw IllegalArgumentException("Unexpected argument: $modelClass")
}
}).get(clazz)
}
In which case we invoke the lambda inside the function of ViewModelProvider.Factory
. Now we can do:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = createViewModel { MyViewModel() }
}
a glance at the Collections API
We can use methods such as map
, filter
, groupBy
and so on.
Let me show you an actual use-case where I had a List<Event>
that needed to be converted to Map<Long, List<Event>>
where each event was separated into a separate list and grouped by their start date.
val calendar = Calendar.getInstance()
val dateEventMap = eventList.groupBy { event ->
calendar.timeInMillis = event.startTimeMillis
calendar.resetHoursMinutesSecondsMillis()
calendar.timeInMillis
}
It's worth knowing that if you catch yourself iterating a list, collecting items by key into a map; then you should be using groupBy
which does this for you in a single method call.
There are many such similar functions in the Collections API and I urge you to check them out.
In Java, there are times when you must pass a Class<T>
to a given method.
RealmQuery<MyObject> query = realm.where(MyObject.class);
In Kotlin though, this typically looks like
val query = realm.where(MyObject::class.java)
Which looks very tacky!
The good news is that we can define an inline fun <reified T>
method which behind the scenes passes the Class object for us, while we can invoke it as a regular extension function. This means that we can access T::class.java
if the template variable is reified
. I'll show you what I mean.
inline fun <reified T: RealmModel> Realm.where(): RealmQuery<T> = this.where(T::class.java)
And now you can use it like this:
val query = realm.where<MyObject>()
Or like this
val query: RealmQuery<MyObject> = realm.where()
Personally, whenever I see a ::class.java
, I wonder "can I make an inline fun <reified T>
for this?" and if I can, I make an extension function for it. In some cases you can't (typically if you need to pass the class to a constructor), but you should where you can.
I've been waiting for this, sealed class
in Kotlin is a very powerful construct. Although you could just say "enums on steroids".
This means that you can create regular classes (data class, object) that extend from a given base-class, and it is enforced that these are the only possible implementations of this base-class.
This is important because it means you can use exhaustive when
statements over them.
sealed class Message {
data class TextMessage(val text: String): Message()
data class PictureMessage(val image: ByteArray): Message()
}
and now we can actually use when
for a message
instance, and check its type. No more ugly instanceof
s, and no need for adding a Visitor
just to avoid instanceof
s; and no need to add an abstract fun
where you don't really need it.
val message = ...
when(message) {
is TextMessage -> println("${message.text}")
is PictureMessage -> sendToFriends(message.image)
}.safe()
Note that it's common to use object
in place of no-arg data class
even when using sealed classes, but it has a different toString()
and therefore I prefer the trick mentioned at the data classes
section.
Another interesting tidbit (although in my opinion, one of the most over-abused ones) is the ability to define infix
functions.
Infix means that if you have a function that takes only 1 argument, then you can replace the function with a different syntax. Check the example below.
val pair: Pair<String, Int> = name.to(age) // not infix
val pair: Pair<String, Int> = name to age // infix
Basically it lets you throw out the .__(__)
and call the function as "if it was a language construct / keyword". This is also how until
is implemented.
In the above example, I showed that to
exists to create Pair<S, T>
, and there's actually also a Triple<S, T, U>
.
What's really nice is that tuples such as Pair
or Triple
can be "destructured" by index.
You can actually destructure any data classes, because they all get component1
...componentN
methods, although that is generally not recommended. Primarily prefer destructuring only for classes meant to be used as tuples.
Here is how it looks.
val triple = Triple(name, age, birthdate)
val (name, age, birthdate) = triple
You may ask "hold on how is this useful?" but there are actual use-cases where creating a class with named arguments is excessive, and we could just use a tuple instead.
fun <T1, T2> Observable<T1>.combineWith(other: Observable<T2>): Observable<Pair<T1, T2>> =
Observable.combineLatest(this, other, BiFunction<T1, T2, Pair<T1, T2>> { t1, t2 -> t1 to t2 })
which now I can use as
firstObservable.combineWith(secondObservable)
.subscribeBy(onNext = { (firstValue, secondValue) ->
...
})
An interesting tidbit about destructuring is that it uses functions called operator fun componentN
.
If these are added as extension functions to a given class, then we gain the ability to destructure it.
// from Android-KTX at https://github.com/android/android-ktx/blob/136ba4cdb3b6ece7470cbddeaf6a168021a69a30/src/main/java/androidx/core/graphics/Color.kt
inline val @receiver:ColorInt Int.alpha get() = (this shr 24) and 0xff
inline val @receiver:ColorInt Int.red get() = (this shr 16) and 0xff
inline val @receiver:ColorInt Int.green get() = (this shr 8) and 0xff
inline val @receiver:ColorInt Int.blue get() = this and 0xff
inline operator fun @receiver:ColorInt Int.component1() = (this shr 24) and 0xff
inline operator fun @receiver:ColorInt Int.component2() = (this shr 16) and 0xff
inline operator fun @receiver:ColorInt Int.component3() = (this shr 8) and 0xff
inline operator fun @receiver:ColorInt Int.component4() = this and 0xff
// ------
val (alpha, red, blue, green) = Color.parseString("#2ABCBC")