alighters

程序、写作、人生

Kotlin DSL 学习

| Comments

DSL(domain-specific language),特定领域语言。wiki 关于 DSL 的定义如下:

A domain-specific language (DSL) is a computer language specialized to a particular application domain. This is in contrast to a general-purpose language (GPL), which is broadly applicable across domains.

通俗来讲,其是指特定领域的语言,如 SQL, Gradle 等。另外其语言是可表达的且易读的。

而 Kotlin 的语言特征,能够让我们更加方便地来实现 DSL。

语言特性

1.Lambda Out of Parentheses

lambda 通常定义的格式为 (list of param types) -> returned type。最简单的格式则为 () -> Unit , 其中 Unit 等同于 Void。

将一个 lambda 赋值给一个变量,最基本的格式如下:

1
val helloPrint: (String) -> Unit = { println(it) }

调用此 lambda 的话:

1
helloPrint("Hello")

而多参数时,可如下使用:

1
2
val helloPrint: (String, Int) -> Unit = { _, _ -> println("Do nothing") } 
helloPrint("Does not matter", 42) //output: Do nothing

其中参数不使用时,可用下划线来代替。

关于 Lambda 的使用,这里假设有个函数 x(),当一个 lambda 是这个函数的最后一个参数时,其可以放置在函数括号的外面。另外,如果这个 lambda 是这个函数的唯一参数时,这个括号是可以省略的。这样,形如 x({…}) 的使用可以转换为 x(){},再省略括号的话,我们得到 x{}。

lambda 的使用则有如下的形式:

1
fun x( lambda: () -> Unit ) { lambda() }

也可以写成单行如下:

1
fun x( lambda: () -> Unit ) = lambda()

这样若是实现一个形如

1
2
3
4
val person = person {
    it.name = "John"
    it.age = 25
}

的 DSL 用法,其中 person 的声明如下:

1
2
3
data class Person(var name: String? = null,
                  var age: Int? = null,
                  var address: Address? = null)

这时,则可以定义一个 person 的 lambda 定义:

1
2
3
4
5
fun person(block: (Person) -> Unit): Person {
    val p = Person()
    block(p)
    return p
}

2.Lambdas with receivers

在上述的 person dsl 用法中,it 的使用来说每次都是累赘的。这时可以通过 Lambda with receivers ,来避免每次写它。

它的意思是可以为 lambda 的声明指定一个接受者 receiver ,这样我们在 lambda 中只能访问这个 receiver 的所有非静态的公开函数。由于其限定了 receiver 的域,所以在 lambda 中,可以不必在提供前缀的 it 参数。

所以,这里的格式为 () -> Unit 转变为了 X.()-> Unit。

注意,这里的写法只是用于方便书写,将这两种形式的代码转变为字节码时,可以发现其并没有区别的。仅仅在于其一个赋值给了变量 it,一个赋值给了变量 receiver。

将 person 的 fun 修改为:

1
2
3
4
5
fun person(block: Person.() -> Unit): Person {
    val p = Person()
    p.block()
    return p
}

简写成一行的话如下:

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

这时,person 的调用便可以简化:

1
2
3
4
val person = person {
    name = "John"
    age = 25
}

3.Extension functions

此功能即为扩展函数,其表现就是给一些类提供额外的方法,来方便开发调用。其在 Java 中的实现则是通过静态函数来实现,参数便是 Kotlin 中对应的类。

所以,要实现如下的 DSL :

1
2
3
4
5
6
7
8
9
val person = person {
    name = "John"
    age = 25
    address {
        street = "Main Street"
        number = 42
        city = "London"
    }
}

在声明一个 Address:

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

便要对 person 类作拓展:

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

4.Builder Pattern

在上面的例子中,其参数都是为 var 定义,若需要为 val 定义,这里可以采用 builder 模式来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fun person(block: PersonBuilder.() -> Unit): Person = PersonBuilder().apply(block).build()


class PersonBuilder {

    var name: String = ""

    private var dob: Date = Date()
    var dateOfBirth: String = ""
        set(value) {
            dob = SimpleDateFormat("yyyy-MM-dd").parse(value)
        }

    private var address: Address? = null

    fun address(block: AddressBuilder.() -> Unit) {
        address = AddressBuilder().apply(block).build()
    }

    fun build(): Person = Person(name, dob, address)

}

class AddressBuilder {

    var street: String = ""
    var number: Int = 0
    var city: String = ""

    fun build() : Address = Address(street, number, city)

}

