8、DSL在布局中的应用:Anko布局DSL(历史与现状)、Jetpack Compose的声明式UI与DSL思想、自定义DSL构建布局、DSL与XML布局的对比

说到Android布局,大家第一反应肯定是XML。这玩意儿用了快十年了,确实稳定。但说实话,写多了真的有点烦——findViewById、类型转换、嵌套层级一多眼睛都看花。后来Kotlin来了,带来了DSL这个利器,布局这件事儿终于有了新玩法。

今天我们就聊聊DSL在布局中的那些事儿。我会结合自己踩过的坑,把Anko、Compose、自定义DSL以及和XML的对比,一次性讲清楚。

8.1 Anko布局DSL:曾经的明星,现在的过客

Anko是JetBrains早期推出的一个库,目的就是用Kotlin代码替代XML写布局。我记得2017年刚接触Kotlin时,看到Anko的DSL写法,简直惊为天人——原来布局还能这么写?

verticalLayout {
    val name = editText()
    button("Say Hello") {
        onClick { toast("Hello, ${name.text}!") }
    }
}

你看,没有XML,没有findViewById,代码就是布局,布局就是代码。这种体验在当时是非常超前的。

但为什么我说它是「过客」呢?因为Anko有几个硬伤:

  • 性能问题:Anko在运行时通过DSL构建View树,比XML解析慢不少。我在一个复杂页面中实测过,Anko的构建耗时大约是XML的2-3倍。
  • 预览困难:XML有布局预览,Anko没有。改个边距得跑一遍App,调试效率低。
  • 维护停滞:JetBrains后来把重心转向了Compose,Anko基本处于半废弃状态。

避坑指南:我曾经在一个中型项目里全面使用Anko,结果半年后Compose出来了,迁移成本极高。如果你现在还在用Anko,建议尽早规划迁移路径。新项目就别碰了。

8.2 Jetpack Compose:声明式UI的DSL思想

Compose的出现,彻底改变了Android布局的玩法。它把DSL思想发挥到了极致——不是用代码描述布局,而是用代码声明布局。

什么叫声明式?说白了就是:你告诉系统「我想要什么」,而不是「你该怎么做」。

@Composable
fun Greeting(name: String) {
    Column(
        modifier = Modifier.padding(16.dp)
    ) {
        Text(text = "Hello, $name!")
        Button(onClick = { /* do something */ }) {
            Text("Click me")
        }
    }
}

这段代码看起来像DSL,实际上就是Kotlin的高阶函数 + 尾随lambda。Column是一个函数,它接收一个lambda作为内容。Text和Button也是函数,它们嵌套在Column的lambda里。

Compose的DSL思想体现在几个方面:

  • 组合优于继承:用@Composable函数组合UI,而不是继承View类
  • 状态驱动:UI是状态的函数,状态变了UI自动更新
  • 声明式:你描述UI应该长什么样,框架负责实现

个人经验:我刚开始用Compose时,总想着「怎么控制刷新时机」,后来才明白——你不需要控制,你只需要声明状态和UI的关系,剩下的交给Compose。这种思维转变,比学API本身更重要。

8.3 自定义DSL构建布局

除了Anko和Compose,我们也可以自己写DSL来构建布局。虽然不常用,但理解它的原理,能帮你更深刻地理解DSL的本质。

来看一个简单的例子:用DSL构建一个线性布局。

class LinearLayoutBuilder(private val context: Context) {
    private val views = mutableListOf<View>()

    fun text(text: String, init: TextView.() -> Unit = {}) {
        val tv = TextView(context).apply {
            this.text = text
            init()
        }
        views.add(tv)
    }

    fun button(text: String, init: Button.() -> Unit = {}) {
        val btn = Button(context).apply {
            this.text = text
            init()
        }
        views.add(btn)
    }

    fun build(): LinearLayout {
        return LinearLayout(context).apply {
            orientation = LinearLayout.VERTICAL
            views.forEach { addView(it) }
        }
    }
}

fun Context.linearLayout(init: LinearLayoutBuilder.() -> Unit): LinearLayout {
    val builder = LinearLayoutBuilder(this)
    builder.init()
    return builder.build()
}

使用起来就像这样:

linearLayout {
    text("Hello") {
        textSize = 16f
        setTextColor(Color.RED)
    }
    button("Click") {
        setOnClickListener { toast("clicked") }
    }
}

嗯,这里的关键是:接收者lambda。LinearLayoutBuilder的init函数是一个带接收者的lambda,接收者就是LinearLayoutBuilder本身。所以在lambda内部,text()和button()方法可以直接调用。

小技巧:自定义DSL时,记得用DslMarker注解标记你的Builder类。这样能防止外部lambda意外访问到内部作用域,避免混淆。我一开始没加这个注解,结果在嵌套时出了不少奇怪的bug。

8.4 DSL与XML布局的对比

说了这么多,到底该用DSL还是XML?我整理了一个对比表,方便你决策:

对比维度 XML布局 DSL布局(如Compose)
学习曲线 低,入门快 中等,需要理解声明式思想
预览能力 强,实时预览 Compose有预览,自定义DSL无
代码量 较多,需要findViewById 少,无需findViewById
类型安全 弱,运行时才能发现类型错误 强,编译时检查
性能 解析XML有开销,但可接受 Compose性能优秀,自定义DSL看实现
动态布局 困难,需要写大量Java/Kotlin代码 天然支持,代码即布局
团队协作 设计师可直接修改XML 需要开发人员参与

我的建议是:

  • 新项目:直接用Compose,别犹豫。Google已经明确Compose是Android UI的未来。
  • 老项目:逐步迁移。先把新页面用Compose写,老页面保持XML。别一次性全改,容易出问题。
  • 自定义DSL:除非你有特殊需求(比如构建一个内部工具),否则不建议在生产环境中自己造轮子。

一句话总结:XML是过去,Anko是过渡,Compose是未来。但理解DSL的思想,能让你在任何UI框架中都游刃有余。

DSL在布局中的应用 · 知识体系 DSL布局 Anko DSL Jetpack Compose 自定义DSL XML布局对比 历史明星 · 性能问题 · 已停滞 声明式 · 状态驱动 · 组合优先 接收者lambda · DslMarker 预览强 · 类型弱 · 动态难 XML是过去 · Anko是过渡 · Compose是未来

公众号:蓝海资料掘金营,微信deep3321