阅读 285

Kotlin 编写好用的工具方法[第一行代码 Kotlin 学习笔记]

编写好用的工具方法

到目前为止,我们已经将 Kotlin 大部分系统性的知识点学习完了。掌握了如此多的 Kotlin 特性,你知道该如何对它们进行灵活运用吗?

事实上,Kotlin 提供的丰富语法特性给我们提供了无限扩展的可能,各种复杂的 API 经过特殊的封装处理之后都能变得简单易用。比如我们之前体验过的 KTX 库,就是 Google 为了简化许多 API 的用法而专门设计的。不过 KTX 库所能覆盖到的功能毕竟有限,因此最重要的还是我们要能养成对 Kotlin 的各种特性进行灵活运用的意识。那么在本节的 Kotlin 课堂中,我将带你对几个常用 API 的用法进行简化,从而编写出一些好用的工具方法。

求 N 个数的最大最小值

两个数比大小这个功能,相信每一位开发者都遇到过。如果我想要获取两个数中较大的那个数,除了使用最基本的 if 语句之外,还可以借助 Kotlin 内置的 max() 函数,如下所示:

val a = 10
val b = 15
val larger  = max(a, b)
复制代码

这种代码看上去简单直观,也很容易理解,因此好像并没有什么优化的必要。

可是现在如果我们想要在 3 个数中获取最大的那个数,应该怎么写呢?由于 max() 函数只能接收两个参数,因此需要先比较前两个数的大小,然后再拿较大的那个数和剩余的数进行比较,写法如下:

val a = 10
val b = 15
val c = 5
val larger = max(max(a, b), c)
复制代码

有没有觉得代码开始变得复杂了呢?3 个数中获取最大值就需要使用这种嵌套 max() 函数的写法了,那如果是 4 个数、5 个数呢?没错,这个时候你就应该意识到,我们是可以对 max() 函数的用法进行简化的。

回顾一下,我们之前学过的 vararg 关键字,它允许方法接收任意多个同等类型的参数,正好满足我们这里的需求。那么我们就可以新建一个 Max.kt 文件,并在其中自定义一个 max() 函数,如下所示:

fun max(vararg nums: Int): Int {
    var maxNum = Int.MIN_VALUE
    for (num in nums) {
        maxNum = kotlin.math.max(num, maxNum)
    }
    return maxNum
}
复制代码

可以看到,这里 max() 函数的参数声明中使用了 vararg 关键字,也就是说现在它可以接收任意多个整型参数。接着我们使用了一个 maxNum 变量来记录所有数的最大值,并在一开始将它赋值成了整型范围的最小值。然后使用 for-in 循环遍历 nums 参数列表,如果发现当前遍历的数字比 maxNum 更大,就将 maxNum 的值更新成这个数,最终将 maxNum 返回即可。

仅仅经过这样的一层封装之后,我们在使用 max() 函数时就会有翻天覆地的变化,比如刚才同样的功能,现在就可以使用如下的写法来实现:

val a = 10
val b = 15
val c = 5
val larger = max(a, b, c)
println(larger)
复制代码

这样我们就彻底摆脱了嵌套函数调用的写法,现在不管是求 3 个数的最大值还是求 N 个数的最大值,只需要不断地给 max() 函数传入参数就可以了。

不过,目前我们自定义的 max() 函数还有一个缺点,就是它只能求 N 个整型数字的最大值,如果我还想求 N 个浮点型或长整型数字的最大值,该怎么办呢?当然你可以定义很多个 max() 函数的重载,来接收不同类型的参数,因为 Kotlin 中内置的 max() 函数也是这么做的。但是这种方案实现起来过于烦琐,而且还会产生大量的重复代码,因此这里我准备使用一种更加巧妙的做法。

Java 中规定,所有类型的数字都是可比较的,因此必须实现 Comparable 接口,这个规则在 Kotlin 中也同样成立。那么我们就可以借助泛型,将 max() 函数修改成接收任意多个实现 Comparable 接口的参数,代码如下所示:

fun <T : Comparable<T>> max(vararg nums: T): T {
    if (nums.isEmpty()) {
        throw RuntimeException("Param can not be empty.")
    }
    var maxNums = nums[0]
    for (num in nums) {
        if (num > maxNums) {
            maxNums = num
        }
    }
    return maxNums
}
复制代码

可以看到,这里将泛型 T 的上界指定成了 Comparable,那么参数 T 就必然是 Comparable 的子类型了。接下来,我们判断 nums 参数列表是否为空,如果为空的话就主动抛出一个异常,提醒调用者 max() 函数必须传入参数。紧接着将 maxNum 的值赋值成 nums 参数列表中第一个参数的值,然后同样是遍历参数列表,如果发现了更大的值就对 maxNum 进行更新。

