Cool Kotlin 1.4 new features!!

Utkarsh Devgan SDE @ Paytm
5 min readNov 19, 2020

SAM conversions for Kotlin interfaces

Before Kotlin 1.4.0, you could apply SAM (Single Abstract Method) conversions only when working with Java methods and Java interfaces from Kotlin. From now on, you can use SAM conversions for Kotlin interfaces as well. To do so, mark a Kotlin interface explicitly as functional with the fun modifier.

SAM conversion applies if you pass a lambda as an argument when an interface with only one single abstract method is expected as a parameter. In this case, the compiler automatically converts the lambda to an instance of the class that implements the abstract member function.

fun interface IntPredicate {
fun accept(i: Int): Boolean
}
val isEven = IntPredicate { it % 2 == 0 }fun main() {
println(“Is 7 even? — ${isEven.accept(7)}”)
}

Mixing named and positional arguments

In Kotlin 1.3, when you called a function with named arguments, you had to place all the arguments without names (positional arguments) before the first named argument. For example, you could call f(1, y = 2), but you couldn't call f(x = 1, 2).

It was really annoying when all the arguments were in their correct positions but you wanted to specify a name for one argument in the middle. It was especially helpful for making absolutely clear which attribute a boolean or null value belongs to.

In Kotlin 1.4, there is no such limitation — you can now specify a name for an argument in the middle of a set of positional arguments. Moreover, you can mix positional and named arguments any way you like, as long as they remain in the correct order.

fun reformat(
str: String,
uppercaseFirstLetter: Boolean = true,
wordSeparator: Char = ' '
) {
// ...
}
//Function call with a named argument in the middle
reformat("This is a String!", uppercaseFirstLetter = false , '-')

Trailing comma

With Kotlin 1.4 you can now add a trailing comma in enumerations such as argument and parameter lists, when entries, and components of destructuring declarations. With a trailing comma, you can add new items and change their order without adding or removing commas.

This is especially helpful if you use multi-line syntax for parameters or values. After adding a trailing comma, you can then easily swap lines with parameters or values.

fun reformat(
str: String,
uppercaseFirstLetter: Boolean = true,
wordSeparator: Character = ‘ ‘, //trailing comma
) {
// …
}
val colors = listOf(
“red”,
“green”,
“blue”, //trailing comma
)

Callable reference improvements

Kotlin 1.4 supports more cases for using callable references:

  • References to functions with default argument values
  • Function references in Unit-returning functions
  • References that adapt based on the number of arguments in a function
  • Suspend conversion on callable references

References to functions with default argument values

Now you can use callable references to functions with default argument values. If the callable reference to the function foo takes no arguments, the default value 0 is used.

fun foo(i: Int = 0): String = “$i!”fun apply(func: () -> String): String = func()fun main() {
println(apply(::foo))
}

Previously, you had to write additional overloads for the function apply to use the default argument values.

// some new overload
fun applyInt(func: (Int) -> String): String = func(0)

Function references in Unit-returning functions

In Kotlin 1.4, you can use callable references to functions returning any type in Unit-returning functions. Before Kotlin 1.4, you could only use lambda arguments in this case. Now you can use both lambda arguments and callable references.

fun foo(f: () -> Unit) { }
fun returnsInt(): Int = 42
fun main() {
foo { returnsInt() } // this was the only way to do it before 1.4
foo(::returnsInt) // starting from 1.4, this also works
}

Using break and continue inside when expressions included in loops

In Kotlin 1.3, you could not use unqualified break and continue inside when expressions included in loops. The reason was that these keywords were reserved for possible fall-through behavior in when expressions.

That’s why if you wanted to use break and continue inside when expressions in loops, you had to label them, which became rather cumbersome.

fun test(xs: List<Int>) {
LOOP@for (x in xs) {
when (x) {
2 -> continue@LOOP
17 -> break@LOOP
else -> println(x)
}
}
}

In Kotlin 1.4, you can use break and continue without labels inside when expressions included in loops. They behave as expected by terminating the nearest enclosing loop or proceeding to its next step.

fun test(xs: List<Int>) {
for (x in xs) {
when (x) {
2 -> continue
17 -> break
else -> println(x)
}
}
}

Smart casts for a lambda’s last expression

In Kotlin 1.3, the last expression inside a lambda wasn’t smart cast unless you specified the expected type. Thus, in the following example, Kotlin 1.3 infers String? as the type of the result variable:

val result = run {
var str = currentValue()
if (str == null) {
str = “test”
}
str // the Kotlin compiler knows that str is not null here
}
// The type of ‘result’ is String? in Kotlin 1.3 and String in Kotlin 1.4

In Kotlin 1.4, thanks to the new inference algorithm, the last expression inside a lambda gets smart cast, and this new, more precise type is used to infer the resulting lambda type. Thus, the type of the result variable becomes String.

In Kotlin 1.3, you often needed to add explicit casts (either !! or type casts like as String) to make such cases work, and now these casts have become unnecessary.

Smart casts for callable references

In Kotlin 1.3, you couldn’t access a member reference of a smart cast type. Now in Kotlin 1.4 you can:

fun perform(animal: Animal) {
val kFunction: KFunction<*> = when (animal) {
is Cat -> animal::meow
is Dog -> animal::woof
}
kFunction.call()
}

You can use different member references animal::meow and animal::woof after the animal variable has been smart cast to specific types Cat and Dog. After type checks, you can access member references corresponding to subtypes.

Better inference for delegated properties

The type of a delegated property wasn’t taken into account while analyzing the delegate expression which follows the by keyword. For instance, the following code didn’t compile before, but now the compiler correctly infers the types of the old and new parameters as String?:

import kotlin.properties.Delegatesfun main() {
var prop: String? by Delegates.observable(null) { p, old, new ->
println(“$old → $new”)
}
prop = “abc”
prop = “xyz”
}

And that’s a wrap :)!!

--

--