Kotlin Context Parameters 详解:别再把 Logger、事务和配置层层往下传

简介

项目代码写久了,很容易出现这种函数签名:

kotlin 复制代码
fun createOrder(
    userId: Long,
    productId: Long,
    logger: Logger,
    config: AppConfig,
    tx: Transaction
) {
    logger.info("create order")
    tx.execute("insert order")
}

真正的业务参数只有 userIdproductId

后面的 loggerconfigtx 更像"运行环境":

  • 日志对象
  • 配置对象
  • 当前登录用户
  • 数据库事务
  • 权限能力
  • 请求上下文
  • 监控埋点对象

这些对象通常不会在每一层变化,但又经常被一层层传下去。

于是代码就变成了这样:

kotlin 复制代码
fun controller(logger: Logger, config: AppConfig, tx: Transaction) {
    service(logger, config, tx)
}

fun service(logger: Logger, config: AppConfig, tx: Transaction) {
    repository(logger, config, tx)
}

fun repository(logger: Logger, config: AppConfig, tx: Transaction) {
    logger.info(config.env)
    tx.execute("insert")
}

业务还没展开,参数先排了一长串。

Kotlin Context Parameters 就是为了解决这类问题:

函数可以声明自己需要哪些上下文,调用时只要当前作用域里有这些上下文,编译器会自动匹配,不用手动一层层传。

它不是全局变量,也不是运行时反射注入,而是编译期能检查的隐式参数。

版本说明

Context Parameters 从 Kotlin 2.2.0 开始进入预览,到了 Kotlin 2.4.0 已经稳定。

如果使用 Kotlin 2.4.0 及以上版本,普通 Context Parameters 不再需要额外开启实验参数。

如果使用 Kotlin 2.2.x 或 2.3.x,需要在 build.gradle.kts 里开启:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xcontext-parameters")
    }
}

注意一点:

text 复制代码
-Xcontext-receivers 和 -Xcontext-parameters 不要同时打开

Context Receivers 是旧实验特性,Context Parameters 是替代方案。

Kotlin 2.4.0 里还有一个能力叫显式上下文参数调用,用来解决部分重载歧义。这个能力仍是实验性的,需要额外开启:

kotlin 复制代码
kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xexplicit-context-arguments")
    }
}

普通使用先不用急着碰它,后面会单独讲。

第一个问题:Logger 到底还要传多少层

先定义一个日志接口:

kotlin 复制代码
interface Logger {
    fun info(message: String)
}

class ConsoleLogger : Logger {
    override fun info(message: String) {
        println("[INFO] $message")
    }
}

普通写法:

kotlin 复制代码
fun saveUser(name: String, logger: Logger) {
    logger.info("save user: $name")
    println("保存用户 $name")
}

fun main() {
    val logger = ConsoleLogger()
    saveUser("Tom", logger)
}

输出:

text 复制代码
[INFO] save user: Tom
保存用户 Tom

这段代码没问题。

问题出现在调用链变长之后:

kotlin 复制代码
fun createUser(name: String, logger: Logger) {
    validateUser(name, logger)
    saveUser(name, logger)
}

fun validateUser(name: String, logger: Logger) {
    logger.info("validate user: $name")
}

fun saveUser(name: String, logger: Logger) {
    logger.info("save user: $name")
}

每一层都要带着 logger 走。

换成 Context Parameters:

kotlin 复制代码
context(logger: Logger)
fun createUser(name: String) {
    validateUser(name)
    saveUser(name)
}

context(logger: Logger)
fun validateUser(name: String) {
    logger.info("validate user: $name")
}

context(logger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
}

fun main() {
    val logger = ConsoleLogger()

    context(logger) {
        createUser("Tom")
    }
}

输出:

text 复制代码
[INFO] validate user: Tom
[INFO] save user: Tom

关键语法只有两个:

kotlin 复制代码
context(logger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
}

表示这个函数需要一个 Logger 上下文,并且在函数体里用名字 logger 访问它。

调用时:

kotlin 复制代码
context(logger) {
    createUser("Tom")
}

表示这段代码块里提供了一个 Logger 上下文。

可以把它理解成"编译器帮忙传参数"

下面这段:

kotlin 复制代码
context(logger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
}