经过这样的修改之后,我们就可以更加灵活地使用 max() 函数了,比如说求 3 个浮点型数字的最大值,同样也变得轻而易举:

 val a = 3.5f
 val b = 4.8f
 val c = 4.1f
 val larger = max(a, b, c)
复制代码

而且现在不管是双精度浮点型、单精度浮点型,还是短整型、整型、长整型,只要是实现 Comparable 接口的子类型,max() 函数全部支持获取它们的最大值,是一种一劳永逸的做法。

而如果你想获取 N 个数的最小值,实现的方式也是类似的,只需要定义一个 min() 函数就可以了,这个功能就当作课后习题留给你来完成吧。

简化 Toast 的用法

我们在本书中已经使用过太多次 Toast,相信你已经非常熟悉了,但是用了这么久,你有没有觉得 Toast 用法其实有些烦琐呢?

首先回顾一下 Toast 的标准用法吧,如果想要在界面上弹出一段文字提示需要这样写:

Toast.makeText(context, "This is Toast", Toast.LENGTH_SHORT).show()
复制代码

是不是很长的一段代码?而且曾经不知道有多少人因为忘记调用最后的 show() 方法,导致 Toast 无法弹出,从而产生一些千奇百怪的 bug。

由于 Toast 是非常常用的功能,每次都需要编写这么长的一段代码确实让人很头疼,这个时候你就应该考虑对 Toast 的用法进行简化了。

我们来分析一下,Toast 的 makeText() 方法接收 3 个参数:第一个参数是 Toast 显示的上下文环境,必不可少;第二个参数是 Toast 显示的内容,需要由调用方进行指定,可以传入字符串和字符串资源 id 两种类型;第三个参数是 Toast 显示的时长,只支持 Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG 这两种值,相对来说变化不大。

那么我们就可以给 String 类和 Int 类各添加一个扩展函数,并在里面封装弹出 Toast 的具体逻辑。这样以后每次想要弹出 Toast 提示时,只需要调用它们的扩展函数就可以了。

新建一个 Toast.kt 文件,并在其中编写如下代码:

fun String.showToast(context: Context) {
    Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}

fun Int.showToast(context: Context) {
    Toast.makeText(context, this, Toast.LENGTH_SHORT).show()
}
复制代码

这里分别给 String 类和 Int 类新增了一个 showToast() 函数,并让它们都接收一个 Context 参数。然后在函数的内部,我们仍然使用了 Toast 原生 API 用法,只是将弹出的内容改成了 this,另外将 Toast 的显示时长固定设置成 Toast.LENGTH_SHORT。

那么经过这样的扩展之后,我们以后在使用 Toast 时可以变得多么简单呢?体验一下就知道了,比如同样弹出一段文字提醒就可以这么写:

"This is Toast".showToast(context)
复制代码

怎么样,比起原生 Toast 的用法,有没有觉得这种写法畅快多了呢?另外,这只是直接弹出一段字符串文本的写法,如果你想弹出一个定义在 strings.xml 中的字符串资源,也非常简单,写法如下:

R.string.app_name.showToast(context)
复制代码

这两种写法分别调用的就是我们刚才在 String 类和 Int 类中添加的 showToast() 扩展函数。

当然,这种写法其实还存在一个问题,就是 Toast 的显示时长被固定了,如果我现在想要使用 Toast.LENGTH_LONG 类型的显示时长该怎么办呢?要解决这个问题,其实最简单的做法就是在 showToast() 函数中再声明一个显示时长参数,但是这样每次调用 showToast() 函数时都要额外多传入一个参数,无疑增加了使用复杂度。

不知道你现在有没有受到什么启发呢?回顾一下,我们在第 2 章学习 Kotlin 基础语法的时候,曾经学过给函数设定参数默认值的功能。只要借助这个功能,我们就可以在不增加 showToast() 函数使用复杂度的情况下,又让它可以支持动态指定显示时长了。修改 Toast.kt 中的代码,如下所示:

fun String.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, this, duration).show()
}

fun Int.showToast(context: Context, duration: Int = Toast.LENGTH_SHORT) {
    Toast.makeText(context, this, duration).show()
}
复制代码

可以看到,我们给 showToast() 函数增加了一个显示时长参数,但同时也给它指定了一个参数默认值。这样我们之前所使用的 showToast() 函数的写法将完全不受影响,默认会使用 Toast.LENGTH_SHORT 类型的显示时长。而如果你想要使用 Toast.LENGTH_LONG 的显示时长,只需要这样写就可以了:

"This is Toast".showToast(context, Toast.LENGTH_LONG)
复制代码

相信我,这样的Toast工具一定会给你的开发效率带来巨大的提升。

