This article has been authorized to be reprinted by guolin_blog, an official wechat account.

What is DSL?

DSL, short for Domin Specific Language, refers to a computer language that focuses on an application domain, such as HTML for displaying web pages, SQL for database processing, and regular expressions for retrieving or replacing text. The opposite of a DSL is the GPL, which stands for General Purpose Language, or general-purpose programming Language, the familiar Java, C, Objective-C, and so on.

DSLS are classified into external DSL and internal DSL. An external DSL is a language that can be parsed independently, like SQL, which focuses on database operations; Internal DSLS are the apis exposed by a common language to perform specific tasks. They take advantage of the language’s own features to expose the APIS in special forms, such as Android Gradle and iOS dependency management component CocosPods. Gradle is based on Groovy, which is a common language. Gradle builds its own DSL based on Groovy’s syntax, so when configuring Gradle, you must follow Groovy’s syntax as well as Gradle’s DSL standard.

Android Gradle file

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.tanjiajun.androidgenericframework"
        minSdkVersion rootProject.minSdkVersion
        targetSdkVersion rootProject.targetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    androidExtensions {
        experimental = true
    }

    dataBinding {
        enabled = true
    }

    packagingOptions {
        pickFirst 'META-INF/kotlinx-io.kotlin_module'
        pickFirst 'META-INF/atomicfu.kotlin_module'
        pickFirst 'META-INF/kotlinx-coroutines-io.kotlin_module'}}Copy the code

IOS Podfile

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '10.0'

use_frameworks!



target 'GenericFrameworkForiOS' **do**

​    pod 'SnapKit'.'~ > 4.0'

**end**
Copy the code

Realize the principle of

Before this article focuses on Kotlin’s DSL, let’s take a look at the concepts and syntax.

Extension function

To declare an extension function, prefix it with a receiver Type, the type being extended, as shown in the following code

fun Activity.getViewModelFactory(a): ViewModelFactory =
        ViewModelFactory((applicationContext as AndroidGenericFrameworkApplication).userRepository)
Copy the code

Declare a getViewModelFactory function, which is an extension of the Activity.

Lambda expressions

Versions of Java below 8 do not support Lambda expressions, and Kotlin addresses interoperability with Java. Kotlin’s Lambda expressions implement functions in a more concise syntax, freeing developers from redundant and verbose syntax.

Classification of Lambda expressions

Ordinary Lambda expressions

() - >Unit
Copy the code

Return a Lambda expression for Unit without taking any arguments.

(tab: TabLayout.Tab?) -> Unit
Copy the code

Taking a nullable tabLayout. Tab argument returns a Lambda expression for Unit.

Lambda expression with receiver

OnTabSelectedListenerBuilder.() -> Unit
Copy the code

With OnTabSelectedListenerBuilder receiver object, Lambda expressions of returns Unit does not accept any parameters.

This type of receiver is common in Kotlin’s library Functions, such as the following Scope Functions:

The apply function

/** * Calls the specified function [block] with `this` value as its receiver and returns `this` value. * * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#apply). */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T. () -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}
Copy the code

Let the function

/** * Calls the specified function [block] with `this` value as its argument and returns its result. * * For detailed usage information see the documentation for [scope functions](https://kotlinlang.org/docs/reference/scope-functions.html#let). */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)}Copy the code

We can also call these Lambda expressions Type aliases.

private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit
Copy the code

We call this Lambda expression OnTabCallback.

These features of Lambda expressions in Kotlin are necessary syntactic sugar for implementing DSLS.

Function type instance call

Kotlin provides the invoke function, which we can write as f.invoke(x), which is actually equivalent to f(x), for example:

onTabReselectedCallback? .invoke(tab) ? :Unit
Copy the code

It can actually be written like this:

onTabReselectedCallback? .let { it(tab) } ? :Unit
Copy the code

These two pieces of code are equivalent.

Infix notation

Functions marked with the infix keyword can be called using infix notation (ignoring the dot and parentheses of the call). The infix function must satisfy the following requirements:

  1. They must be member functions and extension functions.
  2. They must have only one argument.
  3. Its arguments cannot accept a variable number of arguments and cannot have default values.

Here’s an example:

infix fun Int.plus(x: Int): Int =
        this.plus(x)

// The function can be called with infix notation
1 plus 2

