前言
在 Kotlin 中,我们可以利用高阶函数和扩展函数,来简化各种 API 繁琐的样板代码。
现在,我们就来简化 SharedPreferences 和 ContentValues 这两个常用 API 的用法。
简化 SharedPreferences 的用法
我们先来回顾一下,之前我们向 SharedPreferences 中存储数据的代码:
kotlin
val preferences = getSharedPreferences("data", Context.MODE_PRIVATE)
val editor = preferences.edit()
editor.putString("name", "Martin")
editor.putInt("age", 23)
editor.putString("gender","male")
editor.apply()
这种写法使用了 Java 的编程思维。在 Kotlin 中,我们可以做得更好。
我们可以定义一个扩展函数,利用高阶函数来封装 edit() 和 apply() 的过程。
创建 SharedPreferencesExtensions.kt 文件,代码如下:
kotlin
fun SharedPreferences.transaction(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}
我们来详细讲解一下:
-
transaction是一个SharedPreferences类的扩展函数 ,所以在函数中,我们拥有SharedPreferences的上下文,可以直接调用SharedPreferences实例的edit()方法。 -
并且这个函数接收一个函数类型的
block参数,所以它也是一个高阶函数 。block参数带有SharedPreferences.Editor接收者,这意味着我们传入的 Lambda 表达式中,拥有SharedPreferences.Editor的上下文,可以直接调用SharedPreferences.Editor实例的put()系列方法来添加数据,例如putString()。
函数内部还完成了 edit() 和 apply() 的调用,使得我们无需手动创建 Editor 对象和手动提交数据。
现在,向 SharedPreferences 中存储数据的代码将会简化为:
kotlin
getSharedPreferences("data", Context.MODE_PRIVATE).transaction {
putString("name", "Martin")
putInt("age", 23)
putString("gender", "male")
}
代码更少了,而且逻辑很清晰。
KTX 扩展库简化方式
其实 KTX 扩展库(androidx.core:core-ktx)已经提供了功能相同的简化方式。我们可以直接通过以下写法来向 SharedPreferences 中存储数据:
kotlin
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}
我们来看看它的源码:
kotlin
@SuppressLint("ApplySharedPref")
public inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) {
editor.commit()
} else {
editor.apply()
}
}
可以发现它将函数定义为了内联函数,提高了性能,并且还可以让我们选择此次提交方式(apply 或 commit)。一般来说,优先使用官方函数。
简化 ContentValues 的用法
同样,之前构建一个 ContentValues 对象的代码可能是下面这样的:
kotlin
val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
当然,聪明的你,可能会使用 apply 函数来稍微简化一下构建过程,变为这样:
kotlin
val valuesWithApply = ContentValues().apply {
put("name", "Game of Thrones")
put("author", "George Martin")
}
虽然这种写法已经很好了,但这也不是最优的。它是命令式的风格,不是声明式的风格,我们现在来实现直接接收多个键值对来创建 ContentValues 对象。
首先,我们新建一个 ContentValuesExtensions.kt 文件,在其中定义一个 cvOf() 方法。代码如下:
kotlin
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
// TODO 实现方法的逻辑
return ContentValues()
}
我们来稍微解释一下:
-
该方法用于构建一个
ContentValues对象。 -
pairs参数的类型是 Pair,Pair 在 Kotlin 中表示键值对的数据结构。因为
ContentValues的键(key)都是字符串类型的,所以我们将Pair的第一个泛型参数定为了String,ContentValues的值(value)可以有多种类型,所以将第二个泛型参数指定成Any?,Any是 Kotlin 中所有类型的基类,相当于 Java 中的Object,这表示可以传入任意类型,并且在Any后加上了?,说明传入的值可为空。 -
我们还在
pairs参数前加上了vararg关键字,这表示该参数是可变参数,我们可以传入任意数量的Pair对象,编译器最终会将它们装进一个数组中。
接下来,我们实现方法的逻辑。
核心思路很简单:先创建一个 ContentValues 对象,然后遍历 pairs 参数,取出每一个 Pair 对象,将其键和值添加到 ContentValues 对象中,最后返回该 ContentValues 对象即可。
但有一个问题:每个 Pair 对象值的类型都是 Any?,我们并不知道它的具体类型是什么,所以我们无法确定该调用哪个 put() 重载方法往 ContentValues 对象中添加数据。
其实 when 语句就可以进行类型匹配,我们可以利用它来判断值的类型。
实现后的代码如下所示:
kotlin
fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
// 创建 ContentValues 对象
val cv = ContentValues()
// 遍历 Pair 对象
for (pair in pairs) {
// 获取键
val key = pair.first
// 获取值
when (val value = pair.second) {
// 根据值的类型,来判断该调用哪个 put 方法
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
else -> throw IllegalArgumentException("Unsupported value type: ${value.javaClass.name} for key '$key'")
}
}
return cv
}
其中关键在于:
-
我们使用了
when(value)语句来判断value的具体类型,从而能够调用具体的put方法。 -
另外,我们用到了 Kotlin 的智能类型转换(Smart Cast)功能。如果进入了某个类型分支内部,编译器会自动 将
value变量的类型转为该类型,所以我们可以直接调用具体的put()方法,无需手动进行类型转换。这个功能在if语句中也能使用。
现在,我们构建一个 ContentValues 对象的代码会被简化为:
kotlin
val values = cvOf(
"name" to "Game of Thrones",
"author" to "George Martin",
"pages" to 720,
"price" to 20.85
)
注意:使用
A to B这样的语法结构可以创建一个Pair(A,B)对象。
最后,我们可以使用 apply 函数来对 cvOf 函数的实现做进一步简化,简化后:
kotlin
fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
// 遍历 Pair 列表
for (pair in pairs) {
// 获取键
val key = pair.first
// 获取值
when (val value = pair.second) {
// 根据值的类型,来判断该使用哪个方法
is Int -> put(key, value)
is Long ->put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
else -> throw IllegalArgumentException("Unsupported value type: ${value.javaClass.name} for key '$key'")
}
}
}
因为 apply 函数的内部拥有 ContentValues 的上下文,所以可以直接调用 put() 方法,无需通过 cv. 来调用。
KTX 扩展库简化方式
和 SharedPreferences 一样,KTX 库中也提供了一个同样功能的 contentValuesOf() 方法,用法如下所示:
kotlin
val values = contentValuesOf(
"name" to "Game of Thrones",
"author" to "George Martin",
"pages" to 720,
"price" to 20.85
)