Kotlin 成为 Android 首选语言后,越来越多的项目开始全面转向它。但高级特性用起来方便,用不好也容易带来性能和内存上的麻烦——我见过不少代码里 inline 到处飞、点击事件泄漏也没处理。
这里结合我封装的一个小工具库 KtKit(com.hi-dhl:ktkit),整理了几个实际项目中能用上的技巧:Contract 怎么帮编译器推断类型,注解如何消除不必要的警告,委托属性一行搞定传参,以及 Flow 怎样挡住点击事件的泄漏。
Contract:给编译器塞个小纸条
Kotlin 的 smart cast 挺智能,但一旦把判空逻辑抽到单独的扩展函数里,它就不认了。比如:
public inline fun String?.isNotNullOrEmpty(): Boolean {
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}
fun testString(name: String?) {
if (name.isNotNullOrEmpty()) {
println(name.length) // 编译报错:Smart cast to String is impossible
}
}

编译器不敢深入分析每个函数——否则编译耗时扛不住。这时可以靠 contract 告诉它:只要我这个函数返回 true,调用者就不是 null。
inline fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}
加上 contract 块后,上面那行 name.length 就能正常编译了。标准库里大量用到这个特性,习惯了就好。
注解:安抚编译器的唠叨
contract 是实验性 API,调用处需要 @ExperimentalContracts。如果觉得每个地方都加很烦,可以在声明 contract 的文件第一行加:
@file:OptIn(ExperimentalContracts::class)
另一个常见警告是 inline:普通方法用了 inline,编译器会提示「建议只在 lambda 参数或 reified 场景使用」。inline 本身是把双刃剑——方法体短的时候几乎没有收益,但强制内联反而可能导致字节码膨胀。如果想明确消除警告,可以加上:
@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
然后在对应方法上标记 @kotlin.internal.InlineOnly。这个注解会同时修改可见性,编译后函数变成 private:
// 编译前
@kotlin.internal.InlineOnly
inline fun showShortToast(context: Context, message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
// 编译后
@InlineOnly
private static final void showShortToast(Context $this$showShortToast, String message) {
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}

普通方法就别惦记这点了,编译器唠叨得有道理。
一行传参:委托属性真的香
Activity 或 Fragment 里接收 intent / arguments 很啰嗦。委托属性能帮你省掉样板代码。KtKit 里提供了两个简单 API:
class ProfileActivity : Activity() {
// 不带默认值
private val userPassword by intent<String>(KEY_USER_PASSWORD)
// 带默认值
private val userName by intent<String>(KEY_USER_NAME) { "Default Value" }
}
发送方同样一行搞定,支持 Android 任意序列化参数:
activity.startActivity<ProfileActivity> { arrayOf(KEY_USER_NAME to "ByteCode") }
// 或者直接传可变参数
activity.startActivity<ProfileActivity>(KEY_USER_NAME to "ByteCode")
// 需要结果回调
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
// 回传结果
setActivityResult(Activity.RESULT_OK,
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)
Fragment 之间同理,makeBundle 扩展把参数打包进去:
fun newInstance(): Fragment {
return LoginFragment().makeBundle(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
参数再多也不至于写一堆 putExtra / getStringExtra 了。
点击事件:用 Flow 锁住生命周期
View.setOnClickListener 直接设置匿名内部类是最常见的内存泄漏源头之一——界面销毁了但引用还在。KtKit 里用 callbackFlow 把点击事件转成 Flow,再通过 lifecycleScope 绑定生命周期,当 Activity/Fragment 销毁时自动取消监听:
fun View.clickFlow(): Flow<View> = callbackFlow {
setOnClickListener { safeOffer(it) }
awaitClose { setOnClickListener(null) }
}
inline fun View.click(lifecycle: LifecycleCoroutineScope, noinline onClick: (View) -> Unit) {
clickFlow().onEach { onClick(this) }.launchIn(lifecycle)
}
调用时只要传入 lifecycleScope:
view.click(lifecycleScope) { showShortToast("Click Event") }
还封装了防抖和延迟首次点击:clickTrigger、clickDelayed,默认间隔 500ms,也可以自己指定。
这套模式比手动在 onDestroy 置 null 安全得多,也清净得多。
这些技巧在 KtKit 里都能直接用,日常写代码时稍微留意一下,就能避免不少无谓的性能排查和时间损耗。

