跳到主要内容 Kotlin 高级技巧与原理解析:Contract、注解及工具库应用 | 极客日志
Kotlin 大前端 java
Kotlin 高级技巧与原理解析:Contract、注解及工具库应用 Kotlin 高级技巧与原理解析。介绍 Contract 特性解决智能转换失效问题,讲解 @ExperimentalContracts 及 Inline 修饰符的使用规范。演示 KtKit 工具库在 Activity/Fragment 参数传递、点击事件处理中的应用,特别是利用 Flow 避免内存泄漏的方案。涵盖委托属性、高阶函数及扩展函数等核心知识点。
板砖工程师 发布于 2025/2/7 更新于 2026/4/19 1 浏览Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队在项目中使用 Kotlin。
众所周知 XML 十分耗时,因此在 Android 10.0 上新增 tryInflatePrecompiled 方法,这是一个在编译期运行的优化。因为布局文件越复杂 XmlPullParser 解析 XML 越耗时,该方法根据 XML 预编译生成 compiled_view.dex,然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。
因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 Kotlin 去重写这部分 UI,但是门槛还是很高。随着 Jetpack Compose 的出现,其目的是让您更快、更轻松地构建原生 Android 应用,前不久 Google 正式发布了 Jetpack Compose 1.0。
Kotlin 优势已经体现在了方方面面,结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是如果使用不当会对性能造成一些损耗。
本文主要分享了 Kotlin 在实际项目中使用的技巧,以及如果使用不当会对 性能 和 内存 造成的那些影响以及如何规避这些问题。
通过这篇文章你将学习到以下内容:
什么是 Contract,以及如何使用?
Kotlin 注解在项目中的使用?
一行代码接受 Activity 或者 Fragment 传递的参数?
一行代码实现 Activity 之间传递参数?
一行代码实现 Fragment 之间传递参数?
一行代码实现点击事件,避免内存泄露?
KtKit 仓库
这篇文章主要围绕一个新库 KtKit 来介绍一些 Kotlin 技巧,正如其名 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,是 Jetpack ktx 系列的补充,涉及到了很多从 Kotlin 源码、Jetpack ktx、anko 等等知名的开源项目中学习到的技巧,包含了 Kotlin 委托属性、高阶函数、扩展函数、内联、注解的使用等等。
implementation "com.hi-dhl:ktkit:${ktkitVersion} "
什么是 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)
}
}
微信扫一扫,关注极客日志 微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
正如你所见,只有字符串 name 不为空时,才会进入注释 1 的地方,但是以上代码却无法正常编译,如下图所示。
编译器会告诉你一个编译错误,经过代码分析只有当字符串 name 不为空时,才会进入注释 1 的地方,但是编译器却无法正常推断出来,真的是编译器做不到吗?看看官方文档是如何解释的。
However, as soon as these checks are extracted in a separate function, all the smartcasts immediately disappear:
将检查提取到一个函数中,smart cast 所带来的效果都会消失
编译器无法深入分析每一个函数,原因在于实际开发中我们可能写出更加复杂的代码,而 Kotlin 编译器进行了大量的静态分析,如果编译器去分析每一个函数,需要花费时间分析上下文,增加它的编译耗时的时间。
如果要解决上述问题,这就需要用到 Contract 特性,Contract 是 Kotlin 提供的非常有用的特性,Contract 的作用就是当 Kotlin 编译器没有足够的信息去分析函数的情况的时候,Contracts 可以为函数提供附加信息,帮助 Kotlin 编译器去分析函数的情况,修改代码如下所示。
inline fun String?.isNotNullOrEmpty () : Boolean {
contract {
returns(true ) implies (this @isNotNullOrEmpty != null )
}
return this != null && !this .trim().equals("null" , true ) && this .trim().isNotEmpty()
}
fun testString (name: String ?) {
if (name != null && name.isNotNullOrEmpty()) {
println(name.length)
}
}
相比于之前的代码,在 isNotNullOrEmpty() 函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true,this 所指向对象就不为 null。而在 Kotlin 标准库中大量用到 contract 特性。
Kotlin 注解在项目中的使用 contract 是添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts 注解才可以正常使用,但是如果添加 @ExperimentalContracts 注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。
@file:OptIn (ExperimentalContracts::class )
在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。
编译器建议我们将函数作为参数时使用 Inline,Inline () 的作用:提升运行效率,调用被 inline 修饰符的函数,会将方法内的代码段放到调用处。
既然 Inline 修饰符可以提升运行效率,为什么还给出警告,因为 Inline 修饰符的滥用会带来性能损失。
Inline 修饰符常用于下面的情况,编译器才不会有警告:
将函数作为参数(例如:lambda 表达式)
结合 reified 实化类型参数一起使用
但是在普通的方法中,使用 Inline 修饰符,编译会给出警告,如果方法体的代码段很短,想要通过 Inline 修饰符提升性能(虽然微乎其微),可以在文件的第一行添加下列代码,可消除警告。
@file:Suppress ("INVISIBLE_REFERENCE" , "INVISIBLE_MEMBER" )
然后在使用 Inline 修饰符的地方添加以下注解,即可愉快的使用。
@kotlin .internal .InlineOnly
注解 @kotlin.internal.InlineOnly 的作用:
消除编译器的警告
修改内联函数的可见性,在编译时修改成 private
public static final void showShortToast(@NotNull Context $this $showShortToast, @NotNull String message) {
......
Toast.makeText($this $showShortToast, (CharSequence)message, 0 ).show();
}
@InlineOnly
private static final void showShortToast(Context $this $showShortToast, String message) {
......
Toast.makeText($this $showShortToast, (CharSequence)message, 0 ).show();
}
一行代码接受 Activity 或者 Fragment 传递的参数 如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 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" }
}
一行代码实现 Activity 之间传递参数 这个思路是参考了相关实现的,同样是提供了两个 API,根据实际情况使用即可,可以传递 Android 支持的任意参数。
activity.startActivity<ProfileActivity> { arrayOf(KEY_USER_NAME to "ByteCode" ) }
activity.startActivity<ProfileActivity>(KEY_USER_NAME to "ByteCode" )
class ProfileActivity : Activity () {
......
companion object {
......
activity.startActivity<ProfileActivity> {
arrayOf(
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
}
activity.startActivity<ProfileActivity>(
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
}
}
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE) {
arrayOf(
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
}
setActivityResult(Activity.RESULT_OK) {
arrayOf(
KEY_RESULT to "success" ,
KEY_USER_NAME to "ByteCode"
)
}
setActivityResult(
Activity.RESULT_OK,
KEY_RESULT to "success" ,
KEY_USER_NAME to "ByteCode"
)
一行代码实现 Fragment 之间传递参数 和 Activity 一样提供了两个 API 根据实际情况使用即可,可以传递 Android 支持的任意参数。
LoginFragment().makeBundle(KEY_USER_NAME to "ByteCode" )
LoginFragment().makeBundle { arrayOf(KEY_USER_NAME to "ByteCode" ) }
class LoginFragment : Fragment (R.layout.fragment_login) {
......
companion object {
......
fun newInstance1 () : Fragment {
return LoginFragment().makeBundle(
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
}
fun newInstance2 () : Fragment {
return LoginFragment().makeBundle {
arrayOf(
KEY_USER_NAME to "ByteCode" ,
KEY_USER_PASSWORD to "1024"
)
}
}
}
}
一行代码实现点击事件,避免内存泄露 KtKit 提供了常用的三个 API:单击事件、延迟第一次点击事件、防止多次点击
view.click(lifecycleScope) { showShortToast("Click Event" ) }
view.clickDelayed(lifecycleScope){ showShortToast("Click Event" ) }
view.clickDelayed(lifecycleScope, 1000 ){ showShortToast("Click Event" ) }
view.clickTrigger(lifecycleScope){ showShortToast("Click Event" ) }
view.clickTrigger(lifecycleScope, 1000 ){ showShortToast("Click Event" ) }
但是 View#setOnClickListener 造成的内存泄露,如果做过性能优化的同学应该会见到很多这种 case。
根本原因在于不规范的使用,在做业务开发的时候,根本不会关注这些,那么如何避免这个问题呢,Kotlin Flow 提供了一个非常有用的 API callbackFlow,源码如下所示。
fun View.clickFlow () : Flow<View> {
return callbackFlow {
setOnClickListener {
safeOffer(it)
}
awaitClose { setOnClickListener(null ) }
}
}
callbackFlow 正如其名将一个 callback 转换成 flow,awaitClose 会在 flow 结束时执行。
源码中我将 Flow 通过 lifecycleScope 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow,flow 结束时会将 Listener 置为 null,有效的避免内存泄漏,源码如下所示。
inline fun View.click (lifecycle: LifecycleCoroutineScope , noinline onClick: (view : View ) -> Unit ) {
clickFlow().onEach {
onClick(this )
}.launchIn(lifecycle)
}
结语 本文介绍了 Kotlin 高级特性 Contract 的实际应用,以及利用 KtKit 工具库简化 Android 开发流程的方法。通过合理使用内联函数、委托属性和 Flow 协程,可以有效提升代码质量并避免常见的内存泄漏问题。