// This is equivalent to this
1.plus(2)
Copy the code

Let me give you some more examples of functions that we often use:

Until function usage

for (i in 0 until 4) {
    tlOrder.addTab(tlOrder.newTab().setText("The order$i"))}Copy the code

Until function

/** * Returns a range from this value up to but excluding the specified [to] value. * * If the [to] value is less than or equal to `this` value, then the returned range is empty. */
public infix fun Int.until(to: Int): IntRange {
    if (to <= Int.MIN_VALUE) return IntRange.EMPTY
    return this. (to -1).toInt()
}
Copy the code

To function usage

mapOf<String,Any>("name" to "TanJiaJun"."age" to 25)
Copy the code

To function source

/**
 * Creates a tuple of type [Pair] from this and [that].
 *
 * This can be useful for creating [Map] literals with less noise, for example:
 * @sample samples.collections.Maps.Instantiation.mapFromPairs
 */
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
Copy the code

practice

The Kotlin DSL makes our code simpler, more elegant, and more imaginative. Let’s take a look at some examples:

The callback processing

Callback implementation in Java

The general steps we implement are as follows:

  1. Define an interface
  2. Define some callback methods in the interface
  3. Defines a method that sets the callback interface. The method takes an instance of the callback interface, usually in the form of an anonymous object.

Implement the TextWatcher interface

