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.