16、IO与File KTX:文件读写、SharedPreferences的KTX扩展,告别样板代码。
做Android开发这么多年,文件读写和SharedPreferences这两块,我估计每个人都写过无数遍重复代码。打开流、关闭流、try-catch-finally……写到最后自己都烦了。尤其是SharedPreferences,每次都要getString、getInt,然后还要判空,简直是对耐心的考验。
KTX出来之后,我第一时间就把项目里的IO和SP代码重构了一遍。说实话,改完之后代码量至少减了一半,而且可读性提升了不少。今天我们就来聊聊这些扩展库到底帮我们省了哪些事。
核心思想:KTX通过扩展函数和lambda表达式,把资源管理、异常处理、类型转换这些脏活累活都封装起来了。你只需要关心业务逻辑,剩下的交给扩展库。
16.1 文件IO的KTX扩展:读写从未如此简单
先说说文件读写。以前我们写文件操作,大概是这样:
// 传统写法
fun readFile(file: File): String {
var inputStream: InputStream? = null
return try {
inputStream = FileInputStream(file)
val bytes = inputStream.readBytes()
String(bytes, Charsets.UTF_8)
} catch (e: IOException) {
e.printStackTrace()
""
} finally {
inputStream?.close()
}
}
这段代码看着就累。你想想看,每次都要手动管理流的生命周期,稍不留神就忘了close。我在项目中就遇到过因为流没关闭导致文件描述符泄漏的问题,排查了半天才发现是某个工具类里忘了finally块。
KTX提供了use扩展函数,它实现了Closeable接口的自动关闭:
// KTX写法
fun readFile(file: File): String {
return file.inputStream().use { inputStream ->
inputStream.readBytes().toString(Charsets.UTF_8)
}
}
嗯,这里要注意,use函数会在lambda执行完毕后自动调用close(),哪怕中间抛了异常也会正常关闭。这个设计真的很贴心。
除了use,KTX还提供了更直接的扩展:
// 直接读取文本内容
val content = file.readText() // 默认UTF-8
val content = file.readText(charset = Charsets.UTF_16)
// 按行读取
val lines = file.readLines() // 返回List<String>
// 读取字节
val bytes = file.readBytes()
// 写入操作
file.writeText("Hello KTX")
file.appendText("追加内容")
file.writeBytes(byteArrayOf(1, 2, 3))
我个人习惯用readText和writeText,这两个函数覆盖了90%的日常需求。它们内部已经处理了编码和异常,你完全不需要关心底层细节。
小技巧:如果你需要频繁读写同一个文件,可以考虑用bufferedReader()和bufferedWriter(),它们提供了缓冲功能,性能更好。
16.2 SharedPreferences的KTX扩展:告别getString和apply
SharedPreferences的痛点更明显。每次读写都要写一堆模板代码:
// 传统写法
val prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
val name = prefs.getString("name", "") ?: ""
prefs.edit().putString("name", "张三").apply()
这段代码有什么问题?第一,每次都要获取SharedPreferences实例;第二,edit()和apply()是固定套路;第三,取值时还要处理空安全。
KTX的preferences扩展函数解决了这些问题:
// 使用KTX
val prefs = context.preferences("my_prefs")
// 读取
val name: String = prefs.getString("name", "")
val age: Int = prefs.getInt("age", 0)
val isVip: Boolean = prefs.getBoolean("is_vip", false)
// 写入
prefs.edit {
putString("name", "张三")
putInt("age", 25)
putBoolean("is_vip", true)
}
你看,edit函数接受一个lambda,里面直接调用putXxx方法,最后自动执行apply()。我曾经在项目里看到有人用commit()而不是apply(),导致UI线程卡顿。KTX默认用apply(),这个选择很明智。
避坑指南:我曾经在团队里推广KTX时,有人问为什么不用commit()。这里要说明一下:apply()是异步写入,不会阻塞主线程;commit()是同步写入,会返回boolean表示是否成功。绝大多数场景下apply()就够了,除非你需要在写入后立即读取并依赖结果。
16.3 更高级的用法:委托属性与类型安全
如果你觉得上面的写法还不够简洁,KTX还支持委托属性。说白了,就是把SharedPreferences的读写操作封装成属性一样使用:
// 定义委托
class PreferenceManager(context: Context) {
private val prefs = context.preferences("app_prefs")
var userName: String by prefs.string("user_name", "")
var userAge: Int by prefs.int("user_age", 0)
var isLoggedIn: Boolean by prefs.boolean("is_logged_in", false)
}
// 使用
val manager = PreferenceManager(context)
manager.userName = "李四" // 自动写入
val name = manager.userName // 自动读取
这个模式我特别喜欢。它把配置项变成了类的属性,IDE还能自动补全,不容易写错key。我在一个中型项目里用了这个模式,所有配置项都集中管理,维护起来特别方便。
不过要注意,委托属性默认使用apply(),如果你需要同步写入,可以自定义委托:
// 自定义同步写入委托
class SyncStringPreference(
private val prefs: SharedPreferences,
private val key: String,
private val default: String
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return prefs.getString(key, default) ?: default
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
prefs.edit().putString(key, value).commit() // 同步写入
}
}
嗯,说实话,我很少需要同步写入。除非你在写入后立即要在另一个线程读取,否则apply()完全够用。
16.4 知识体系总览
下面这张图总结了本章的核心内容,你可以看到KTX是如何一步步简化IO和SP操作的:
16.5 实际项目中的最佳实践
最后分享几个我在项目中总结的经验:
- 文件读写优先用
use:不管是InputStream还是OutputStream,只要实现了Closeable,就用use包裹。这是最安全的做法。 - SP的key统一管理:建议定义一个object类,把所有key作为常量。这样不容易写错,也方便后续维护。
- 不要滥用委托属性:委托属性虽然方便,但如果配置项太多(比如超过20个),建议还是用传统的
edit{}方式,避免类过于臃肿。 - 注意线程安全:SharedPreferences本身是线程安全的,但如果你在多个线程中同时读写同一个key,还是要注意同步问题。
个人建议:如果你在写新项目,不妨从一开始就用KTX的扩展。老项目重构的话,可以分模块逐步替换,先改那些频繁读写的SP和文件操作,效果最明显。
好了,关于IO和SharedPreferences的KTX扩展就聊到这里。这些扩展函数虽然简单,但确实能让我们少写很多重复代码。你想想看,每次少写几行,一个项目下来能省多少时间?
公众号:蓝海资料掘金营,微信deep3321