译者前言:在阅读本篇文章之前,你需要了解Kotlin以下知识点:
在这篇文章中,我们将要学习如何在Android项目中编写Kotlin DSL。
本文篇幅较长,需要花费一定时间来阅读,从而来一起编写DSL。我们将会阐述以下话题:
维基百科上有如下解释:
DSL(domain-specific language),是一门特定于某个应用领域的计算机语言。这和通用语言(GPL,general-purpose-language)形成对比,通用语言广泛应用于多个领域
我靠,你是认真的吗?(译注:说人话)
通俗来说,DSL为你提供了一种针对于任何特定语言的灵活工具,从而使得特定编程语言发挥更大的作用。
如果你是一名Android开发者,并且正在项目中使用kotlin,你可能正在项目有意地或者无意地使用DSL。你能想起哪些例子吗?(译注:像koin、DBFlow这些第三方框架都在普遍使用DSL)
让我来帮帮你,你是否曾经编写过如下代码:
yourlist.forEach { //your code is here }
上面的实例中,你正在使用forEach来遍历列表。forEach就是kotlin中的一种DSL示例。
我们可以使用DSL来简化和完善应用的复杂度,并且使得代码易读。
在介绍编写我们自己的DSL之前,我们需要学习带接收者的lambda表达式(lambda with receiver)。笔者强烈推荐阅读此篇文章 ,不过我现在也可对带接收者的lambda表达式做一个简短的描述。
我们有如下一个buildString函数:
fun buildString(action: (StringBuilder).() -> Unit): String {
val stringBuilder = StringBuilder()
action(stringBuilder)
return stringBuilder.toString()
}
(译注:在kotlin.text包下,有个同名内联函数buildString,实现如下:
public inline fun buildString(builderAction: StringBuilder.() -> Unit): String =
StringBuilder().apply(builderAction).toString()
)
这里,buildString函数需要一个action作为形参。而action函数自身需要接收一个StringBuilder来作为参数,并且返回一个String类型的值。
现在,我们就可以这样使用buildString:
buildString {
append("<")
append("MindOrks")
append(">")
}
由于我们在buildString函数中使用了扩展函数,所以我们可以使用StringBuilder类相关的属性。
我们同样可以创建自定义的DSL用法
在kotlin中,使用infix(可译作中缀表达式或中缀函数)可以帮助我们创建自定义的DSL,就好像我们在编写英文一样。比如,
在英文中,我们说“1 plus 2”从而来得到两者之和或者说“1 minus 2”来得到两者之差。在kotlin中我们可以使用中缀调用来实现同样的效果。
我们来创建一个中缀函数用于两数相加:
infix fun Int.plus(num: Int) = this + num
我们在这里创建了Int的一个扩展函数,将传入的参数和自身进行相加。所以,我们可以这样使用该中缀函数:
val output = 1 plus 2
在上面的代码中,使用我们创建的中缀函数plus来产生结果。当在Logcat中打印时,将会打印和为3.
这是由于我们创建了一个中缀扩展函数,从而在传统方式中使用“+”之处使用plus。
同样地,如果我们想要创建中缀函数minus,我们可以有如下定义:
infix fun Int.minus(num: Int) = this - num
然后我们就可以这样去使用:
val output = 1 minus 2
中缀调用提高代码的可读性并且更具条理性,易于人类去阅读。所以对于不了解编程语言的人来说,他们同样知道这是在对两个数相加或相减。
invoke operator(译注:即(),比如 add(),这就是在调用add函数)允许任何对象像函数一样被调用。让我们先创建一个Student类:
class Student {
operator fun invoke(student: Student.() -> Unit) = student
fun addName(name: String) {
}
fun addMarks(marks: Int) {
}
}
在这个类中,我们创建了一个invoke方法,参数为接收类型为Student的lambda,并且返回参数自身。这样,我们就可以在student对象中调用所有Student类的方法。
现在,像通常一样,我们创建一个Student类的实例:
val student = Student()
现在我们可以使用Student类的addName和addMarks方法作为DSL。不过,我们可以有两种不同的方式来使用它:
方式一(传统的方式):
student.addName("MindOrks")
student.addMarks(100)
方式二(DSL方式):
student {
addName("MindOrks")
addMarks(100)
}
在这我们将讨论如何为data class创建一个DSL。假定我们有一个data class Student:
data class Student(var name: String? = null, var age: Int? = null, var marks: Int? = null)
这样,如果我们要使用这个data class,我们会写成:
val student = Student("MindOrks", 20, 30)
现在我们把上面的代码转换成DSL形式,首先我们将会创建一个新lambda:
fun student(student: Student.() -> Unit): Student = Student().apply(student)
这样:
val student = student {
name = "MindOrks"
age = 20
marks = 30
}
我们将TextView作为UI元素。这样:
textView.text = "MindOrks"
textView.setOnClickListener {
}
textView.setTextColor(Color.BLACK)
但是我们可以以DSL的方式使用它:
textView.apply {
text = "MindOrks"
setOnClickListener {
}
textColor(Color.BLACK)
}
我们可以使用apply函数为任何一个UI元素创建DSL。
在Android中为了创建一个JSON我们会这样使用:
val jsonObject = JSONObject()
jsonObject.put("name","MindOrks")
jsonObject.put("age",20)
这就是我们传统的创建JSON对象的方式。
现在,让我们看看如何创建用来生成JSON对象的DSL。首先,我们将会创建一个类,让它继承自JSONObject:
class Json() : JSONObject() {
}
此时此刻,我们将再创建一个参数为带接收者的lambda的构造器。
constructor(json: Json.() -> Unit) : this() {
this.init()
}
紧接着,我们利用中缀函数来创建规范,用于往JSON对象中添加值。
infix fun String.to(value: T) {
put(this, value)
}
现在,类的整体结构如下:
class Json() : JSONObject() {
constructor(json: Json.() -> Unit) : this() {
this.json()
}
infix fun String.to(value: T) {
put(this, value)
}
}
那么我们就可以在Activity中使用DSL来创建JSON:
val json = Json {
"name" to "MindOrks"
"age" to 20
}
在这里:
{“name”:“MindOrks”,“age”:20}
学习愉快!