可以先粗略理解成:

kotlin 复制代码
fun saveUser(name: String, logger: Logger) {
    logger.info("save user: $name")
}

区别在于:

text 复制代码
普通参数:调用时手动传
Context Parameters:编译器从当前上下文里找

所以 Context Parameters 不是"凭空出现的依赖"。

没有上下文时,代码直接编译失败:

kotlin 复制代码
fun main() {
    // 编译失败:缺少 Logger 上下文
    saveUser("Tom")
}

这比运行时才发现缺 Bean、缺 Service、缺配置要早得多。

多个上下文:日志、配置、数据库一起用

实际项目不会只有一个 Logger

再加两个对象:

kotlin 复制代码
data class AppConfig(
    val env: String,
    val enableAudit: Boolean
)

interface Database {
    fun insert(table: String, values: Map<String, Any>)
}

class MemoryDatabase : Database {
    override fun insert(table: String, values: Map<String, Any>) {
        println("INSERT INTO $table VALUES $values")
    }
}

一个函数可以声明多个上下文:

kotlin 复制代码
context(
    logger: Logger,
    config: AppConfig,
    db: Database
)
fun registerUser(name: String) {
    logger.info("env=${config.env}, register user: $name")

    db.insert(
        table = "users",
        values = mapOf(
            "name" to name,
            "audit" to config.enableAudit
        )
    )
}

调用:

kotlin 复制代码
fun main() {
    val logger = ConsoleLogger()
    val config = AppConfig(env = "dev", enableAudit = true)
    val db = MemoryDatabase()

    context(logger, config, db) {
        registerUser("Lucy")
    }
}

输出:

text 复制代码
[INFO] env=dev, register user: Lucy
INSERT INTO users VALUES {name=Lucy, audit=true}

编译器按类型查找当前上下文里的对象:

text 复制代码
Logger     -> logger
AppConfig  -> config
Database   -> db

函数签名里只保留真正的业务参数:

kotlin 复制代码
registerUser("Lucy")

这就是它最直接的价值。

用 context 块,还是用 with

提供上下文常见写法是 context(...) { }

kotlin 复制代码
context(logger, config, db) {
    registerUser("Lucy")
}

有些示例也会看到 with

kotlin 复制代码
with(logger) {
    registerUser("Lucy")
}

单个上下文时,with(logger) 能工作,因为 logger 成为当前接收者,编译器也能把它当作上下文候选。

多个上下文时,用 context(...) { } 更清楚:

kotlin 复制代码
context(logger, config, db) {
    registerUser("Lucy")
}

不需要写成多层嵌套:

kotlin 复制代码
with(logger) {
    with(config) {
        with(db) {
            registerUser("Lucy")
        }
    }
}

代码一多,context(...) { } 更适合表达"这里提供一组环境"。

Demo:事务范围,不再到处传 tx

事务是 Context Parameters 很适合的场景。

先定义一个事务对象:

kotlin 复制代码
interface Transaction {
    fun begin()
    fun commit()
    fun rollback()
    fun execute(sql: String)
}

class ConsoleTransaction : Transaction {
    override fun begin() = println("BEGIN")
    override fun commit() = println("COMMIT")
    override fun rollback() = println("ROLLBACK")
    override fun execute(sql: String) = println("SQL: $sql")
}

再写几个需要事务的函数:

kotlin 复制代码
data class OrderItem(
    val productId: Long,
    val count: Int
)

context(tx: Transaction, logger: Logger)
fun createOrder(userId: Long, items: List<OrderItem>) {
    logger.info("create order for user=$userId")
    tx.execute("insert into orders(user_id) values($userId)")

    items.forEach { item ->
        createOrderItem(item)
    }
}

context(tx: Transaction, logger: Logger)
fun createOrderItem(item: OrderItem) {
    logger.info("create order item product=${item.productId}")
    tx.execute(
        "insert into order_items(product_id, count) values(${item.productId}, ${item.count})"
    )
}

封装一个事务入口:

kotlin 复制代码
context(tx: Transaction, logger: Logger)
fun <T> transactional(block: context(Transaction, Logger) () -> T): T {
    tx.begin()

    return try {
        val result = block()
        tx.commit()
        result
    } catch (e: Exception) {
        tx.rollback()
        logger.info("transaction rollback: ${e.message}")
        throw e
    }
}