简化 Snackbar 的用法

Snackbar 和 Toast 的用法基本类似,但是又比 Toast 稍微复杂一些。

先来回顾一下 Snackbar 的常规用法吧,如下所示:

Snackbar.make(view, "This is Snackbar", Snackbar.LENGTH_SHORT)
    .setAction("Action") {
        // 处理具体的逻辑
    }
    .show()
复制代码

可以看到,Snackbar 中 make() 方法的第一个参数变成了 View,而 Toast 中 makeText() 方法的第一个参数是 Context,另外 Snackbar 还可以调用 setAction() 方法来设置一个额外的点击事件。除了这些区别之外,Snackbar 和 Toast 的其他用法都是相似的。

那么对于这种结构的 API,我们该如何进行简化呢?其实简化的方式并不固定,接下来我即将演示的写法也只是我个人认为比较不错的一种。

由于 make() 方法接收一个 View 参数,Snackbar 会使用这个 View 自动查找最外层的布局,用于展示 Snackbar。因此,我们就可以给 View 类添加一个扩展函数,并在里面封装显示 Snackbar 的具体逻辑。新建一个 Snackbar.kt 文件,并编写如下代码:

fun View.showSnackbar(text: String, duration: Int = Snackbar.LENGTH_SHORT) {
    Snackbar.make(this, text, duration).show()
}

fun View.showSnackbar(resId: Int, duration: Int = Snackbar.LENGTH_SHORT) {
    Snackbar.make(this, resId, duration).show()
}
复制代码

这段代码应该还是很好理解的,和刚才的 showToast() 函数比较相似。只是我们将扩展函数添加到了 View 类当中,并在参数列表上声明了 Snackbar 要显示的内容以及显示的时长。另外,Snackbar 和 Toast 类似,显示的内容也是支持传入字符串和字符串资源 id 两种类型的,因此这里我们给 showSnackbar() 函数进行了两种参数类型的函数重载。

现在想要使用 Snackbar 显示一段文本提示,只需要这样写就可以了:

view.showSnackbar("This is Snackbar")
复制代码

假如 Snackbar 没有 setAction() 方法,那么我们的简化工作到这里就可以结束了。但是 setAction() 方法作为 Snackbar 最大的特色之一,如果不能支持的话,我们编写的 showSnackbar() 函数也就变得毫无意义了。

这个时候,神通广大的高阶函数又能派上用场了,我们可以让 showSnackbar() 函数再额外接收一个函数类型参数,以此来实现 Snackbar 的完整功能支持。修改 Snackbar.kt 中的代码,如下所示:

fun View.showSnackbar(text: String, actionText: String? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
    val snackbar = Snackbar.make(this, text, duration)
    if (actionText != null && block != null) {
        snackbar.setAction(actionText) {
            block()
        }
    }
    snackbar.show()
}

fun View.showSnackbar(resId: Int, actionResId: Int? = null, duration: Int = Snackbar.LENGTH_SHORT, block: (() -> Unit)? = null) {
    val snackbar = Snackbar.make(this, resId, duration)
    if (actionResId != null && block != null) {
        snackbar.setAction(actionResId) {
            block()
        }
    }
    snackbar.show()
}
复制代码

可以看到,这里我们给两个 showSnackbar() 函数都增加了一个函数类型参数,并且还增加了一个用于传递给 setAction() 方法的字符串或字符串资源 id。这里我们需要将新增的两个参数都设置成可为空的类型,并将默认值都设置成空,然后只有当两个参数都不为空的时候,我们才去调用 Snackbar 的 setAction() 方法来设置额外的点击事件。如果触发了点击事件,只需要调用函数类型参数将事件传递给外部的 Lambda 表达式即可。

这样 showSnackbar() 函数就拥有比较完整的 Snackbar 功能了,比如本小节最开始的那段示例代码,现在就可以使用如下写法进行实现:

view.showSnackbar("This is Snackbar", "Action") {
    // 处理具体的逻辑
}
复制代码

怎么样,和 Snackbar 原生 API 的用法相比,我们编写的 showSnackbar() 函数是不是要明显简单好用得多?

在本章的 Kotlin 课堂中,我带着你一共编写了 3 个工具方法,分别应用了顶层函数、扩展函数以及高阶函数的知识,当然还用到了像 vararg、参数默认值等技巧。Kotlin 给我们提供了太多出色的特性,因此在你学完了这么多特性之后,能否将它们灵活运用就成为了至关重要的事情。本节课里所实现的 3 个工具方法只能算是开胃菜,我非常期待未来你能编写出许多自己的工具方法,将 Kotlin 提供给我们的优秀特性充分发挥出来。

文章分类
Android
文章标签