【Kotlin精简】第5章 简析DSL

1 DSL是什么?

Kotlin 是一门对 DSL 友好的语言,它的许多语法特性有助于 DSL 的打造,提升特定场景下代码的可读性和安全性。本文将带你了解 Kotlin DSL 的一般实现步骤,以及如何通过 @DslMarkerContext Receivers 等特性提升 DSL 的易用性。

DSL 全称是 Domain Specific Language,即领域特定语言。顾名思义 DSL 是用来专门解决某一特定问题的语言,比如我们常见的 SQL 或者正则表达式等,DSL 没有通用编程语言(Java、Kotlin等)那么万能,但是在特定问题的解决上更高效。

2 Gradle Kotlin DSL的优点和使用

Gradle Kotlin DSLGradle 5.0引入的一种新型的Gradle脚本语言,作为Groovy语言的替代方案。
官方文档中提到,Kotlin DSL具有如下的优点:

  1. 类型安全:编写Gradle脚本时,可以进行静态类型检查,这样可以保证更高的代码质量和更好的可维护性;
  2. 代码提示:Kotlin语言具有良好的编码体验,比如IDE可以提示代码补全、语法错误等,这些在Groovy语言中不易得到;
  3. 使用简单:Kotlin是一种现代化的语言,语法易懂,学习成本低;
  4. 高效性:Gradle使用Kotlin编写的DSL脚本会比同样的Groovy脚本快2~10倍。

创作一套全新新语言的成本很高,所以很多时候我们可以基于已有的通用编程语言打造自己的 DSL,比如日常开发中我们将常见到 gradle 脚本 ,其本质就是来自 Groovy 的一套 DSL