完整调用:

kotlin 复制代码
fun main() {
    val tx = ConsoleTransaction()
    val logger = ConsoleLogger()

    context(tx, logger) {
        transactional {
            createOrder(
                userId = 1001,
                items = listOf(
                    OrderItem(productId = 10, count = 2),
                    OrderItem(productId = 20, count = 1)
                )
            )
        }
    }
}

输出:

text 复制代码
BEGIN
[INFO] create order for user=1001
SQL: insert into orders(user_id) values(1001)
[INFO] create order item product=10
SQL: insert into order_items(product_id, count) values(10, 2)
[INFO] create order item product=20
SQL: insert into order_items(product_id, count) values(20, 1)
COMMIT

这里的重点不是少写几个参数,而是事务边界变清楚了:

kotlin 复制代码
context(tx, logger) {
    transactional {
        createOrder(...)
    }
}

这段代码表达的含义很直接:

text 复制代码
在这个事务和日志上下文里,执行一组订单逻辑

block: context(...) () -> T 是什么

上面这段可能有点陌生:

kotlin 复制代码
fun <T> transactional(block: context(Transaction, Logger) () -> T): T

它表示:

text 复制代码
block 是一个函数
这个函数执行时需要 Transaction 和 Logger 上下文
这个函数最终返回 T

普通函数类型长这样:

kotlin 复制代码
() -> T

带上下文的函数类型长这样:

kotlin 复制代码
context(Transaction, Logger) () -> T

所以 transactional { ... } 里的代码可以直接调用需要 TransactionLogger 的函数。

这种写法很适合封装:

  • 事务
  • 权限范围
  • 请求范围
  • 临时配置范围
  • DSL 构建范围

Demo:权限不是 if,而是一种编译期能力

很多系统里,删除用户必须有管理员权限。

常见写法:

kotlin 复制代码
fun deleteUser(id: Long, currentUser: CurrentUser) {
    if (!currentUser.isAdmin) {
        error("无权限")
    }

    println("delete user $id")
}

这种写法是运行时检查。

Context Parameters 可以把"能不能调用"提前到编译期。

先定义一个能力类型:

kotlin 复制代码
interface AdminPermission

object Admin : AdminPermission

删除函数声明自己需要管理员能力:

kotlin 复制代码
context(permission: AdminPermission, logger: Logger)
fun deleteUser(id: Long) {
    logger.info("delete user: $id")
    println("用户 $id 已删除")
}

调用:

kotlin 复制代码
fun main() {
    val logger = ConsoleLogger()

    context(Admin, logger) {
        deleteUser(1001)
    }
}

输出:

text 复制代码
[INFO] delete user: 1001
用户 1001 已删除

没有 AdminPermission 上下文时:

kotlin 复制代码
fun main() {
    val logger = ConsoleLogger()

    context(logger) {
        // 编译失败:缺少 AdminPermission
        deleteUser(1001)
    }
}

这类写法常被称为 Capability-Based Design,能力驱动设计。

deleteUser 的函数签名已经把限制写出来了:

kotlin 复制代码
context(permission: AdminPermission, logger: Logger)
fun deleteUser(id: Long)

含义是:

text 复制代码
只有拿到 AdminPermission 能力的代码区域,才能调用 deleteUser

Demo:小型 HTML DSL

Context Parameters 不只适合依赖注入,也适合 DSL。

先定义一个构建器:

kotlin 复制代码
class HtmlBuilder {
    private val lines = mutableListOf<String>()

    fun tag(name: String, content: String) {
        lines += "<$name>$content</$name>"
    }

    fun build(): String {
        return lines.joinToString("\n")
    }
}

定义几个 DSL 函数:

kotlin 复制代码
context(html: HtmlBuilder)
fun h1(text: String) {
    html.tag("h1", text)
}

context(html: HtmlBuilder)
fun p(text: String) {
    html.tag("p", text)
}

context(html: HtmlBuilder)
fun button(text: String) {
    html.tag("button", text)
}

再写一个入口函数:

kotlin 复制代码
fun html(block: context(HtmlBuilder) () -> Unit): String {
    val builder = HtmlBuilder()

    context(builder) {
        block()
    }

    return builder.build()
}

