Tonnes of fun with Kotlin

Or lessons I learn every year

Michael Spitsin
ProAndroidDev
Published in
8 min readDec 29, 2020

--

Hi. The new year is coming and it is a great time to recap things that we learned and achieved during this year. I thought what could I write this time. And in 2020 we started to work remotely. We started to share our knowledge with colleagues through our internal chats, instead of just verbally showing some fun and tricky things. This allowed me to gather three of my personal findings and share them with you! I decided to share some interesting and fun stuff that I’ve learned from working with a Kotlin in 2020.

Small synopsis. Today we will know

  • When extensions are prioritized over member/virtual methods
  • Why it is better to have unique names of extensions,
  • When they are not resolved as static methods always,
  • Why it is hard to follow DRY principle with inline classes,
  • Oddities of Kotlin type system
  • How interface’s default methods are implemented

1. Extensions not always are static methods

Suppose we have a simple extension method:

fun String.andThat(): String {
return "$this and that"
}

If we open Kotlin Bytecode preview we will see that this method is resolved as a static function:

And if we will decompile it to java code, we will see the next:

So everything works as expected. Now let suppose we have a class with some logic inside and this extension as a private method as well:

To be honest for me this case always was not much different from the first one. The only difference is a scope of andThat extension since it will not be available outside the Printer class. But still, I thought that this method will be resolved as private static method in bytecode.

What was my amazement when I saw this:

But why? Why we can not write a static method? Maybe it is related somehow to the ability to access private members of the class from the extension method, and that’s why we can not use static? For instance:

But we can write in Java the next:

One more log in the fire will add this ability to rewrite that code in kotlin to be converted on what you want:

Which will give us the required result. 🤔

Summary

To be honest, I don’t know the exact reason for that, but I think it may be related somehow to the scoping of the extension methods and with strict rules of formulating analogous bytecode methods based on those methods. But that’s not a problem for me personally. Just a nice thing that now I will remember: Extension methods are not always converted to static functions!

2. Inline classes should be straightforward and simple or you highly risk to lose inline 🤔

Inline classes are a great feature of Kotlin 1.3, and they can provide more structured syntactic sugar without allocating new objects. But not always.

Yes, we all know the simple case in which inline class will not work and the instance of it will be created:

Yes, in order to work properly inside doThis global method, we will have to pass an instance of Base interface. Yes, Inline1 is inline class and may be avoided in the straightforward usages, but here we will have to wrap view: Any with instance of Inline1 since the view: Any does not implement Base.

Now let’s take a more complicated example. Suppose we have ViewPadding and ViewMargin inline classes. Both you can find here (ViewMargin) and here (ViewPadding). If we will look closely we will notice that both of those classes contain completely the same logic behind horizontal vertical and total properties:

As engineers who read about DRY we want to eliminate that common part and somehow reuse that logic between ViewPadding and ViewMargin so we write something like:

And now we have less duplicated code and we can use our ViewMargin and ViewPadding like this:

fun usage(view: View) {
view.padding.end = 5
view.padding.vertical = 10
}

Right?

WRONG!

Now if we will look at the decompiled code we will see next:

When we invoke padding.end = 5 everything works as expected, but whenever we call padding.vertical / padding.horizontal / padding.total the boxing will happen and a new instance of ViewPadding will be created.

Why is that? — you wonder.

The reason lies in the way of working with default methods of interfaces. To be honest, it is a pretty straightforward thing, but I never tried to find out how default methods work.

So, when you write ViewOffsets they will be converted into:

Do you see it? Default methods accept ViewOffsets instance as an argument, that’s why View is boxed into ViewPadding / ViewMargin instances. :(

Wait. But what if we will make vertical / horizontal / total properties as inline extension properties of ViewOffsets? For instance:

inline var ViewOffsets.horizontal: Int
get() = start + end
set(value) {
start = value
end = value
}

In that case our usage method after decompilation will look like:

Summary

Still, boxing is happening 😔 Probably would be nice from Kotlin team to introduce some kind of templates for keeping inline classes working but at the same time making code more reusable. But by now: don’t try to reduce duplication in inline classes. They are not ready for that right now.

3. Extensions name shadowing can be tricky and unpredictable

Suppose we have the next interface-class hierarchy:

interface A; 
interface B : A

abstract class C : A;
class D : C(), B;
class E : C(), B

fun <T> List<T>.add(item: T): List<T> =
toMutableList().apply { add(item) }

The extensions add works for immutable lists and creates a new mutable instance of that list and adds the item to it (remember, that we have add virtual/member method for MutableList). Now if we will formulate the list:

fun buildList(): List<C> {
val result = mutableListOf<C>()
result.add(D())
result.add(E())
return result
}

Everything will works as expected. We will receive the list of two items: D and E. But now let’s make things more complicated. If we will have to call the next buildList with true arg, what we have as an answer?

fun buildList(arg: Boolean): List<C> {
val result = mutableListOf<C>()
result.add(if (arg) D() else E())
return result
}

You may assume that logically we will have a single item list with D as the item of it. But the truth is …

… that the list will be empty!

If we think, we will understand that that is possible only in case if add extension will be called instead of the virtual add method. Because in that case new list will be formulated and returned and the old list which is result = mutableListOf<C>() will remain unchanged.

Moreover, if we will rename add extension to something else, like addImmutable, everything will work as expected. So the extension somehow gets more prioritized than the virtual method!

You know, what’s more interesting? Try to answer the next question: what is the result type of if (arg) D() else E()?

Initially, I thought that the answer is C since we adding that to the list of the type C. But that’s a false hope since the computation of types happens step by step and don’t look into the outer things. Since both D and E are C and B at the same time, it is hard to choose the result type because of type ambiguity. So the result type will be A as a common parent of C and B

So because the result type of if condition is A which means that we can not add that to the list of the type C. But apparently, we can.

Here is more strange and fun stuff here:

  • If we will rename the extension, everything will work
  • If we will write like that result.add(if (arg) D() as C else E()), everything will work, even if as C will be grayed out as not needed!
  • If we will write: if (arg) result.add(D()) else result.add(E()), everything will work
  • If we will remove the extension, everything will work
  • If we will remove the extension and will write this
    (if (arg) D() else E()).let { result.add(it) }
    We will receive the next compilation error:
    Kotlin: Type mismatch: inferred type is A but C was expected

Summary

I can not answer the question “Why extension was prioritized over the virtual method in that case”. I can not answer why the type system works in a way that the result.add(if (arg) D() else E()) will invoke the virtual method, but (if (arg) D() else E()).let { result.add(it) } will not (why both not). But the main lesson I learned here: better to not use the same names of extensions as for virtual methods if you have the same list of arguments.

Afterwords

I hope you enjoyed this small researches and findings as I did when I faced them and I hope you also learned something as I did :)

Kotlin is a great language and gives a lot of features. But with great power comes great responsibility. And we should remember about all those tricks and specialties in order to use the language 100% and keep our code clean and errorless at the same time 🤯

If you liked that article, don’t forget to support me by clapping and if you have any questions, comment me and let’s have a discussion. Happy coding!

Also, there are other articles, that can be interesting:

--

--

Love being creative to solve some problems with an simple and elegant ways