如此,Person 的构造函数便可以使用 val 了,同时 builder 模式保证了类型安全的目的 (type-safe)。

5. Scope control: @DslMarker (Since 1.1)

因为 lambda 的实现都是匿名函数,其可以访问外部作用域。所以这里使用 @DslMarker ,可以达到收窄作用域的目的。如下,使用其声明一个 annotation class:

1
2
@DslMarker
annotation class PersonDsl

之后,将 @PersonDsl 添加在指定的类上,然后以此类定义的闭包,则不能访问外层作用域的内容。其会在编译器中得到错误的提示。

Anko

在 Android 中关于 Kotlin 的使用,集大成者便属于 Kotlin/anko: Pleasant Android application development 了,其主要包含的内容:

  • Anko Commons: 关于 intents、dialogs、 logging 等轻量级的工具库
  • Anko Layouts: 提供一个快速且类型安全的快速 Android 布局方法。
  • Anko SQLite: 对 Android SQLite 支持的查询和集合转换的 DSL 功能。
  • Anko Coroutines: 对 kotlinx.coroutines library 提供的工具类。

其中,以最常用的 Anko Layouts 为例:

1
2
3
4
5
6
verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

这里指定了一个竖直的 layout,在其中声明了一个名称为 name 的 editText,另有一个文本为 “Say hello” 的按钮,并为按钮添加一个点击的事件,事件可以弹出一个 toast,提示内容为 Hello 加 name 控件的文本。

这里,相比以前的 xml 写法,这种写法简洁了许多。但要使用预览功能,这里需要另外安装 Anko Support Plugin 的插件,并采用 AnkoComponet 的方式书写 Anko Layout。

其中关于 verticalLayout 的定义是在 CustomViews 类中。且此方法针对 ViewManager、Context 及 Activity 做了扩展,以 Actiivty 为例:

1
2
3
4
inline fun Activity.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
    return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}

其中的 Lambda 为 _LinearLayout 类,关于 VERTICAL_LAYOUT_FACTORY 的定义如下:

1
2
3
4
5
6
7
8
@PublishedApi
internal object `$$Anko$Factories$CustomViews` {
    val VERTICAL_LAYOUT_FACTORY = { ctx: Context ->
        val view = _LinearLayout(ctx)
        view.orientation = LinearLayout.VERTICAL
        view
    }
}

通过 lambda 的内容,来实例化一个 _LinearLayout,并指定其 orientation 为 LinearLayout.VERTICAL。另外, ankoView 方法,这里所做的工作:

1
2
3
4
5
6
7
inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
    val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
    val view = factory(ctx)
    view.init()
    AnkoInternals.addView(this, view)
    return view
}

对 context 做以包装,通过 factory 方法得到 View , 再调用 view 的 lambda init 方法。之后通过 AnkoInternals 的 addView 方法对 view 进行添加操作,将其添加至视图中。

回到上面的视图书写中。因为 Android 中的 view 都实现了接口 ViewManager,而这里的扩展方法,针对类便是以 Context、ViewManager、Activity 这三个为主。包含下面的 editText 方法:

1
2
3
4
inline fun ViewManager.editText(): android.widget.EditText = editText() {}
inline fun ViewManager.editText(init: (@AnkoViewDslMarker android.widget.EditText).() -> Unit): android.widget.EditText {
    return ankoView(`$$Anko$Factories$Sdk25View`.EDIT_TEXT, theme = 0) { init() }
}

其中 editText 方法会调用到 ViewManager 的扩展,紧接着调用到下面的 editText,也就意味着 ankoView 方法的调用,所以在调用 editText 及 button 后,都会执行它们的 addView 方法。 以上,便是简单的通过扩展来实现 view 布局的 DSL 了。另,还有其他更加复杂的扩展可再自行研究了。

Android KTX

android/android-ktx 其定义了一系列关于 Android App 开发中 Kotlin 的扩展,其目的是将我们用 Kotlin 开发 Android 代码更加简化,而并不是对已有的 Android API 添加新的功能。

如:

Kotlin:

1
val uri = Uri.parse(myUriString)

Kotlin with Android KTX:

1
val uri = myUriString.toUri()

这是一个 Extension functions 的应用,另外还有其他关于 Lambda 等的应用。不过其目前处于一个 preview 的开发应用,可对其未支持的 API 提 pr,进行贡献开发。

参考资料

版权归作者所有,转载请注明原文链接:/blog/2018/03/02/kotlin-dsl-learn/

给 Ta 个打赏吧...

Comments