使用:

kotlin 复制代码
fun main() {
    val page = html {
        h1("Kotlin Context Parameters")
        p("把共享上下文放进作用域,让业务参数保持干净。")
        button("开始")
    }

    println(page)
}

输出:

text 复制代码
<h1>Kotlin Context Parameters</h1>
<p>把共享上下文放进作用域,让业务参数保持干净。</p>
<button>开始</button>

这个 DSL 的关键点是:

kotlin 复制代码
fun html(block: context(HtmlBuilder) () -> Unit): String

html { } 块里的函数都需要 HtmlBuilder 上下文,而入口函数负责创建并提供这个上下文。

调用方不用看到 HtmlBuilder,也不用手动传它。

带上下文的属性

Context Parameters 不只支持函数,也支持属性。

示例:

kotlin 复制代码
data class RequestContext(
    val requestId: String,
    val userId: Long
)

context(request: RequestContext)
val requestTag: String
    get() = "requestId=${request.requestId}, userId=${request.userId}"

使用:

kotlin 复制代码
fun main() {
    val request = RequestContext(
        requestId = "req-001",
        userId = 1001
    )

    context(request) {
        println(requestTag)
    }
}

输出:

text 复制代码
requestId=req-001, userId=1001

不过带上下文的属性有几个限制:

  • 不能有幕后字段
  • 不能写初始化器
  • 不能使用委托

所以这种属性一般写成计算属性:

kotlin 复制代码
context(request: RequestContext)
val requestTag: String
    get() = "requestId=${request.requestId}, userId=${request.userId}"

不要写成:

kotlin 复制代码
// 错误写法
context(request: RequestContext)
val requestTag: String = request.requestId

匿名上下文参数 _

有些函数不直接使用上下文对象,只是要求调用者处在某种能力范围内。

例如只要求有管理员权限,但函数体不关心权限对象本身:

kotlin 复制代码
interface AuditPermission

object AuditAdmin : AuditPermission

context(_: AuditPermission, logger: Logger)
fun exportAuditLog() {
    logger.info("export audit log")
    println("导出审计日志")
}

_ 表示:

text 复制代码
需要这个上下文,但函数体内不通过名字访问它

调用:

kotlin 复制代码
fun main() {
    val logger = ConsoleLogger()

    context(AuditAdmin, logger) {
        exportAuditLog()
    }
}

这种写法常见于"能力证明":

text 复制代码
只要能进入这个上下文,就说明具备调用资格

同类型上下文会歧义

Context Parameters 按类型匹配上下文。

如果同一层作用域里有两个同类型对象,就会产生歧义:

kotlin 复制代码
interface MessageSender {
    fun send(message: String)
}

class EmailSender : MessageSender {
    override fun send(message: String) {
        println("email: $message")
    }
}

class SmsSender : MessageSender {
    override fun send(message: String) {
        println("sms: $message")
    }
}

context(sender: MessageSender)
fun notifyUser(message: String) {
    sender.send(message)
}

fun main() {
    val email = EmailSender()
    val sms = SmsSender()

    context(email, sms) {
        // 编译失败:两个 MessageSender 都能匹配
        notifyUser("hello")
    }
}

编译器不会随便选一个。

解决方式之一是使用更具体的上下文类型:

kotlin 复制代码
context(sender: EmailSender)
fun sendEmail(message: String) {
    sender.send(message)
}

context(sender: SmsSender)
fun sendSms(message: String) {
    sender.send(message)
}

调用:

kotlin 复制代码
context(email, sms) {
    sendEmail("hello")
    sendSms("hello")
}

如果需要同名函数重载,Kotlin 2.4.0 提供了显式上下文参数调用,但这个能力仍是实验性的。

示例:

kotlin 复制代码
context(email: EmailSender)
fun sendNotification() {
    email.send("邮件通知")
}

context(sms: SmsSender)
fun sendNotification() {
    sms.send("短信通知")
}

context(defaultEmail: EmailSender, defaultSms: SmsSender)
fun notifyAllChannels() {
    sendNotification(email = defaultEmail)
    sendNotification(sms = defaultSms)
}

需要开启:

kotlin 复制代码
freeCompilerArgs.add("-Xexplicit-context-arguments")