android {
  compileSdkVersion 28
  defaultConfig {
    applicationId "com.my.app"
    minSdkVersion 24
    targetSdkVersion 30
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  }
  buildTypes {
    release {
      minifyEnabled false
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
  }
}

build.gradle 中我们可以用大括号表现层级结构,使用键值对的形式设置参数,没有多余的程序符号,非常直观。如果将其还原成标准的 Groovy 语法则变成下面这样,是下面这样,在可读性上的好坏立判:

Android(30,
  DefaultConfig("com.my.app",
    24,
    30,
    1,
    "1.0",
    "android.support.test.runner.AndroidJUnitRunner"
  )
),
  BuildTypes(
  Release(false,
    getDefaultProguardFile('proguard-android-optimize.txt'),
    'proguard-rules.pro'
    )
)

除了 GroovyKotlin 也非常适合 DSL 的书写,正因如此 Gradle 开始推荐使用 kts 替代 gradle,其实就是利用了 Kotlin 优秀的 DSL 特性。

3 Kotlin DSL 及其优势

KotlinAndroid 的主要编程语言,因此我们可以在 Android 开发中发挥其 DSL 优势,提升特定场景下的开发效率。例如 ComposeUI 代码就是一个很好的示范,它借助 DSLKotlin 代码具有了不输于 XML 的表现力,同时还兼顾了类型安全,提升了 UI 开发效率。

3.1 一个简单DSL例子

Kotlin中实现DSL构建要依靠这几样东西:

  1. 扩展函数;
  2. 带接收者的 Lambda 表达式;
  3. 在方法括号外使用Lambda

我们先来看一下一个DSL例子:

val person = person {
    name = "John"
    age = 25
    address {
        street = "Main Street"
        number = 42
        city = "London"
    }
}


// 数据模型
data class Person(var name: String? = null,
                  var age: Int? = null,
                  var address: Address? = null)


data class Address(var street: String? = null,
                   var number: Int? = null,
                   var city: String? = null)

要实现上面的语法糖,现在要做的第一件事就是创建一个新文件,将保持DSL与模型中的实际类分离。首先为Person类创建一些构造函数。看看我们想要的结果,看到Person的属性是在代码块中定义的。这些花括号实际上是定义一个lambda。这就是使用上面提到的三种Kotlin语言特征中的第一种语言特征的地方:在方法括号外使用Lambda

如果一个函数的最后一个参数是一个lambda,可以把它放在方法括号之外。而当你只有一个lambda作为参数时,你可以省略整个括号。person {…}实际上与person({…})相同。这在我们的DSL中变得更简洁。现在来编写person函数的第一个版本。

// 数据模型
fun person(block: (Person) -> Unit): Person {
    val p = Person()
    block(p)
    return p
}

所以在这里我们有一个创建一个Person对象的函数。它需要一个带有我们在第2行创建的对象的lambda。当在第3行执行这个lambda时,我们期望在返回第4行的对象之前,该对象获得它所需要的属性。下面展示如何使用这个函数:

val person = person {
    it.name = "John"
    it.age = 25
}

由于这个lambda只接收一个参数,可以用它来调用person对象。这看起来不错,但还不够完美,如果在我们的DSL看到的东西。特别是当我们要在那里添加额外的对象层。这带来了我们接下来提到的Kotlin功能:带接受者的Lambda

person函数的定义中,可以给lambda添加一个接收者。这样只能在lambda中访问那个接收者的函数。由于lambda中的函数在接收者的范围内,则可以简单地在接收者上执行lambda,而不是将其作为参数提供。

fun person(block: Person.() -> Unit): Person {
    val p = Person()
    p.block()
    return p
}

// 这实际上可以通过使用Kotlin提供的apply函数在一个简单的单行程中重写。
fun person(block: Person.() -> Unit): Person = Person().apply(block)

现在可以将其从DSL中删除:

val person = person {
    name = "John"
    age = 25
}

到目前为止,还差一个Address类,在我们想要的结果中,它看起来很像刚刚创建的person函数。唯一的区别是必须将它分配给Person对象的Address属性。为此,可以使用上面提到的三个Kotlin语言功能中的最后一个:扩展函数

扩展函数能够向类中添加函数,而无需访问类本身的源代码。这是创建Address对象的完美选择,并直接将其分配给Person的地址属性。这是DSL文件的最终版本:

fun person(block: Person.() -> Unit): Person = Person().apply(block)

fun Person.address(block: Address.() -> Unit) {
    address = Address().apply(block)
}

现在为Person添加一个地址函数,它接受一个Address作为接收者的lambda表达式,就像对person构造函数所做的那样。然后它将创建的Address对象设置为Person的属性:

val person = person {
    name = "John"
    age = 25
    address {
        street = "Main Street"
        number = 42
        city = "London"
    }
}

3.2 实现简单的UI布局

我们先来看下这个布局


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:textSize="16sp"
        android:paddingTop="10dp" />

FrameLayout>

上面XML使用DSL写法如下:

context.FrameLayout {
                layout_width = match_parent
                layout_height = wrap_content

                TextView {
                    layout_id = "tv"
                    layout_width = match_parent
                    layout_height = match_parent
                    textSize = 16f
                    padding_top = 10
                }
           }

首先要定义一种声明方式来初始化对象,所以可以写一个基于Context扩展函数

inline fun Context.FrameLayout(
    style: Int? = null,
    init: FrameLayout.() -> Unit
): FrameLayout {
    val frameLayout =
        if (style != null) FrameLayout(
            ContextThemeWrapper(this, style)
        ) else FrameLayout(this)
    return frameLayout.apply(init)
}

// 扩展View的layout_width、layout_height等属性,
// 其他属性这里不做详解,写法同layout_width、layout_height
inline var View.layout_width: Number
    get() {
        return 0
    }
    set(value) {
        val w = if (value.dp > 0) value.dp else value.toInt()
        val h = layoutParams?.height ?: 0
        updateLayoutParams<ViewGroup.LayoutParams> {
            width = w
            height = h
        }
    }
    
inline var View.layout_height: Number
    get() {
        return 0
    }
    set(value) {
        val w = layoutParams?.width ?: 0
        val h = if (value.dp > 0) value.dp else value.toInt()
        updateLayoutParams<ViewGroup.LayoutParams> {
            width = w
            height = h
        }
    }

这里的init就是上面说的带接受者的lamba表达式拉,所以代码里去实现一个FrameLayout布局就可以这样子拉

context.FrameLayout {
       layout_width = match_parent
       layout_height = wrap_content
}

而对于子控件,TextView举个栗子:

inline fun ViewGroup.TextView(
    style: Int? = null,
    init: AppCompatTextView.() -> Unit
): TextView {
    val textView =
        if (style != null) AppCompatTextView(
            ContextThemeWrapper(context, style)
        ) else AppCompatTextView(context)
    return textView.apply(init).also { addView(it) }
}

这样一个简单的动态布局就出来了,没想象中那么高级,其实就是对扩展函数高阶函数的运用。

3.3 小结

Kotlin DSL的好处,尤其是对View进行特定领域的处理的时候 很有用。

  • 有着近似 XML 的结构化表现力
  • 较少的字符串,更多的强类型,更安全
  • 可提取 linearLayoutParams 这样的对象方便复用
  • 在布局中同步嵌入 onClick 等事件处理
  • 如需要还可以嵌入 iffor 这样的控制语句

4 DSL实现的原理

4.1 扩展函数(扩展属性)

package strings

fun String.lastChar(): Char = this.get(this.length - 1)

【Kotlin精简】第5章 简析DSL_第1张图片

4.2 lambda使用

lambda 表达式定义:
【Kotlin精简】第5章 简析DSL_第2张图片

高阶函数:高阶函数就是以另一个函数作为参数或返回值的函数。
【Kotlin精简】第5章 简析DSL_第3张图片

Kotlin 的 lambda 有个规约:如果 lambda 表达式是函数的最后一个实参,则可以放在括号外面,并且可以省略括号

person.maxBy({ p:Person -> p.age })

// 可以写成
person.maxBy(){
    p:Person -> p.age
}

// 更简洁的风格:
person.maxBy{
    p:Person -> p.age
}

带接收者的 lambda
【Kotlin精简】第5章 简析DSL_第4张图片
【Kotlin精简】第5章 简析DSL_第5张图片
想一想 File就是带接受者,说明这个lambda的对象是File

4.3 中缀调用

【Kotlin精简】第5章 简析DSL_第6张图片
中缀调用是实现类似英语句子结构 DSL 的核心。

4.4 invoke 约定

【Kotlin精简】第5章 简析DSL_第7张图片
invoke约定的作用:它的作用就是让对象像函数一样调用方法。
【Kotlin精简】第5章 简析DSL_第8张图片

class DependencyHandler{
    //编译库
    fun compile(libString: String){
        Logger.d("add $libString")
    }
    //定义invoke方法
    operator fun invoke(body: DependencyHandler.() -> Unit){
        body()
    }
}

//我们有下面的3种调用方式:
val dependency = DependencyHandler()
//调用invoke
dependency.invoke {
    compile("androidx.core:core-ktx:1.6.0")
}
//直接调用
dependency.compile("androidx.core:core-ktx:1.6.0")
//带接受者lambda方式
dependency{
    compile("androidx.core:core-ktx:1.6.0")
}

5 总结

Kotlin DSL 是一种强大的工具,可以帮助我们编写更简洁、优雅的代码。通过使用 Kotlin DSL,我们可以提高代码的可读性、灵活性和类型安全性。当然 AndroidDSL 远不止这些使用场景 ,但是实现思路都是相近的,最后再来一起回顾一下:

  1. DSL 是什么?
    DSL 是一种针对特殊编程场景的语言或范式,它处理效率更高,且表达式更为专业。
    例如 SQL、HTML、正则表达式等。
  2. Kotlin 如何支持 DSL
    通过 扩展函数、带接收者的函数类型等来支持使用 DSL。
  3. Kotlin 自定义 DSL 的优势
    提供一套编程风格,可以简化构建一些复杂对象的代码,提高简洁程度的同时,具备很高的可读性。
  4. Kotlin 自定义 DSL 的缺点
    构造代码较为复杂,有一定上手难度,非必要不使用。

Tips: 对于顶级的Android发烧友,或者是Kotlin学习爱好者可以深度去挖掘DSL,或者是高级的Kotlin语法糖。注意对于在职场打拼的各位朋友们,还是那句话:学值得变现的知识点,并且要等机会来变现,从这个角度,Kotlin会用就可以了,不一定要非要死磕语法糖。切记。职场和自由职业free style 学习的东西是不一样的。

你可能感兴趣的:(Kotlin,Android,kotlin,开发语言,android)