preface

When I saw the DSL recently, I thought that it could take advantage of some of Kotlin’s features to simplify the code, so let’s see how it works.

The body of the

It might be a bit jarring for those unfamiliar with Kotlin to start by saying the principles, so I’m going to go through them from the beginning.

convention

Kotlin’s convention is definitely something we use in our development, but we don’t pay close attention to it. The idea of a convention is to call a specially named function using a different and more concise notation than the normal method call syntax.

Two key points are extracted here, one for more concise symbolic calls and one for specially named functions. In plain English, it makes function calls simpler.

For example, the most familiar set and call [index] instead of get(index), let’s define our own class to implement this convention:

data class TestBean(val name: String.val age: Int){
    // The definition is very simple. Use operator to override the get method
    operator fun  get(index : Int): Any{
        return when(index) {
            0 -> name
            1 -> age
            else -> name
        }
    }

}
Copy the code

Then when we use:

// Here we can use [] instead of get to simplify calling methods
val testBean = TestBean("zyh".20)
testBean.get(0)
testBean[0]
Copy the code

Invoke conventions

As with the get convention above, [] is a cleaner way to call a GET method. Here’s the invoke convention, which causes an object to call a method like a function. Here’s a straightforward example:


data class TestBean(val name: String.val age: Int){

    // Override the invoke method
    operator fun invoke() : String{
        return "$name - $age"}}Copy the code

With the above code defined, let’s use it:

val testBean = TestBean("zyh".20)
// Normal call
testBean.invoke()
// A simplified call after the convention
testBean()
Copy the code

Here you’ll see that testBean objects can call the Invoke method normally, but testBean() can also call the Invoke method directly. This is what the Invoke convention does to make invoking the Invoke method easier.

Invoke convention and functional types

Now that we know about the Invoke convention, let’s combine it with lambda.

For those wondering about lambda, check out the article:

# Kotlin lambda has everything you need to know

We know that a function type is actually a class that implements the FunctionN interface, and then when the function type is a function type, we pass it a lambda, which is compiled into the anonymous inner class of FunctionN (non-inline, of course), The call to lambda then becomes an invoke call to the FunctionN interface.

Here’s an example code:

// Define the code
class TestInvoke {
    // High order function type variable
    private var mSingleListener: ((Int) -> Unit)? = null
    // Set variables
    public fun setSingleListener(listener:((Int) -> Unit)?){
        this.mSingleListener = listener
    }
    //
    fun testRun() {
        // InvokemSingleListener? .invoke(100)
        // Use the invoke convention without invoking
        if(mSingleListener ! =null){ mSingleListener!! (100)}}}Copy the code

After defining the above callback variable, we use this callback, since we know that higher-order functions are classes that implement the FunctionN interface, i.e.

// Notice that the interface method here is invoke
public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}
Copy the code

Then I can pass the argument directly using the following code:

val function1 = object: Function1<Int,Unit> {
    override fun invoke(p1: Int) {
        Logger.d("$p1")
    }
}
testInvoke.setSingleListener(function1)
Copy the code

This seems reasonable because in the testRun function we call invoke with 100 as an argument, and that 100 is then called back to function1, but what about when we pass lambda:

val testInvoke  = TestInvoke()
testInvoke.setSingleListener { returnInt ->
    Logger.d("$returnInt")}Copy the code

The above code passing a lambda has the same effect as passing an instance of a class, except that it is a block of code that does not invoke anything, so this is a feature. When a lambda is called as an argument to a function, it can be treated as an automatic call to invoke.

Invoke in DSL practice: Gradle dependency

The invoke dependency is a good use in some DSLS. Let’s look at the use of Gradle dependencies.

It is common to see the following code:

Dependencies {implementation 'androidx. Core: the core - KTX: 1.6.0' implementation 'androidx. Appcompat: appcompat: 1.3.1' / /... }Copy the code

This is something we’re used to, and it feels more like a configuration item than it does like code, and this is actually a piece of code, but in this style. How to implement this style? Let’s implement it briefly:

class DependencyHandler{
    / / compile library
    fun compile(libString: String){
        Logger.d("add $libString")}// Define the invoke method
    operator fun invoke(body: DependencyHandler.() -> Unit){
        body()
    }
}
Copy the code

After the above code is written, we can have the following three calls:

val dependency = DependencyHandler()
/ / calls to invoke
dependency.invoke {
    compile("Androidx. Core: the core - KTX: 1.6.0." ")}// Call directly
dependency.compile("Androidx. Core: the core - KTX: 1.6.0." ")
// take the receiver lambda way
dependency{
    compile("Androidx. Core: the core - KTX: 1.6.0." ")}Copy the code

The third method is the one common in Gradle configuration files. There are only two key points: define invoke and define a lambda with a receiver, leaving out this.

conclusion

In fact, the invoke convention and the method for writing lambdas with recipients are becoming more and more popular. For example, the anko library and now the compose library are both written declaratively in this way. After looking at the principle, you will find that it is actually quite convenient.

When I started working with Compose, I added another wave.