高阶函数的应用:简化 SharedPreferences 与 ContentValues 操作

前言

在 Kotlin 中,我们可以利用高阶函数和扩展函数,来简化各种 API 繁琐的样板代码。

现在,我们就来简化 SharedPreferencesContentValues 这两个常用 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()
    }
}

可以发现它将函数定义为了内联函数,提高了性能,并且还可以让我们选择此次提交方式(applycommit)。一般来说,优先使用官方函数。

简化 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 的第一个泛型参数定为了 StringContentValues 的值(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
)
相关推荐
移动开发者1号16 小时前
Retrofit动态URL与Path参数处理
android·kotlin
移动开发者1号16 小时前
Android 中 OkHttp 的自定义 Interceptor 实现统一请求头添加
android·kotlin
小金子同志21 小时前
发现 Kotlin MultiPlatform 的一点小变化
kotlin
androidwork1 天前
嵌套滚动交互处理总结
android·java·kotlin
橙子199110161 天前
Kotlin 中的 Object
android·开发语言·kotlin
岸芷漫步2 天前
Kotlin中协程的关键函数分析
kotlin
纳于大麓2 天前
Kotlin基础语法五
android·开发语言·kotlin
移动开发者1号2 天前
嵌套滚动交互处理总结
android·kotlin
移动开发者1号2 天前
Android工程中FTP加密传输与非加密传输的深度解析
android·java·kotlin