普通业务代码里,更建议先用清晰的函数名或更具体的类型,少制造重载歧义。

和 Context Receivers 的区别

旧 Context Receivers 写法大致是这样:

kotlin 复制代码
context(Logger)
fun saveUser(name: String) {
    info("save user: $name")
}

Logger 像一个隐式 receiver,函数体里可以直接调用 info

Context Parameters 写法:

kotlin 复制代码
context(logger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
}

最大区别:

text 复制代码
Context Receivers:上下文像隐式 this
Context Parameters:上下文是有名字的参数

新写法看起来稍微啰嗦一点,但好处很明显:

kotlin 复制代码
context(logger: Logger, auditLogger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
    auditLogger.info("audit user: $name")
}

两个 Logger 角色不同,名字直接写清楚。

旧写法里,多个 receiver 混在同一个作用域,很容易看不出方法到底来自哪里。

和 DI 框架是什么关系

Context Parameters 很像轻量版依赖注入,但它不是 Spring、Koin、Dagger 的替代品。

可以这样理解:

text 复制代码
DI 框架:负责创建对象、管理生命周期、组装依赖图
Context Parameters:负责把已经存在的上下文带进一段代码

适合 Context Parameters 的场景:

  • 日志、事务、配置等横切对象
  • 请求范围内的用户、租户、traceId
  • 小型模块内部的依赖传递
  • DSL 作用域
  • 权限能力约束
  • 测试时替换上下文实现

不适合直接替代 DI 框架的场景:

  • 大型对象图自动装配
  • 复杂生命周期管理
  • 多环境 Bean 扫描
  • AOP、拦截器、代理增强
  • 框架级组件管理

更实际的用法是两者配合。

例如 Spring 负责创建 UserRepositoryLoggerTransactionManager,业务模块内部用 Context Parameters 减少重复透传。

和全局单例的区别

全局单例也能少传参数:

kotlin 复制代码
object GlobalLogger {
    fun info(message: String) {
        println(message)
    }
}

fun saveUser(name: String) {
    GlobalLogger.info("save user: $name")
}

看起来简单,但问题不少:

  • 测试替换麻烦
  • 并发场景容易混乱
  • 多租户、多请求上下文不好处理
  • 函数签名看不出依赖
  • 代码耦合到具体实现

Context Parameters 的依赖写在函数签名上:

kotlin 复制代码
context(logger: Logger)
fun saveUser(name: String)

调用处必须提供上下文:

kotlin 复制代码
context(TestLogger()) {
    saveUser("Tom")
}

依赖不是全局到处可见,而是只在指定作用域内可见。

什么时候适合用

适合用 Context Parameters 的代码,通常有这些特征:

text 复制代码
依赖在一段调用链里经常使用
依赖本身不是业务输入
依赖应该受作用域限制
缺少依赖时希望编译期报错

典型例子:

  • Logger
  • Transaction
  • RequestContext
  • CurrentUser
  • TenantContext
  • AppConfig
  • Clock
  • CoroutineScope
  • HtmlBuilder
  • AdminPermission

不太适合用的场景:

text 复制代码
只在一个函数里用一次的普通参数
业务含义很强的输入参数
每次调用都明显不同的值
读代码时必须明确看到的核心数据

例如 userIdamountproductIdpageSize 这类值,通常还是普通参数更清楚。

不要为了少写参数,把所有东西都塞进上下文。

一个完整小案例:请求上下文 + 日志 + 订单服务

最后串一个更接近业务的示例。

定义上下文:

kotlin 复制代码
data class RequestContext(
    val requestId: String,
    val userId: Long,
    val tenantId: String
)

interface OrderRepository {
    fun save(userId: Long, productId: Long)
}

class MemoryOrderRepository : OrderRepository {
    override fun save(userId: Long, productId: Long) {
        println("save order: userId=$userId, productId=$productId")
    }
}

定义业务函数:

kotlin 复制代码
context(
    request: RequestContext,
    logger: Logger,
    repo: OrderRepository
)
fun placeOrder(productId: Long) {
    logger.info(
        "requestId=${request.requestId}, tenant=${request.tenantId}, place order"
    )

    checkProduct(productId)
    repo.save(request.userId, productId)
}

