Kotlin is a modern programming language that compiles to Java bytecode. It is free and open source, and promises to make coding for Android even more fun.
In the previous article, you learned about packages and basic functions in Kotlin. Functions are at the heart of Kotlin, so in this post we'll look more closely at them. We'll be exploring the following kinds of functions in Kotlin:
- top-level functions
- lambda expressions or function literals
- anonymous functions
- local or nested functions
- infix functions
- member functions
You'll be amazed at all the cool things you can do with functions in Kotlin!
1. Top-Level Functions
Top-level functions are functions inside a Kotlin package that are defined outside of any class, object, or interface. This means that they are functions you call directly, without the need to create any object or call any class.
If you're a Java coder, you know that we typically create utility static methods inside helper classes. These helper classes don't really do anything—they don't have any state or instance methods, and they just act as a container for the static methods. A typical example is the Collections
class in the java.util
package and its static methods.
Top-level functions in Kotlin can be used as a replacement for the static utility methods inside helper classes we code in Java. Let's look at how to define a top-level function in Kotlin.
package com.chikekotlin.projectx.utils fun checkUserStatus(): String { return "online" }
In the code above, we defined a package com.chikekotlin.projectx.utils
inside a file called UserUtils.kt and also defined a top-level utility function called checkUserStatus()
inside this same package and file. For brevity's sake, this very simple function returns the string "online".
The next thing we'll do is to use this utility function in another package or file.
package com.chikekotlin.projectx.users import com.chikekotlin.projectx.utils.checkUserStatus if (checkUserStatus() == "online") { // do something }
In the preceding code, we imported the function into another package and then executed it! As you can see, we don't have to create an object or reference a class to call this function.
Java Interoperability
Given that Java doesn't support top-level functions, the Kotlin compiler behind the scenes will create a Java class, and the individual top-level functions will be converted to static methods. In our own case, the Java class generated was UserUtilsKt
with a static method checkUserStatus()
.
/* Java */ package com.chikekotlin.projectx.utils public class UserUtilsKt { public static String checkUserStatus() { return "online"; } }
This means that Java callers can simply call the method by referencing its generated class, just like for any other static method.
/* Java */ import com.chikekotlin.projectx.utils.UserUtilsKt ... UserUtilsKt.checkUserStatus()
Note that we can change the Java class name that the Kotlin compiler generates by using the @JvmName
annotation.
@file:JvmName("UserUtils") package com.chikekotlin.projectx.utils fun checkUserStatus(): String { return "online" }
In the code above, we applied the @JvmName
annotation and specified a class name UserUtils
for the generated file. Note also that this annotation is placed at the beginning of the Kotlin file, before the package definition.
It can be referenced from Java like this:
/* Java */ import com.chikekotlin.projectx.utils.UserUtils ... UserUtils.checkUserStatus()
2. Lambda Expressions
Lambda expressions (or function literals) are also not bound to any entity such as a class, object, or interface. They can be passed as arguments to other functions called higher-order functions (we'll discuss these more in the next post). A lambda expression represents just the block of a function, and using them reduces the noise in our code.
If you're a Java coder, you know that Java 8 and above provides support for lambda expressions. To use lambda expressions in a project that supports earlier Java versions such as Java 7, 6, or 5, we can use the popular Retrolambda library.
One of the awesome things about Kotlin is that lambda expressions are supported out of the box. Because lambda is not supported in Java 6 or 7, for Kotlin to interoperate with it, Kotlin creates a Java anonymous class behind the scene. But note that creating a lambda expression in Kotlin is quite different than it is in Java.
Here are the characteristics of a lambda expression in Kotlin:
- It must be surrounded by curly braces
{}
. - It doesn't have the
fun
keyword. - There is no access modifier (private, public or protected) because it doesn't belong to any class, object, or interface.
- It has no function name. In other words, it's anonymous.
- No return type is specified because it will be inferred by the compiler.
- Parameters are not surrounded by parentheses
()
.
And, what's more, we can assign a lambda expression to a variable and then execute it.
Creating Lambda Expressions
Let's now see some examples of lambda expressions. In the code below, we created a lambda expression without any parameters and assigned it a variable message
. We then executed the lambda expression by calling message()
.
val message = { println("Hey, Kotlin is really cool!") } message() // "Hey, Kotlin is really cool!"
Let's also see how to include parameters in a lambda expression.
val message = { myString: String -> println(myString) } message("I love Kotlin") // "I love Kotlin" message("How far?") // "How far?"
In the code above, we created a lambda expression with the parameter myString
, along with the parameter type String
. As you can see, in front of the parameter type, there is an arrow: this refers to the lambda body. In other words, this arrow separates the parameter list from the lambda body. To make it more concise, we can completely ignore the parameter type (already inferred by the compiler).
val message = { myString -> println(myString) } // will still compile
To have multiple parameters, we just separate them with a comma. And remember, we don't wrap the parameter list in parentheses like in Java.
val addNumbers = { number1: Int, number2: Int -> println("Adding $number1 and $number2") val result = number1 + number2 println("The result is $result") } addNumbers(1, 3)
However, note that if the parameter types can't be inferred, they must be specified explicitly (as in this example), otherwise the code won't compile.
Adding 1 and 3 The result is 4
Passing Lambdas to Functions
We can pass lambda expressions as parameters to functions: these are called "higher-order functions", because they are functions of functions. These kinds of functions can accept a lambda or an anonymous function as parameter: for example, the last()
collection function.
In the code below, we passed in a lambda expression to the last()
function. (If you want a refresher on collections in Kotlin, visit the third tutorial in this series) As the name says, it returns the last element in the list. last()
accepts a lambda expression as a parameter, and this expression in turn takes one argument of type String
. Its function body serves as a predicate to search within a subset of elements in the collection. That means that the lambda expression will decide which elements of the collection will be considered when looking for the last one.
val stringList: List<String> = listOf("in", "the", "club") print(stringList.last()) // will print "club" print(stringList.last({ s: String -> s.length == 3})) // will print "the"
Let's see how to make that last line of code above more readable.
stringList.last { s: String -> s.length == 3 } // will also compile and print "the"
The Kotlin compiler allows us to remove the function parentheses if the last argument in the function is a lambda expression. As you can observe in the code above, we were allowed to do this because the last and only argument passed to the last()
function is a lambda expression.
Furthermore, we can make it more concise by removing the parameter type.
stringList.last { s -> s.length == 3 } // will also compile print "the"
We don't need to specify the parameter type explicitly, because the parameter type is always the same as the collection element type. In the code above, we're calling last
on a list collection of String
objects, so the Kotlin compiler is smart enough to know that the parameter will also be a String
type.
The it
Argument Name
We can even simplify the lambda expression further again by replacing the lambda expression argument with the auto-generated default argument name it
.
stringList.last { it.length == 3 }
The it
argument name was auto-generated because last
can accept a lambda expression or an anonymous function (we'll get to that shortly) with only one argument, and its type can be inferred by the compiler.
Local Return in Lambda Expressions
Let's start with an example. In the code below, we pass a lambda expression to the foreach()
function invoked on the intList
collection. This function will loop through the collection and execute the lambda on each element in the list. If any element is divisible by 2, it will stop and return from the lambda.
fun surroundingFunction() { val intList = listOf(1, 2, 3, 4, 5) intList.forEach { if (it % 2 == 0) { return } } println("End of surroundingFunction()") } surroundingFunction() // nothing happened
Running the above code might not have given you the result you might have expected. This is because that return statement won't return from the lambda but instead from the containing function surroundingFunction()
! This means that the last code statement in the surroundingFunction()
won't execute.
// ... println("End of surroundingFunction()") // This won't execute // ...
To fix this problem, we need to tell it explicitly which function to return from by using a label or name tag.
fun surroundingFunction() { val intList = listOf(1, 2, 3, 4, 5) intList.forEach { if (it % 2 == 0) { return@forEach } } println("End of surroundingFunction()") // Now, it will execute } surroundingFunction() // print "End of surroundingFunction()"
In the updated code above, we specified the default tag @forEach
immediately after the return
keyword inside the lambda. We have now instructed the compiler to return from the lambda instead of the containing function surroundingFunction()
. Now the last statement of surroundingFunction()
will execute.
Note that we can also define our own label or name tag.
// ... intList.forEach myLabel@ { if (it % 2 == 0) { return@myLabel // ...
In the code above, we defined our custom label called myLabel@
and then specified it for the return
keyword. The @forEach
label generated by the compiler for the forEach
function is no longer available because we have defined our own.
However, you'll soon see how this local return problem can be solved without labels when we discuss anonymous functions in Kotlin shortly.
3. Member Functions
This kind of function is defined inside a class, object, or interface. Using member functions helps us to modularize our programs further. Let's now see how to create a member function.
class Circle { fun calculateArea(radius: Double): Double { require(radius > 0, { "Radius must be greater than 0" }) return Math.PI * Math.pow(radius, 2.0) } }
This code snippet shows a class Circle
(we'll discuss Kotlin classes in later posts) that has a member function calculateArea()
. This function takes a parameter radius
to calculate the area of a circle.
To invoke a member function, we use the name of the containing class or object instance with a dot, followed by the function name, passing any arguments if need be.
val circle = Circle() print(circle.calculateArea(4.5)) // will print "63.61725123519331"
4. Anonymous Functions
An anonymous function is another way to define a block of code that can be passed to a function. It is not bound to any identifier. Here are the characteristics of an anonymous function in Kotlin:
- has no name
- is created with the
fun
keyword - contains a function body
val stringList: List<String> = listOf("in", "the", "club") print(stringList.last{ it.length == 3}) // will print "the"
Because we passed a lambda to the last()
function above, we can't be explicit about the return type. To be explicit about the return type, we need to use an anonymous function instead.
val strLenThree = stringList.last( fun(string): Boolean { return string.length == 3 }) print(strLenThree) // will print "the"
In the above code, we have replaced the lambda expression with an anonymous function because we want to be explicit about the return type.
Towards the end of the lambda section in this tutorial, we used a label to specify which function to return from. Using an anonymous function instead of a lambda inside the forEach()
function solves this problem more simply. The return expression returns from the anonymous function and not from the surrounding one, which in our case is surroundingFunction()
.
fun surroundingFunction() { val intList = listOf(1, 2, 3, 4, 5) intList.forEach ( fun(number) { if (number % 2 == 0) { return } }) println("End of surroundingFunction()") // statement executed } surroundingFunction() // will print "End of surroundingFunction()"
5. Local or Nested Functions
To take program modularization further, Kotlin provides us with local functions—also known as nested functions. A local function is a function that is declared inside another function.
fun printCircumferenceAndArea(radius: Double): Unit { fun calCircumference(radius: Double): Double = (2 * Math.PI) * radius val circumference = "%.2f".format(calCircumference(radius)) fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0) val area = "%.2f".format(calArea(radius)) print("The circle circumference of $radius radius is $circumference and area is $area") } printCircumferenceAndArea(3.0) // The circle circumference of 3.0 radius is 18.85 and area is 28.27
As you can observe in the code snippet above, we have two single-line functions: calCircumference()
and calArea()
nested inside the printCircumferenceAndAread()
function. The nested functions can be called only from within the enclosing function and not outside. Again, the use of nested functions makes our program more modular and tidy.
We can make our local functions more concise by not explicitly passing parameters to them. This is possible because local functions have access to all parameters and variables of the enclosing function. Let's see that now in action:
fun printCircumferenceAndArea(radius: Double): Unit { fun calCircumference(): Double = (2 * Math.PI) * radius val circumference = "%.2f".format(calCircumference()) fun calArea(): Double = (Math.PI) * Math.pow(radius, 2.0) val area = "%.2f".format(calArea()) // ... }
As you can see, this updated code looks more readable and reduces the noise we had before. Though the enclosing function in this example given is small, in a larger enclosing function that can be broken down into smaller nested functions, this feature can really come in handy.
6. Infix Functions
The infix
notation allows us to easily call a one-argument member function or extension function. In addition to a function being one-argument, you must also define the function using the infix
modifier. To create an infix function, two parameters are involved. The first parameter is the target object, while the second parameter is just a single parameter passed to the function.
Creating an Infix Member Function
Let's look at how to create an infix function in a class. In the code example below, we created a Student
class with a mutable kotlinScore
instance field. We created an infix function by using the infix
modifier before the fun
keyword. As you can see below, we created an infix function addKotlinScore()
that takes a score and adds to the kotlinScore
instance field.
class Student { var kotlinScore = 0.0 infix fun addKotlinScore(score: Double): Unit { this.kotlinScore = kotlinScore + score } }
Calling an Infix Function
Let's also see how to invoke the infix function we have created. To call an infix function in Kotlin, we don't need to use the dot notation, and we don't need to wrap the parameter with parentheses.
val student = Student() student addKotlinScore 95.00 print(student.kotlinScore) // will print "95.0"
In the code above, we called the infix function, the target object is student
, and the double 95.00
is the parameter passed to the function.
Using infix functions wisely can make our code more expressive and clearer than the normal style. This is greatly appreciated when writing unit tests in Kotlin (we'll discuss testing in Kotlin in a future post).
"Chike" should startWith("ch") myList should contain(myElement) "Chike" should haveLength(5) myMap should haveKey(myKey)
The to
Infix Function
In Kotlin, we can make the creation of a Pair
instance more succinct by using the to
infix function instead of the Pair
constructor. (Behind the scenes, to
also creates a Pair
instance.) Note that the to
function is also an extension function (we'll discuss these more in the next post).
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Let's now compare the creation of a Pair
instance using both the to
infix function and directly using the Pair
constructor, which performs the same operation, and see which one is better.
val nigeriaCallingCodePair = 234 to "Nigeria" val nigeriaCallingCodePair2 = Pair(234, "Nigeria") // Same as above
As you can see in the code above, using the to
infix function is more concise than directly using the Pair
constructor to create a Pair
instance. Remember that using the to
infix function, 234
is the target object and the String
"Nigeria" is the parameter passed to the function. Moreover, note that we can also do this to create a Pair
type:
val nigeriaCallingCodePair3 = 234.to("Nigeria") // same as using 234 to "Nigeria"
In the Ranges and Collections post, we created a map collection in Kotlin by giving it a list of pairs—the first value being the key, and the second the value. Let's also compare the creation of a map by using both the to
infix function and the Pair
constructor to create the individual pairs.
val callingCodesMap: Map<Int, String> = mapOf(234 to "Nigeria", 1 to "USA", 233 to "Ghana")
In the code above, we created a comma-separated list of Pair
types using the to
infix function and passed them to the mapOf()
function. We can also create the same map by directly using the Pair
constructor for each pair.
val callingCodesPairMap: Map<Int, String> = mapOf(Pair(234, "Nigeria"), Pair(1, "USA"), Pair(233, "Ghana"))
As you can see again, sticking with the to
infix function has less noise than using the Pair
constructor.
Conclusion
In this tutorial, you learned about some of the cool things you can do with functions in Kotlin. We covered:
- top-level functions
- lambda expressions or function literals
- member functions
- anonymous functions
- local or nested functions
- infix functions
But that's not all! There is still more to learn about functions in Kotlin. So in the next post, you'll learn some advanced uses of functions, such as extension functions, higher-order functions, and closures. See you soon!
To learn more about the Kotlin language, I recommend visiting the Kotlin documentation. Or check out some of our other Android app development posts here on Envato Tuts+!
-
Android SDKHow to Use the Google Cloud Vision API in Android Apps
-
JavaAndroid Design Patterns: The Observer Pattern
-
Android SDKAdding Physics-Based Animations to Android Apps
-
Android SDKAndroid O: Phone Number Verification With SMS Tokens
-
Android SDKWhat Are Android Instant Apps?
No comments:
Post a Comment