EditText etCommonCallbackContent = findViewById(R.id.et_common_callback_content);
etCommonCallbackContent.addTextChangedListener(new TextWatcher() {
    @Override
    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
        // no implementation
    }

    @Override
    public void onTextChanged(CharSequence s, int start, int before, int count) {
        // no implementation
    }

    @Override
    public void afterTextChanged(Editable s) {
        // no implementation}});Copy the code

Implement TabLayout OnTabSelectedListener interface

TabLayout tlOrder = findViewById(R.id.tl_order);
tlOrder.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
    @Override
    public void onTabSelected(TabLayout.Tab tab) {
        // no implementation
    }

    @Override
    public void onTabUnselected(TabLayout.Tab tab) {
        // no implementation
    }

    @Override
    public void onTabReselected(TabLayout.Tab tab) {
        // no implementation}});Copy the code

The callback implementation in Kotlin

Implement the TextWatcher interface

findViewById<EditText>(R.id.et_common_callback_content).addTextChangedListener(object :
    TextWatcher {
    override fun beforeTextChanged(s: CharSequence? , start:Int, count: Int, after: Int) {
        // no implementation
    }

    override fun onTextChanged(s: CharSequence? , start:Int, before: Int, count: Int) {
        // no implementation
    }

    override fun afterTextChanged(s: Editable?). {
        tvCommonCallbackContent.text = s
    }
})
Copy the code

Implement TabLayout OnTabSelectedListener interface

tlOrder.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener{
    override fun onTabReselected(tab: TabLayout.Tab?). {
        // no implementation
    }

    override fun onTabUnselected(tab: TabLayout.Tab?). {
        // no implementation
    }

    override fun onTabSelected(tab: TabLayout.Tab?).{ vpOrder.currentItem = tab? .position ? :0}})Copy the code

Do you find it similar to the Java way of writing, and you don’t experience Kotlin’s advantages?

Kotlin DSL

TextWatcherBuilder

package com.tanjiajun.kotlindsldemo

import android.text.Editable
import android.text.TextWatcher

/** * Created by TanJiaJun on 2019-10-01. */
privatetypealias BeforeTextChangedCallback = (s: CharSequence? , start:Int, count: Int, after: Int) - >Unit

privatetypealias OnTextChangedCallback = (s: CharSequence? , start:Int, before: Int, count: Int) - >Unit

private typealias AfterTextChangedCallback = (s: Editable?) -> Unit

class TextWatcherBuilder : TextWatcher {

    private var beforeTextChangedCallback: BeforeTextChangedCallback? = null
    private var onTextChangedCallback: OnTextChangedCallback? = null
    private var afterTextChangedCallback: AfterTextChangedCallback? = null

    override fun beforeTextChanged(s: CharSequence? , start:Int, count: Int, after: Int)= beforeTextChangedCallback? .invoke(s, start, count, after) ? :Unit

    override fun onTextChanged(s: CharSequence? , start:Int, before: Int, count: Int)= onTextChangedCallback? .invoke(s, start, before, count) ? :Unit

    override fun afterTextChanged(s: Editable?).= afterTextChangedCallback? .invoke(s) ? :Unit

    fun beforeTextChanged(callback: BeforeTextChangedCallback) {
        beforeTextChangedCallback = callback
    }

    fun onTextChanged(callback: OnTextChangedCallback) {
        onTextChangedCallback = callback
    }

    fun afterTextChanged(callback: AfterTextChangedCallback) {
        afterTextChangedCallback = callback
    }

}

fun registerTextWatcher(function: TextWatcherBuilder. () -> Unit) =
    TextWatcherBuilder().also(function)
Copy the code

OnTabSelectedListenerBuilder

package com.tanjiajun.androidgenericframework.utils

import com.google.android.material.tabs.TabLayout

/** * Created by TanJiaJun on 2019-09-07. */
private typealias OnTabCallback = (tab: TabLayout.Tab?) -> Unit

class OnTabSelectedListenerBuilder : TabLayout.OnTabSelectedListener {

    private var onTabReselectedCallback: OnTabCallback? = null
    private var onTabUnselectedCallback: OnTabCallback? = null
    private var onTabSelectedCallback: OnTabCallback? = null

    override fun onTabReselected(tab: TabLayout.Tab?).= onTabReselectedCallback? .invoke(tab) ? :Unit

    override fun onTabUnselected(tab: TabLayout.Tab?).= onTabUnselectedCallback? .invoke(tab) ? :Unit

    override fun onTabSelected(tab: TabLayout.Tab?).= onTabSelectedCallback? .invoke(tab) ? :Unit

    fun onTabReselected(callback: OnTabCallback) {
        onTabReselectedCallback = callback
    }

    fun onTabUnselected(callback: OnTabCallback) {
        onTabUnselectedCallback = callback
    }

    fun onTabSelected(callback: OnTabCallback) {
        onTabSelectedCallback = callback
    }

}

fun registerOnTabSelectedListener(function: OnTabSelectedListenerBuilder. () -> Unit) =
        OnTabSelectedListenerBuilder().also(function)
Copy the code

General steps:

  1. Define a class that implements the callback interface and implements its callback methods.
  2. Observe the argument to the callback method, extract it into a function type, and give the function type an alternate name using the type alias as needed, and use the private modifier.
  3. Declare some nullable variable (VAR) private member variables of the function type inside the class, and take the corresponding variable from the callback function to implement its invoke function, passing in the corresponding parameters.
  4. Define functions in a class with the same name as the callback interface, but with arguments of the corresponding function type, and assign the function type to the corresponding member variable of the current class.
  5. Define a member function that takes a Lambda expression with the recipient object of that class and returns Unit, creates the corresponding object in the function, and passes the Lambda expression in using the also function.

How do you use it? Take a look at the following code:

TextWatcher

findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(
    registerTextWatcher {
        afterTextChanged { tvDSLCallbackContent.text = it }
    })
Copy the code

TabLayout.OnTabSelectedListener

tlOrder.addOnTabSelectedListener(registerOnTabSelectedListener { onTabSelected { vpOrder.currentItem = it? .position ? :0}})Copy the code

Let me elaborate a little bit more on why I can write this way? In fact, before simplification is written like this, code is as follows:

findViewById<EditText>(R.id.et_dsl_callback_content).addTextChangedListener(registerTextWatcher({
    this.afterTextChanged({ s: Editable? ->
        tvDSLCallbackContent.text = s
    })
}))
Copy the code

Kotlin syntax states that if the last argument to a function is a Lambda expression, it can be raised outside the parentheses and the parentheses can be omitted. Kotlin can then deduce the type of the argument and use the default argument it instead of the named argument. Then, since this is a Lambda expression with a receiver, So we can take the object with this and call its afterTextChanged function, and we end up with our simplified code.

Object object expression callback and DSL callback comparison

  1. The DSL notation is much more Kotlin than object notation.
  2. The object notation implements all methods, and the DSL notation implements the desired method as needed.
  3. In terms of performance, the DSL method creates an instance object of a Lambda expression for each callback function, while the Object method generates only one anonymous object instance no matter how many callback methods there are. Therefore, the Object method performs better than the DSL method.

Here I take TextWatcher as an example and decompile them into Java code like this:

Object Specifies the callback of an object expression

((EditText)this.findViewById(-1000084)).addTextChangedListener((TextWatcher)(new TextWatcher() {
   public void beforeTextChanged(@Nullable CharSequence s, int start, int count, int after) {}public void onTextChanged(@Nullable CharSequence s, int start, int before, int count) {}public void afterTextChanged(@Nullable Editable s) {
      TextView var10000 = tvCommonCallbackContent;
      Intrinsics.checkExpressionValueIsNotNull(var10000, "tvCommonCallbackContent"); var10000.setText((CharSequence)s); }}));Copy the code

DSL callback

((EditText)this.findViewById(-1000121)).addTextChangedListener((TextWatcher)TextWatcherBuilderKt.registerTextWatcher((Function1)(new Function1() {
   // $FF: synthetic method
   // $FF: bridge method
   public Object invoke(Object var1) {
      this.invoke((TextWatcherBuilder)var1);
      return Unit.INSTANCE;
   }

   public final void invoke(@NotNull TextWatcherBuilder $this$registerTextWatcher) {
      Intrinsics.checkParameterIsNotNull($this$registerTextWatcher, "$receiver");
      $this$registerTextWatcher.beforeTextChanged((Function4)(new Function4() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1, Object var2, Object var3, Object var4) {
            this.invoke((CharSequence)var1, ((Number)var2).intValue(), ((Number)var3).intValue(), ((Number)var4).intValue());
            return Unit.INSTANCE;
         }

         public final void invoke(@Nullable CharSequence s, int start, int count, int after) {
            TextView var10000 = tvDSLCallbackContent;
            Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText(s); }})); $this$registerTextWatcher.onTextChanged((Function4)(new Function4() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1, Object var2, Object var3, Object var4) {
            this.invoke((CharSequence)var1, ((Number)var2).intValue(), ((Number)var3).intValue(), ((Number)var4).intValue());
            return Unit.INSTANCE;
         }

         public final void invoke(@Nullable CharSequence s, int start, int before, int count) {
            TextView var10000 = tvDSLCallbackContent;
            Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText(s); }})); $this$registerTextWatcher.afterTextChanged((Function1)(new Function1() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke(Object var1) {
            this.invoke((Editable)var1);
            return Unit.INSTANCE;
         }

         public final void invoke(@Nullable Editable it) {
            TextView var10000 = tvDSLCallbackContent;
            Intrinsics.checkExpressionValueIsNotNull(var10000, "tvDSLCallbackContent"); var10000.setText((CharSequence)it); }})); }})));Copy the code

