为数不多的人知道的 Kotlin 技巧及解析(三)

本文没有什么奇淫技巧,都是一些在实际开发中常用的技巧

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。

众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled 方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。源码解析请查看 Android 资源加载源码分析一

因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 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 委托属性、高阶函数、扩展函数、内联、注解的使用等等。

如果想要使用文中的 API 需要将下列代码添加在模块级 build.gradle 文件内, 最新版本号请查看 版本记录

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) // 1
    }
}

正如你所见,只有字符串 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)  // 1
    }
}

相比于之前的代码,在 isNotNullOrEmpty() 函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true ,this 所指向对象就不为 null。 而在 Kotlin 标准库中大量的用到 contract 特性。 上述示例的使用可前往查看 KtKit/ProfileActivity.kt

Kotlin 注解在项目中的使用

contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts 注解才可以正常使用,但是如果添加 @ExperimentalContracts 注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。

@file:OptIn(ExperimentalContracts::class)

在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。

编译器建议我们将函数作为参数时使用 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
// 未添加 InlineOnly 编译后的代码
public static final void showShortToast(@NotNull Context $this$showShortToast, @NotNull String message) {
  ......
  Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}


// 添加 InlineOnly 编译后的代码
@InlineOnly
private static final void showShortToast(Context $this$showShortToast, String message) {
  ......
  Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}

关于注解完整的使用案例,可前往仓库 KtKit 查看。

一行代码接受 Activity 或者 Fragment 传递的参数

如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit 中提供了两个 API,根据实际情况使用即可。案例可前往查看 KtKit/ProfileActivity.kt

class ProfileActivity : Activity() {
    // 方式一: 不带默认值
    private val userPassword by intent<String>(KEY_USER_PASSWORD)
    
    // 方式二:带默认值:如果获取失败,返回一个默认值
    private val userName by intent<String>(KEY_USER_NAME) { "公众号:ByteCode" }
}

一行代码实现 Activity 之间传递参数

这个思路是参考了 anko 的实现,同样是提供了两个 API , 根据实际情况使用即可,可以传递 Android 支持的任意参数,案例可前往查看 KtKit/ProfileActivity.kt

// API:
activity.startActivity<ProfileActivity> {  arrayOf( KEY_USER_NAME to "ByteCode" ) }
activity.startActivity<ProfileActivity>( KEY_USER_NAME to "ByteCode" )

// Example: 
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" 
        )
    }
}

Activity 之间传递参数 和 并回传结果

// 方式一
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 支持的任意参数,源码前往查看 KtKit/LoginFragment.kt

// API: 
LoginFragment().makeBundle(  KEY_USER_NAME to "ByteCode" )
LoginFragment().makeBundle { arrayOf( KEY_USER_NAME to "ByteCode" ) }

// Example: 
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("公众号:ByteCode" }

延迟第一次点击事件

// 默认延迟时间是 500ms
view.clickDelayed(lifecycleScope){ showShortToast("公众号:ByteCode" }

// or
view.clickDelayed(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

防止多次点击

// 默认间隔时间是 500ms
view.clickTrigger(lifecycleScope){ showShortToast("公众号:ByteCode") }

// or
view.clickTrigger(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

但是 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 什么时候结束执行

源码中我将 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)
}

总结

仓库 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,但是目前还不是很完善,正在陆续将一些常用的功能,结合着 Kotlin 的高级函数的特性,不仅让代码可读性更强,使用更加简单,而且还可以帮助我们解决项目中常见的问题。

项目中引用了 spotless 插件,执行 ./gradlew spotlessApply 会将 Java 、Kotlin 、xml 、gradle 、md 、gitignore 等等文件按照官方标准去格式化代码。这也是 Google 提交代码的时候,推荐的方式。

全文到这里就结束了,如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ❤️❤️❤️

如果有帮助 点个赞 就是对我最大的鼓励
代码不止,文章不停


最后推荐我一直在更新维护的项目和网站:

  • 个人博客,将所有文章进行分类,欢迎前去查看 https://hi-dhl.com

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析

  • 剑指 offer 及国内外大厂面试题解:在线阅读

  • LeetCode 系列题解:在线阅读

  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis

  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation

  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容