Kotlin provides an interesting set of tools to extend the simple term “function”. Now we can use all that stuff JS developers talk about and even more.
Local/Extension/Infix-functions
In software development we quite often encounter situations when one and the same piece of code inside one function needs to be executed several times, but in different parts of it. Yes, you could just do the Extract Method, but it will not be the desired solution. Why elevate the inner logic of one function (which does not occur anywhere else) to the class level? Kotlin has the Local functions that might come in handy. Now you can declare inner(local) functions inside other functions and they will have access to the variables declared in the external context.
If by any chance you decide to extend the functionality of an existing and not final class with your own set of methods, you will need Extension functions to accomplish it. My first desire was to go and extend the String class with my is Empty() method, in order not to use the external call TextUtils.isEmpty(). It turned out that I was late and the developers of Kotlin had already thought about it and added a String.isNullOrEmpty() method which does exactly what I needed.
Now that we have Extension functions, there’s no need to create additional static helper-classes. Let’s try and figure out how it works. For instance, you always wanted to know the length of a circle for an arbitrary integer if it were the radius of this circle. In the world of Java developers, you’d need to create a Circle class, for instance, and the method getLength() in it, or a static class with a separate method. Not too nice and quite cumbersome. Now let’s see how Kotlin does it:
fun Int?.circleLength() = if (this != null) 2 * this * PI else 0; println(10.circleLength()) // prints 62.83185307179586 println(null.circleLength()) // prints 0
In the Android context I can provide the following simple example of Extension functions usage:
//this is how we inflate layout to view in Java LayoutInflater inflater = LayoutInflater.from(getContext()); view = inflater.inflate(R.layout.item_user, container, false); //and this is how we can do it in a smarter way in Kotlin //just by extending ViewGoup class with ‘inflate’ function fun ViewGroup.inflate(layoutId: Int, attachToRoot: Boolean = false): View { return LayoutInflater.from(context).inflate(layoutId, this, attachToRoot) } view = viewGroup.inflate(R.layout.item_user, false)If you wish to turn your function into an operation in terms of syntax, you can do it with the help of the key infix:
infix fun String.mix() = {...} //and now you can use your function in both ways //just as usual OR as a command "Hello " mix "World" "Hello ".mix("World")HOFs/Anonymous functions/Lamdas
Kotlin wouldn’t enter the world of Android development in such a bold fashion if apart from everything mentioned above (although it’s already more than enough, imho) it didn’t offer truly new benefits in comparison with Java 6/7 (the support of Java 8 has only recently been added and in a rather simplified form). And it did offer the following things:
- Higher Order functions(HOFs)
- Anonymous functions
- Lambdas
They are all tightly interconnected and in fact you can’t use HOF without using lambdas or anonymous functions. For those not familiar with the basics: HOF allow to accept other functions as parameters or return the functions as work result.
JS-developers use these things every other day, while doing the AJAX-request and transmitting anonymous functions as callbacks. In the very Python this has been implemented initially and is known by the term “decorators”. It is simple and convenient and not accessible to every Android developer.
If you have a function ready-and-waiting and you want to send it as a parameter you can do it by placing ‘::’ before its name. Moreover, in Kotlin 1.1 they added the ability to transfer the methods of a class instance in the same way. Let’s look into a simple example to see how it works. The task is the following: we have a list of lines and we need to select only those with even length. Our extension function— isEven() will help here.
fun Int?.isEven() : Boolean = this ?.rem(2) == 0 //just a dummy func used for an experiment fun check(str: String): Boolean = str.length.isEven() //@param validator - it’s a function that takes String param and returns Boolean value fun sortOutStrings(list: List, validator: (String) -> Boolean): List { val result = arrayListOf() for (item in list) if (validator(item)) result.add(item) return result }//sending reference of our check() function inside sortOutStrings()
println(sortOutStrings(listOf("A", "AB", "ABC"), ::check)) //prints [AB]Similarly, the filter() function in Collections receives lambda/function( the result of which is Boolean) to filter out the data and return the result collection.
In fact, this example still looks cumbersome, let’s try and simplify it using an anonymous function and a lambda:
//anonymous function
println(sortOutStrings(listOf("A", "AB", "ABC"), fun(str) = str.length.isEven())) //prints [AB]//lambda with the chain of other functions that process the result of each other
println(listOf("abc", "ab", "a").filter{!it.length.isEven()}.sortBy{it}.map{it.toUpperCase})//prints [A, ABC]//lambdas
println(sortOutStrings(listOf("A", "AB", "ABC"), {it.length.isEven()})) //prints [AB] println(sortOutStrings(listOf("A", "AB", "ABC")){it.length.isEven()}) //prints [AB]The last two lines of code are especially interesting – in fact it is the same structure, but the syntax is slightly different. It is the implicit name of a single parameter. If the last parameter in the call is a function, then its body can be defined outside of parentheses, immediately after them – in the curly brackets. And that’s why it’s great: in this way you can define blocks consisting of several functions, and this is implemented using the function literal with a specified receiver object. On the one hand, this is very similar to the extension functions, because we can call the methods of this object without any additional qualifiers, only now we can pass the set of methods of this object that we want to call. Consider an example with TextView:
inline fun textview(parent: ViewGroup, setup: TextView.() -> Unit): TextView { val view = TextView(parent.context) view.setup() parent.addView(view) return view }The textview() function takes a setup lambda as the last parameter with the explicitly specified type of receiver object – TextView, and you can use it like this:
textview(container) { layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) setText(R.string.app_name) // OR text = getString(R.string.app_name) setTextColor(Color.RED) textSize = 12 f; setOnClickListener { Toast.makeText(context, "That's Me!", Toast.LENGTH_LONG).show() } }It means that all the code that is inside the parentheses will be executed in the place where the call to view.setup() occurs. This approach is very convenient for implementing builders and tree structures. If you liked this approach, I advise you to look for a separate section in the documentation on Type-Safe Builders, there is an excellent example of building HTML-markup using the approach of function with receiver object.
In the description of the textview() function there is an inline keyword. It tells the compiler that the code for this function (ie its body) needs to be inserted directly into the place where this function is triggered in its pure form. Thus, we save memory (cause we omit creation of one more abstraction), we do not lose in performance, BUT the number of output code grows slightly. It should not be forgotten and, if possible, you should use inline as much as possible and take all benefit out of it. For example, extension functions for base types are implemented in this very approach.
Going back to lambdams, it is worth noting that in Kotlin 1.1 they have added destructuring support and the ability to skip unnecessary parameters with the underscore “_”.
//this is our distracted data class data class DistractedDataClass(val id: Int, val text: String, val weight: Int) //some threshold value we want filter data against val threshold = 6; val dummyList = listOf(DistractedDataClass(0, "Hello", 1), DistractedDataClass(1, "World", 2)) //we don’t need first parameter for inner logic of lambda, so missing it in lambda declaration val goodList = dummyList.filter { (_, txt, w) -> txt.length + w > threshold } if (goodList.size > 0) println("${goodList.get(0).toString()}") //prints “DistractedDataClass(id=1, text=World, weight=2)”