You can see that the Object notation only generates an anonymous TextWatcher object instance, whereas the DSL notation creates an instance object of a Lambda expression (Function1, Function4) for each callback function, as expected.

digression

Java8 introduced the default keyword, which can include some default method implementations in the interface.

interface Handlers{

    void onLoginClick(View view);

    default void onLogoutClick(View view){}}Copy the code

With Kotlin, we can add ** @jvmdefault ** as follows:

interface Handlers{

    fun onLoginClick(view: View)

    @JvmDefault
    fun onLogoutClick(view: View){}}Copy the code

We can decompile into Java code as follows:

@Metadata(
   mv = {1.1.15},
   bv = {1.0.3},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
   d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
   void onLoginClick(@NotNull View var1);

   @JvmDefault
   default void onLogoutClick(@NotNull View view) {
      Intrinsics.checkParameterIsNotNull(view, "view"); }}Copy the code

Note the following when using ** @jvmdefault ** :

Since the default keyword was only introduced in Java8, we need to do something special, as you can see from the official Kotlin documentation

Specifies that a JVM default method should be generated for non-abstract Kotlin interface member.

Usages of this annotation require an explicit compilation argument to be specified: either -Xjvm-default=enable or -Xjvm-default=compatibility.

  • with -Xjvm-default=enable, only default method in interface is generated for each @JvmDefault method. In this mode, annotating an existing method with @JvmDefault can break binary compatibility, because it will effectively remove the method from the DefaultImpls class.
  • with -Xjvm-default=compatibility, in addition to the default interface method, a compatibility accessor is generated in the DefaultImpls class, that calls the default interface method via a synthetic accessor. In this mode, annotating an existing method with @JvmDefault is binary compatible, but results in more methods in bytecode.

