前言
在 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
)