1. 前言

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

2. 什么是 DSL?

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

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

android {compileSdkVersion 28defaultConfig {applicationId "com.my.app"minSdkVersion 24targetSdkVersion 30versionCode 1versionName "1.0"testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles 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')
)

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

3. Kotlin DSL 及其优势

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

普通的 Android View 也可以使用 DSL 进行描述。下面是一个简单的 UI 布局

XML代码

DSL代码

通过对比可以看到 Kotin DSL 有诸多好处:

  • 有着近似 XML 的结构化表现力

  • 较少的字符串,更多的强类型,更安全

  • linearLayoutParams 这样的对象可以多次复用

  • 可以在定义布局的同时实现 onClick 等

  • 如果需要,还可以嵌入 if ,for 这样的控制语句

倘若没有 DSL ,我们想借助 Kotlin 达到上述好处,代码可能是下面这样的:

LinearLayout(context).apply {addView(ImageView(context).apply { image = context.getDrawable(R.drawable.avatar)}, LinearLayout.LayoutParams(context, null).apply {...})addView(LinearLayout(context).apply { ...}, LinearLayout.LayoutParams(context,null).apply {...})addView(Button(context).apply { setOnClickListener { ...}}, LinearLayout.LayoutParams(0,0).apply {...})
}

虽然代码已经借助 apply 等作用域函数进行了优化,但写起来仍然很繁琐,这样的代码是完全无法替代 XML 的。

接下来,本文带大家看看上述 DSL 是如何实现的,以及更进一步的优化技巧

4. Kotlin 如何实现 DSL

4.1 高阶函数实现大括号层级


常见的 DSL 都会用大括号来表现层级。Kotlin 的高阶函数允许指定一个 lambda 类型的参数,且当 lambda 位于参数列表的最后位置时可以脱离圆括号,满足 DSL 中的大括号语法要求。

我们知道了实现大括号语法的核心就是将对象创建及初始化逻辑封装成带有尾 lambda 的高阶函数中,我们按照这个思路改造下面代码

LinearLayout(context).apply {orientation = LinearLayout.HORIZONTALaddView(ImageView(context))
}

我们为 LinearLayout 的创建定义一个高阶函数,根据预设的 orientation 命名为 HorizontalLayout 以提高可读性。另外我们模仿 Compose 的风格使用首字母大写,让 DSL 节点更具辨识度

fun HorizontalLayout(context: Context, init: (LinearLayout) -> Unit) : LinearLayout {return LinearLayout(context).apply {orientation = LinearLayout.HORIZONTALinit(this)}
}

参数 init 是一个尾 lambda,传入刚创建的 LinearLayout 对象,便于我们在大括号中为其进行初始化。我们为 ImageView 也定义类似的高阶函数后,调用效果如下:

HorizontalLayout(context) {...it.addView(ImageView(context) {...})
}

虽然避免了 apply 的出现,但是效果仍然差强人意。

4.2 通过 Receiver 传递上下文


前面经高阶函数转化后的 DSL 中大括号内必须借助 it 进行初始化,而且 addView 的出现也难言优雅。首先,我们可以将 lambda 的参数改为 Receiver,大括号中对 it 的引用可以变为 this 并直接省略:

fun HorizontalLayout(context: Context, init: LinearLayout.() -> Unit) : LinearLayout {return LinearLayout(context).apply {orientation = LinearLayout.HORIZONTALinit()}
}

其次,我们如果能将 addView 隐藏到 ImageView 内部代码会更加简洁,这需要 ImageView 持有它的父 View 的引用,我们可以将参数 context 换成 ViewGroup

fun ImageView(parent: ViewGroup, init: ImageView.() -> Unit) {parent.addView(ImageView(parent.context).apply(init))
}

由于不再需要返回实例给父 View,返回值也可以改为 Unit 了。

按照前面参数转 Receiver 的思路,我们可以进一步上 ImageView 的 parent 参数提到 Receiver 的位置,实际就是改成 ViewGroup 的扩展函数:

fun ViewGroup.ImageView(init: ImageView.() -> Unit) {addView(ImageView(context).apply(init))
}

经过上面优化,DSL 中写 ImageView 时无需再传递参数 context,而且大括号中也不会出现 it

HorizontalLayout {...ImageView {...}
}

4.3 扩展函数优化代码风格


View 的固有方法签名都是为命令式语句设计的,不符合 DSL 的代码风格,此时可以借助 Kotlin 的扩展函数进行重新定义。

那么什么是 DSL 应该有的代码风格? 虽然不同功能的 DSL 不能一概而论,但是它们大都是偏向于对结构的静态描述,所以应该避免出现命令式的命名风格。

fun View.onClick(l: (v: View) -> Unit) {setOnClickListener(l)
}

比如上面这样,通过扩展函数使用 onClick 优化 setOnClickListener 命名,而且参数中使用函数类型替代了原有的 OnClickListener 接口类型,在 DSL 写起来更简单。由于 OnClickListener 是一个 SAM 接口,所以优势不够明显。下面的例子可能更能说明问题。

如果想在 DSL 中调用 TextView 的 addTextChangedListener 方法,写法上将非常冗余:

TextView {addTextChangedListener( object : TextWatcher {override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {...}override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {...}override fun afterTextChanged(s: Editable?) {...}})

为 TextView 新增适合 DSL 的扩展函数:

fun TextView.textChangedListener(init: _TextWatcher.() -> Unit) {val listener = _TextWatcher()listener.init()addTextChangedListener(listener)
}class _TextWatcher : android.text.TextWatcher {private var _onTextChanged: ((CharSequence?, Int, Int, Int) -> Unit)? = nulloverride fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {_onTextChanged?.invoke(s, start, before, count)}fun onTextChanged(listener: (CharSequence?, Int, Int, Int) -> Unit) {_onTextChanged = listener}// beforeTextChanged 和 afterTextChanged 的相关代码省略  }

DSL 中使用的效果如下,清爽了不少

Text {textChangedListener {beforeTextChanged { charSequence, i, i2, i3 ->//...}onTextChanged { charSequence, i, i2, i3 ->//...}afterTextChanged {//...}}
}

5. 进一步优化你的 DSL

经过前面的优化我们的 DSL 基本达到了预期效果,接下来通过更多 Kotlin 的特性让这套 DSL 更加好用。

5.1 infix 增强可读性


Kotlin 的中缀函数可以让函数省略圆点以及圆括号等程序符号,让语句更自然,进一步提升可读性。比如所有的 View 都有 setTag 方法,正常使用如下:

HorizontalLayout {setTag(1,"a")setTag(2,"b")
}

我们使用中缀函数来优化 setTag 的调用如下:

class _Tag(val view: View) {infix fun <B> Int.to(that: B) =  view.setTag(this, that)
}fun View.tag(block: _Tag.() -> Unit) {_Tag(this).apply(block)
}

DSL 中调用的效果如下:

HorizontalLayout {tag {1 to "a"2 to "b"}
}

5.2 @DslMarker 限制作用域


HorizontalLayout {// this: LinearLayout...TextView {//this : TextView// 此处仍然可以调用 HorizontalLayoutHorizontalLayout {...}}}

上述 DSL 代码,我们发现在 TextView {...} 可以调用 HorizontalLayout {...} ,这显示是不合逻辑的。由于 Text 的作用域同时处于父 HorizontalLayout 的作用域中,所以上面代码中,编译器会认为其内部的 HorizontalLayout {...} 是调用在 this@LinearLayout 中不会报错。缺少了编译器的提醒,会增大出现 Bug 的几率

Kotlin 为 DSL 的使用场景提供了 @DslMarker 注解,可以对方法的作用域进行限制。添加注解的 lambda 中在省略 this 的隐式调用时只能访问到最近的 Receiver 类型,当调用更外层的 Receiver 的方法会报错如下:

@DslMarker 是一个元注解,我们需要基于它定义自己的注解

@DslMarker
@Target(AnnotationTarget.TYPE)
annotation class ViewDslMarker

接着,在尾 lambda 的 Receiver 添加注解,如下:

fun ViewGroup.TextView(init: (@ViewDslMarker TextView).() -> Unit) {addView(TextView(context).apply(init))
}

TextView {...} 中如果不写 this. 则只能调用 TextView 的方法,如果想调用外层 Receiver 的方法,必须显示的使用 this@xxx 调用

5.3 Context Receivers 传递多个上下文


Context Receivers 是刚刚在 Kotlin 1.6.20-M1 中发布的新语法,它使函数定义时拥有多个 Receiver 成为可能。

context(View)
val Float.dp get() = this * this@View.resources.displayMetrics.densityclass SomeView : View {val someDimension = 4f.dp
}

上面代码是使用 Context Receivers 定义函数的例子,dp 是 Float 的扩展函数,所以已经有了一个 Receiver,在此基础上,通过 context(View) 又增加了 View 作为 Receiver,可以通过 this@xxx 引用不同 Receiver 完成运算。

context 的新特性乍看起来好像没啥用,但其实它对于 DSL 场景有很重要的意义,可以让我们的代码变得更智能。比如下面的例子

fun View.dp(value: Int): Int = (value * context.resources.displayMetrics.density).toInt()HorizontalLayout {TextView {layoutParams = LinearLayout.LayoutParams(context, null).apply {width = dp(60)height = 0weight = 1.0}}
}RelativeLayout {TextView {layoutParams = RelativeLayout.LayoutParams(context, null).apply {width = dp(60)height = ViewGroup.LayoutParams.WRAP_CONTENT}}
}

上面的代码中有几点可以使用 context 帮助改善。

首先,代码中使用带参数的 dp(60) 进行 dip 转换。我们可以通过前面介绍的 context 语法替换为 60f.dp 这样的写法 ,避免括号的出现,写起来更加舒适。

此外,我们知道 View 的 LayoutParams 的类型由其父 View 类型决定,上面代码中,我们在创建 LayoutParams 时必须时刻留意类型是否正确,心理负担很大。

这个问题也可以用 context 很好的解决,如下我们为 TextView 针对不同的 context 定义 layoutParams 扩展函数:

context(RelativeLayout)
fun TextView.layoutParams(block: RelativeLayout.LayoutParams.() -> Unit) {layoutParams = RelativeLayout.LayoutParams(context, null).apply(block)
}context(LinearLayout)
fun TextView.layoutParams(block: LinearLayout.LayoutParams.() -> Unit) {layoutParams = LinearLayout.LayoutParams(context, null).apply(block)
}

在 DSL 中使用效果如下:

TextView 的 layoutParams {...} 会根据父容器类型自动返回不同的 this 类型,便于后续配置。

5.4 使用 inline 和 @PublishedApi 提高性能


DSL 的实现使用了大量高阶函数,过多的 lambda 会产生过的匿名类,同时也会增加运行时对象创建的开销,不少 DSL 选择使用 inline 操作符,减少匿名类的产生,提高运行时性能。比如为 ImageView 的定义添加 inline :

inline fun ViewGroup.ImageView(init: ImageView.() -> Unit) {addView(ImageView(context).apply(init))
}

inline 函数内部调用的函数必须是 public 的,这会造成一些不必要的代码暴露,此时可以借助 @PublishedApi 化解。

//resInt 指定图片
inline fun ViewGroup.ImageView(resId: Int, init: ImageView.() -> Unit) {_ImageView(init).apply { setImageResource(resId) }
}//drawable 指定图片
inline fun ViewGroup.ImageView(drawable: Drawable, init: ImageView.() -> Unit) {_ImageView(init).apply { setImageDrawable(drawable) }
}@PublishedApi
internal inline fun ViewGroup._ImageView(init: ImageView.() -> Unit) =ImageView(context).apply {this@_ImageView.addView(this)init()}

如上,为了方便 DSL 中使用,我们定义了两个 ImageView 方法,分别用于 resId 和 drawable 的图片设置。由于大部分代码可以复用,我们抽出了一个 _ImageView 方法。但是由于要在 inline 方法中使用,所以编译器要求 _ImageView 必须是 public 类型。_ImageView 只需在库的内部服务,所以可以添加为 internal 的同时加 @PublishdApi 注解,它允许一个模块内部方法在 inline 中使用,且编译器不会报错。

6. 总结

经过上述几个步骤,我们的 DSL 终于成型了,而且还经过了优化,看看最终的样子:

val linearLayoutParams = LinearLayout.LayoutParams(context, null).apply {width = MATCH_PARENTheight = WRAP_CONTENT
}HorizontalLayout {ImageView(R.drawable.avatar) {layoutParams {width = 60f.dpheight = MATCH_PARENT}}VerticalLayout {Text("Andy Rubin") {textSize = 18.dplayoutParams = linearLayoutParams}Text("American computer programmer") {textSize = 14f.dplayoutParams = linearLayoutParams}layoutParams {width = dip(0)height = MATCH_PARENTweight = 1fgravity = Grivaty.CENTER}}Button("Follow") {onClick {//...}layoutParams {width = 120f.dpheight = MATCH_PARENT}}layoutParams = linearLayoutParams
}

当然 Android 中 DSL 远不止 UI 这一种使用场景 ,但是实现思路都是相近的,最后再来一起回顾一下基本步骤:

使用带有尾 lambda 的高阶函数实现大括号的层级调用
为 lambda 添加 Receiver,通过 this 传递上下文
通过扩展函数优化代码风格,DSL 中避免出现命令式的语义
infix 减少点号圆括号等符号的出现,提高可读性
@DslMarker 限制 DSL 作用域,避免错误调用
Context Receivers 传递多个上下文代码更智能(实验语法未来有变动可能)
inline 提升性能,同时使用 @PublishedApi 避免不必要的代码暴露

Kotlin DSL 实战相关推荐

  1. 像 Compose 那样写代码 :Kotlin DSL 原理与实战

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

  2. Spring Webflux: Kotlin DSL [片断]

    原文链接:https://dzone.com/articles/spring-webflux-kotlin-dsl-snippets 作者:Biju Kunjummen 译者:Jackie Tang ...

  3. 安卓开发重磅炸弹!程序员福利!《高级Kotlin强化实战学习手册(附Demo)》开放下载!

    前言 自Google宣布将 Kotlin 作为 Android 开发的首选语言 (Kotlin-first),现已有60% 的专业 Android 开发者已经采用了该编程语言.在 Google Pla ...

  4. kotlin dsl_Spring Webflux – Kotlin DSL –实现的演练

    kotlin dsl 在以前的博客文章中,我描述了Spring Web Framework中的响应式编程支持Spring Webflux如何使用基于Kotlin的DSL使用户能够以非常直观的方式描述路 ...

  5. Spring Webflux – Kotlin DSL –实现的演练

    在先前的博客文章中,我描述了Spring Web Framework中的响应式编程支持Spring Webflux如何使用基于Kotlin的DSL使用户能够以非常直观的方式描述路由. 在这里,我想探索 ...

  6. gradle kotlin_我对Gradle Kotlin DSL的第一印象

    gradle kotlin by Adam Arold 亚当·阿罗德(Adam Arold) 我对Gradle Kotlin DSL的第一印象 (My first impressions of Gra ...

  7. 《Kotin 极简教程》第14章 使用 Kotlin DSL

    第14章 使用 Kotlin DSL 最新上架!!!< Kotlin极简教程> 陈光剑 (机械工业出版社) 可直接打开京东,淘宝,当当===> 搜索: Kotlin 极简教程 htt ...

  8. Kotlin/DSL(Anko),原汁原味Kotlin开发Android---Activity Fragment与AnkoUI分离,强大的复用,更加便捷的开发

    /写在前面 翻开自己的CSDN,已经很久很久没有活动了,最近的关于PDF签章的博客还是16年写的.将近年关,工作内容阶段性告一段落,终于有时间写一下自己的东西. 废话少说,说说Kotlin.kotli ...

  9. Kotlin 编程实战

    code小生 一个专注大前端领域的技术平台 公众号回复Android加入安卓技术群 导读:Kotlin诞生于2011年,开源于2012年,吸收了Java等语言的优良特性,提供了令人惊艳的编程体验,是编 ...

最新文章

  1. 关于欧盟的芯片法案,ASML是这样看的!
  2. (提示)ubuntu16.04通过sealos安装k8s,需要重新部署apply一下calico组件
  3. ASP.NET 2.0运行时简要分析
  4. Java性能调优调查结果(第一部分)
  5. 硬核总结 9 个关于认证授权的常见问题!看看自己能回答几个!
  6. android 8.0 edittext,关注TextInputEditText的Android 8.0 Oreo崩溃
  7. android swf游戏下载工具,安卓swf游戏播放器下载
  8. python的统计库_Python-Scipy库-卡方分布统计量计算
  9. CentOS 编译安装 MySQL5.7
  10. LeetCode||颜色分类--给定一个包含红色、白色和蓝色,一共 *n* 个元素的数组,**原地**对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
  11. 科罗拉多大学波尔得分校计算机科学,科罗拉多大学波尔得分校院系设置
  12. 高射炮打蚊子丨用Visual Studio 2017写最初级的C语言程序
  13. FastQC 测序质量
  14. 炒股精髓:多位高手多年心血结晶(推荐)
  15. Android 集成华为HMS Scankit实现扫一扫二维码
  16. c语言矩阵的逆的程序,C语言求矩阵的逆矩阵
  17. 大数据之hive:行列转换系列总结
  18. XILINX 7系列FPGA_Slice_存储器_XADC篇
  19. 【训练题】航线设计 | 使用最长上升子序列(LIS)长度的O(nlogn)算法优化
  20. 川大667真题数据分析 | 利用Python和SPASS分析名词解释

热门文章

  1. 分享图表制作技巧_15个最有用的Windows 10提示和技巧信息图表
  2. 软件测试--用例测试方法综合选择(教育APP案例分析)
  3. App Store创赢艺术—— Apple 开发的赚钱机密
  4. 免费ZBlog插件批量采集发布管理全能插件
  5. PostgreSQL 9.4 patch : Row-Level Security
  6. 魅族android n内测报名,魅族17/Pro系列Android 11内测申请入口
  7. 中学生学计算机6,计算机教学中学生创新能力培养-计算机教学论文-计算机论文_1(6页)-原创力文档...
  8. 年薪80万!AI革了程序员的命
  9. Java 中的面向数据编程
  10. mysql 查询日期字段按年月查_Mysql 关于日期的查询 查询某年某月末日 或单年单月单日...