1、 来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 largest = max(max(a,b),c)
有没有觉得代码开始变得复杂了呢,那如果是4个数、5个数呢?没错,这个时候你就应该意识到,我们是可以对max() 函数的用法进行简化的。
回顾一下,我们之前中学过vararg 关键字(可变参数列表),它允许方法接收任意多个同等类型的参数,正好满足我们这里的需求。那么我们就可以新建一个Max.kt 文件,并在其中定义一个max() 函数,如下所示:
fun max(vararg nums:Int):Int{
var maxNum = Int.MIN_VALUE
for (num in nums) {
maxNum = max(maxNum,num)
}
return maxNum
}
可以看到,这里max() 函数的参数声明中使用了vararg 关键字,也就是说现在它可以接收任意多个整型参数。接着我们使用了一个maxNum 变量来记录所有数的最大值,并在一开始将它赋值变成了整型范围的最小值。然后使用for-in 循环遍历nums 参数列表,如果发现当前遍历的数字比 maxNum 大,就将maxNum 的值更新成这个数,最终将maxNum 返回即可。
仅仅经过这样的一层封装之后,我们在使用max() 函数就会有翻天覆地的变化,比如刚才同样的功能,现在就可以使用如下的写法来实现:
val a = 10
val b = 15
val c = 5
val largest = max(a, b, c)
这样我们就彻底摆脱了嵌套函数调用的写法,现在不管是求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("Params can not be empty.")
var maxNum = nums[0]
for (num in nums) {
if (num > maxNum) {
maxNum = num
}
}
return maxNum
}
可以看到,这里将泛型T 的上届指定成了Comparable ,那么参数T 就必然是Comparable 的子类型了。接下来,我们判断nums 参数列表是否为空,如果为空的话就主动抛出一个异常,提醒调用者max() 函数必须传入参数。紧接着将maxNum 的值赋值成nums 参数列表中第一个参数的值,然后同样是遍历参数列表,如果发现了更大的值就对maxNum 进行更新。
经过这样的修改之后,我们就可以更加灵活地使用max() 函数了,比如说求3个浮点型数字的最大值,同样也变得轻而易举:
val a = 3.5
val b = 3.8
val c = 4.1
val largest = max(a,b,c)
而且现在不管是双精度浮点型、单精度浮点型,还是短整型、整型、长整型,只要是实现Comparable 接口的子类型,max() 函数全部支持获取它们最大的值,是一种一劳永逸的做法。
2、 简化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() 函数时都要额外多传入一个参数,无疑增加了使用复杂度。
可以通过给函数设定参数默认值的功能,借助这个功能,我们就可以在不增加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 工具一定会给你的开发效率带来巨大的提升。
3、简化Snackbar的用法
先来回顾一下Snackbar 的常规用法吧,如下所示:
Snackbar.make(view,"This is Snackbar",Snackbar.LENGTH_SHORT)
.setAction("action"){
// 处理具体的逻辑
}
.show()
可以看到,Snackbar 中make() 方法的第一个参数变成了View,而Toast 中的makeText() 方法的第一个参数是Context,另外Snackbar 还可以调用setAction() 方法来设置一个额外的点击事件。除了这些区别之外,Snackbar 和 Toast 的其他用法都是相似的。
由于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"){
// 处理具体的逻辑
}