context(request: RequestContext, logger: Logger)
fun checkProduct(productId: Long) {
    logger.info("user=${request.userId}, check product=$productId")
}

调用:

kotlin 复制代码
fun main() {
    val request = RequestContext(
        requestId = "req-20260610-001",
        userId = 1001,
        tenantId = "cn"
    )
    val logger = ConsoleLogger()
    val repo = MemoryOrderRepository()

    context(request, logger, repo) {
        placeOrder(productId = 9527)
    }
}

输出:

text 复制代码
[INFO] requestId=req-20260610-001, tenant=cn, place order
[INFO] user=1001, check product=9527
save order: userId=1001, productId=9527

placeOrder 的业务参数只有一个:

kotlin 复制代码
productId: Long

请求信息、日志、仓储对象都来自上下文。

函数签名仍然保留了依赖说明:

kotlin 复制代码
context(
    request: RequestContext,
    logger: Logger,
    repo: OrderRepository
)
fun placeOrder(productId: Long)

读签名就能知道:

text 复制代码
这段逻辑必须在请求上下文、日志上下文、订单仓储上下文里运行

常见坑

不是运行时注入

Context Parameters 是编译期解析。

缺少上下文时,编译失败,不会等到运行时报错。

不是全局变量

上下文只在当前作用域和内部调用链里可见。

kotlin 复制代码
context(logger) {
    saveUser("Tom")
}

// 这里已经离开 logger 上下文

同类型对象别放太多

多个同类型上下文容易歧义。

如果一个模块里经常同时需要两个 Logger,最好拆成不同接口:

kotlin 复制代码
interface BusinessLogger : Logger
interface AuditLogger : Logger

然后分别声明:

kotlin 复制代码
context(
    businessLogger: BusinessLogger,
    auditLogger: AuditLogger
)
fun saveUser(name: String) {
    businessLogger.info("save user: $name")
    auditLogger.info("audit user: $name")
}

比两个 Logger 混在一起更清楚。

不要隐藏核心业务参数

下面这种写法不推荐:

kotlin 复制代码
data class OrderInput(
    val userId: Long,
    val productId: Long
)

context(input: OrderInput)
fun placeOrder() {
    println(input.productId)
}

userIdproductId 是业务输入,不是环境。

更清楚的写法:

kotlin 复制代码
context(logger: Logger, repo: OrderRepository)
fun placeOrder(userId: Long, productId: Long) {
    logger.info("place order")
    repo.save(userId, productId)
}

上下文应该放"环境"和"能力",不要拿来藏业务数据。

总结

Context Parameters 的核心很简单:

kotlin 复制代码
context(logger: Logger)
fun saveUser(name: String) {
    logger.info("save user: $name")
}

它解决的是这类问题:

text 复制代码
Logger、Config、Transaction、RequestContext 这类对象到处透传

它带来的效果是:

  • 业务参数更干净
  • 共享上下文不用层层传
  • 依赖仍然写在函数签名上
  • 缺少上下文时编译期报错
  • DSL、事务、权限范围更容易表达

一句话概括:

Context Parameters 不是为了把参数藏起来,而是把"业务输入"和"运行环境"分开,让函数签名更像真正的业务动作。

相关推荐
Coffeeee5 小时前
Android功耗优化,哪些代码会让你的机子发热又耗电
android·架构·kotlin
Android打工仔5 小时前
Kotlin 编译器是如何把 suspend 函数变成状态机的?
kotlin
Android打工仔5 小时前
一个 suspend 函数,为什么会变成一个普通函数?
kotlin
苦瓜花1 天前
【Kotlin】初入门
android·开发语言·kotlin
Kapaseker1 天前
Rust 是如何处理异常的
rust·kotlin
冬日砥砺前行1 天前
Kotlin 协程:几个曾经被我误解的点
kotlin
plainGeekDev3 天前
libs 目录 → Gradle 依赖管理
android·java·kotlin
监督者修3 天前
从零构建 GIS 数据引擎:方案驱动架构的设计与实践
android·架构·kotlin
urkay-4 天前
Kotlin Flow分类
android·开发语言·kotlin
alexhilton5 天前
Kotlin DSL深度解析:从Gradle脚本到构建你自己的DSL
android·kotlin·android jetpack