Removing this annotation from an interface member is a binary incompatible change in both modes.

Generation of default methods is only possible with JVM target bytecode version 1.8 (-jvm-target 1.8) or higher.

@JvmDefault methods are excluded from interface delegation.

Translate the main content, mainly include:

  1. The default method can only be generated using JVM target bytecode version 1.8 or higher.

  2. Use this annotation to specify an explicit compilation parameter: -xJVM-default =enable or

    -xjVM-default =compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility, compatibility Because it removes the method from the DefaultImpls class; With -xJVM-default = Compatibility, in addition to generating the default method, compatibility accessors are generated in the DefaultImpls class, which calls the default** method through the composite accessors. In this mode, it is binary compatible, But it leads to more methods in bytecode.

Add the following code to the build.gradle file:

allprojects {
    repositories {
        google()
        jcenter()
        
    }

    tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
        kotlinOptions {
            jvmTarget = '1.8'
            freeCompilerArgs += '-Xjvm-default=compatibility'}}}Copy the code

Let’s try these two cases to see if they meet the above expectations. The code is as follows:

Decompiled code with -xJVM-default =enable

@Metadata(
   mv = {1.1.15},
   bv = {1.0.3},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
   d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
   void onLoginClick(@NotNull View var1);

   @JvmDefault
   default void onLogoutClick(@NotNull View view) {
      Intrinsics.checkParameterIsNotNull(view, "view"); }}Copy the code

-xJVM-default =compatibility (compatibility

@Metadata(
   mv = {1.1.15},
   bv = {1.0.3},
   k = 1,
   d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\ u0002\bf\u0018\u00002\u00020\u0001J\u0010\u0010\u0002\u001a\u00020\u00032\u0006\u0010\u0004\u001a\u00020\u0005H&J\u0010\ U0010 \ u0006 \ u001a \ u00020 \ u00032 \ u0006 \ u0010 \ u0004 \ u001a \ u00020 \ u0005H \ u0017 ø \ u0001 \ u0000 \ u0082 \ u0002 \ u0007 \ n \ u0005 \ \ u00 b 91 (0 \ u0001 ¨ \ u0006 \ u0007"},
   d2 = {"Lcom/tanjiajun/kotlindsldemo/MainActivity$Handlers;".""."onLoginClick".""."view"."Landroid/view/View;"."onLogoutClick"."app_debug"})public interface Handlers {
   void onLoginClick(@NotNull View var1);

   @JvmDefault
   default void onLogoutClick(@NotNull View view) {
      Intrinsics.checkParameterIsNotNull(view, "view");
   }

   @Metadata(
      mv = {1.1.15},
      bv = {1.0.3},
      k = 3
   )
   public static final class DefaultImpls {
      @JvmDefault
      public static void onLogoutClick(MainActivity.Handlers $this, @NotNull View view) {$this.onLogoutClick(view); }}}Copy the code

** -xjVM-default = Compatibility > -xJVM-default =enable -xJVM-default =compatibility** -xjVm-default =compatibility** -xjVm-default =compatibility** -xjVm-default =compatibility**

Spek

Spek is a testing framework built for Kotlin.

describe("Verify Check Email Valid") {
    it("Email Is Null") {
        presenter.checkEmailValid("")
        verify { viewRenderer.showEmailEmptyError() }
    }

    it("Email Is Not Null and Is Not Valid") {
        presenter.checkEmailValid("ktan")
        verify { viewRenderer.showEmailInvalidError() }
    }

    it("Email Is Not Null and Is Valid") {
        presenter.checkEmailValid("[email protected]")
        verify { viewRenderer.hideEmailError() }
    }
}
Copy the code

Making: Spek

kxDate

KxDate is a date processing library, we can write similar to the English sentence code, very interesting, code as follows:

val twoMonthsLater = 2 months fromNow
val yesterday = 1 days ago
Copy the code

Making: kxdate

Anko

Anko is a Kotlin library developed specifically for Android. We can write the layout as follows:

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!")}}}Copy the code

Making: Anko

Demo: KotlinDSLDemo

My GitHub: TanJiaJunBeyond

Common Android Framework: Common Android framework

My nuggets: Tan Jiajun

My simple book: Tan Jiajun

My CSDN: Tan Jiajun