Kotlin DSL
DSL 总体来说是非常简洁的,在使用 Android 开发 java 的过程中,貌似么有使用过 DSL之类的写法,而在 kotlin 中,使用最多的 DSL 就是 anko
根据 anko 的官方文档说明,使用 DSL 有以下好处(也就是使用 xml 的坏处):
By default, UI in Android is written using XML. That is inconvenient in the following ways:
- It is not typesafe 非类型安全的,也即是 findView 的时候,需要注意类型
- It is not null-safe 可能为空的
- It forces you to write almost the same code for every layout you make layout 布局里面存在大量的共同代码,复用率较低
- XML is parsed on the device wasting CPU time and battery xml 解析消耗额外的 cpu 和时间
- Most of all, it allows no code reuse 代码不存在复用关系
同样对比传统的 硬编码编写UI 来说,anko 也存在吸引点,就是代码简洁。
例如,传统的硬编码编写 UI 如下:
val act = this
val layout = LinearLayout(act)
layout.orientation = LinearLayout.VERTICAL
val name = EditText(act)
val button = Button(act)
button.text = "Say Hello"
button.setOnClickListener {
Toast.makeText(act, "Hello, ${name.text}!", Toast.LENGTH_SHORT).show()
}
layout.addView(name)
layout.addView(button)
但是如果使用 anko-layout:
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello, ${name.text}!") }
}
}
anko-layout 原理
简单来说,anko-layout 的原理就是扩展函数以及DSL,例如在 AnkoLayoutActivity.kt 中有如下代码:
class AnkoLayoutActivity:Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun initLayout(){
verticalLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
}
}
实际上使用的是 Activity 的一个扩展函数,如下:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
后面的操作,实际上就是帮你创建一个 LinearLayout ,这个 LinearLayout 也是特殊的 LinearLayout,增加了对应的扩展方法,如下:
open class _LinearLayout(ctx: Context): LinearLayout(ctx) {
inline fun <T: View> T.lparams(
c: Context?,
attrs: AttributeSet?,
init: LinearLayout.LayoutParams.() -> Unit
): T {
val layoutParams = LinearLayout.LayoutParams(c!!, attrs!!)
layoutParams.init()
this@lparams.layoutParams = layoutParams
return this
}
......
}
整体流程图如下:
(图片来源网络)
View的创建一层一层的传递下去,其中共有三种情况,即创建View的三种上下文:
- Activity,在Activity的onCreate中调用,判断到最后通过setContentView来填充整个布局
- Context & Fragment,在其他场景下创建的View,可直接通过UI { } 域生成相应的View
- AnkoContext,在自定义AnkoComponent中的createView方法中定义
也就意味着,你可以在 Activity,Fragment,或者自定义 ViewGroup 中使用 Anko-layout.
原生DSL
anko 支持 DSL 实现原理有两个要素:
我们以一个案例讲解这里的用法:
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
首先,在Activity 的扩展函数 verticalLayout() 里面,传入一个 lambdas 作为参数,init: (@AnkoViewDslMarker _LinearLayout).() -> Unit),然后这个命名为 init 的函数,是一个带接受者的函数,也就是 _LinearLayout.() -> Unit,其接受者类型为 _LinearLayout,这个类继承自 LinearLayout。
上述代码可以简化成这样:
fun Context.createLinearLayout(init:LinearLayout.() ->Unit):LinearLayout{
val layout = LinearLayout(this)
layout.init()
return layout
}
所以你在某种 Context 内调用这个 createLinearLayout 的时候,就可以直接传入一个 Lambdas 表达式,其this 指向 LinearLayout,所以你调用该方法的时候,如下:
//anko 的写法
verticalLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
//我们自己的写法
createLinearLayout {
backgroundColor = Color.parseColor("#eeeeee")
}
DslMarker
其次Anko 为了提高 DSL 的效率,还是使用了 @DslMarker 注解:
//Ui.kt 中
@DslMarker
@Target(AnnotationTarget.TYPE)
annotation class AnkoViewDslMarker
//Annotatios.kt 中
@Target(ANNOTATION_CLASS)
@Retention(BINARY)
@MustBeDocumented
@SinceKotlin("1.1")
public annotation class DslMarker
根据kotlin 官方文档的解释:
The general rule:
-
an implicit receiver may belong to a DSL @X if marked with a corresponding DSL marker annotation
-
two implicit receivers of the same DSL are not accessible in the same scope
-
the closest one wins
-
other available receivers are resolved as usual, but if the resulting resolved call binds to such a receiver, it’s a compilation error
这里规定了DSL 的一些有意义的规范,例如DSL 块必须有接收者,其次不允许在一个代码块,存在两个相同的接收者,只要靠近代码块那个,才会生效。
简单来说,DslMarker 是为了控制作用范围,例如以下的情况:
html {
head {
head { } // 错误:外部接收者的成员
}
// ……
}
也就是说,第二个 head 代码块的接受者实际上应该是 html,但是在这种情况下,明显就不是了,所以使用 DslMarker,编译器会提示这种错误。