SharedPreferences注解:自动生成SP读写代码、类型安全访问
SharedPreferences,简称SP,是Android开发中最古老的本地存储方案之一。说实话,我入行那会儿它还是官方推荐的轻量级存储方式。但用过的朋友都知道,手写SP代码有多痛苦——每次都要写getString、putString、apply,还得自己维护key常量。更坑的是,类型写错了编译器也不报错,运行时直接崩。
今天我们就用注解处理器,把SP的读写代码自动生成出来。顺便实现类型安全访问——说白了,就是让编译器帮我们检查类型,写错了直接编译失败。
痛点分析:手写SP到底有多烦
先看一段典型的SP代码:
// 保存用户信息
val sp = context.getSharedPreferences("user_config", Context.MODE_PRIVATE)
sp.edit().putString("user_name", "张三").apply()
sp.edit().putInt("user_age", 25).apply()
sp.edit().putBoolean("is_vip", true).apply()
// 读取用户信息
val name = sp.getString("user_name", "")
val age = sp.getInt("user_age", 0)
val isVip = sp.getBoolean("is_vip", false)
这段代码有什么问题?我列几个:
- key散落各处:
"user_name"这种字符串常量,写错一个字母就读取失败 - 类型不安全:
getString("user_age", "")编译器不会报错,但运行时返回null - 样板代码多:每个字段都要重复
getXxx/putXxx模式 - 没有默认值管理:每个读取操作都要手动传默认值
我曾经在一个老项目里接手过这种代码,光SP的key就维护了200多个,散落在十几个类里。改一个key名,得全局搜索替换,还经常漏掉。嗯,那次重构让我下定决心——必须用注解处理器干掉手写SP。
设计思路:注解 + 处理器 + 代码生成
我们的目标很简单:定义一个注解,标注在数据类上,自动生成对应的SP读写代码。
核心流程如下:
@SharedPref(name = "user_config")
data class UserConfig(
val userName: String = "",
val userAge: Int = 0,
val isVip: Boolean = false
)
处理器会生成一个UserConfigSP类,提供类型安全的读写方法:
// 自动生成的代码
class UserConfigSP(context: Context) {
private val sp = context.getSharedPreferences("user_config", Context.MODE_PRIVATE)
var userName: String
get() = sp.getString("userName", "") ?: ""
set(value) = sp.edit().putString("userName", value).apply()
var userAge: Int
get() = sp.getInt("userAge", 0)
set(value) = sp.edit().putInt("userAge", value).apply()
var isVip: Boolean
get() = sp.getBoolean("isVip", false)
set(value) = sp.edit().putBoolean("isVip", value).apply()
}
你看,使用方只需要操作属性,完全不用关心SP的细节。类型不对?编译器直接报红。
核心思想:用Kotlin属性代理 + 注解处理器,把SP的读写操作封装成属性访问。开发者只需要定义数据类,剩下的交给代码生成。
注解定义:@SharedPref
我们先定义注解:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class SharedPref(
val name: String = "", // SP文件名,默认用类名
val mode: Int = Context.MODE_PRIVATE
)
这里我用了SOURCE级别,因为生成的代码不需要在运行时反射注解。处理器在编译时读完注解信息,生成代码后,注解本身就可以丢弃了。
小技巧:name参数留空时,处理器自动使用类名作为SP文件名。我个人习惯在注解里显式指定,方便后期迁移。
处理器实现:解析注解 + 生成代码
处理器需要做三件事:
- 找到所有标注了
@SharedPref的类 - 解析类的属性信息(名称、类型、默认值)
- 生成对应的SP操作类
核心代码片段:
@AutoService(Processor::class)
@SupportedOptions(SharedPrefProcessor.KAPT_KOTLIN_GENERATED_OPTION_NAME)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class SharedPrefProcessor : AbstractProcessor() {
override fun getSupportedAnnotationTypes(): Set<String> {
return setOf(SharedPref::class.java.canonicalName)
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
roundEnv.getElementsAnnotatedWith(SharedPref::class.java).forEach { element ->
val classElement = element as TypeElement
val packageName = processingEnv.elementUtils.getPackageOf(classElement).qualifiedName.toString()
val className = classElement.simpleName.toString()
val annotation = classElement.getAnnotation(SharedPref::class.java)
val spName = annotation.name.ifEmpty { className }
// 收集属性信息
val properties = mutableListOf<PropertyInfo>()
classElement.enclosedElements
.filterIsInstance<VariableElement>()
.filter { it.kind == ElementKind.FIELD }
.forEach { field ->
val fieldName = field.simpleName.toString()
val fieldType = field.asType().toString()
val defaultValue = field.constantValue ?: getDefaultValue(fieldType)
properties.add(PropertyInfo(fieldName, fieldType, defaultValue))
}
// 生成代码
generateSpClass(packageName, className, spName, properties)
}
return true
}
private fun generateSpClass(
packageName: String,
className: String,
spName: String,
properties: List<PropertyInfo>
) {
val spClassName = "${className}SP"
val builder = TypeSpec.classBuilder(spClassName)
.addModifiers(KModifier.PUBLIC)
.addField(
FieldSpec.builder(
ClassName("android.content", "SharedPreferences"),
"sp",
KModifier.PRIVATE
).build()
)
.addFunction(
FunSpec.constructorBuilder()
.addParameter("context", ClassName("android.content", "Context"))
.addStatement("sp = context.getSharedPreferences(\$S, Context.MODE_PRIVATE)", spName)
.build()
)
// 为每个属性生成getter/setter
properties.forEach { prop ->
val getterStmt = when (prop.type) {
"java.lang.String", "kotlin.String" ->
"sp.getString(\$S, \$L) ?: \$L"
"int", "kotlin.Int" ->
"sp.getInt(\$S, \$L)"
"boolean", "kotlin.Boolean" ->
"sp.getBoolean(\$S, \$L)"
"float", "kotlin.Float" ->
"sp.getFloat(\$S, \$L)"
"long", "kotlin.Long" ->
"sp.getLong(\$S, \$L)"
else -> throw IllegalArgumentException("Unsupported type: ${prop.type}")
}
val setterStmt = when (prop.type) {
"java.lang.String", "kotlin.String" ->
"sp.edit().putString(\$S, value).apply()"
"int", "kotlin.Int" ->
"sp.edit().putInt(\$S, value).apply()"
"boolean", "kotlin.Boolean" ->
"sp.edit().putBoolean(\$S, value).apply()"
"float", "kotlin.Float" ->
"sp.edit().putFloat(\$S, value).apply()"
"long", "kotlin.Long" ->
"sp.edit().putLong(\$S, value).apply()"
else -> throw IllegalArgumentException("Unsupported type: ${prop.type}")
}
builder.addProperty(
PropertySpec.builder(prop.name, ClassName.bestGuess(prop.type))
.addModifiers(KModifier.VAR)
.getter(
FunSpec.getterBuilder()
.addStatement(getterStmt, prop.name, prop.defaultValue, prop.defaultValue)
.build()
)
.setter(
FunSpec.setterBuilder()
.addParameter("value", ClassName.bestGuess(prop.type))
.addStatement(setterStmt, prop.name)
.build()
)
.build()
)
}
val file = FileSpec.builder(packageName, spClassName)
.addType(builder.build())
.build()
file.writeTo(processingEnv.filer)
}
private fun getDefaultValue(type: String): Any {
return when (type) {
"java.lang.String", "kotlin.String" -> "\"\""
"int", "kotlin.Int" -> "0"
"boolean", "kotlin.Boolean" -> "false"
"float", "kotlin.Float" -> "0.0f"
"long", "kotlin.Long" -> "0L"
else -> "null"
}
}
data class PropertyInfo(
val name: String,
val type: String,
val defaultValue: Any
)
}
注意:上面的代码为了演示清晰,简化了类型映射逻辑。实际项目中,你需要处理Set<String>、可空类型、自定义类型序列化等场景。我曾经因为没处理Set<String>,导致线上用户配置丢失,排查了一整天。
使用示例:一行注解,全自动
定义数据类:
@SharedPref(name = "app_settings")
data class AppSettings(
val isDarkMode: Boolean = false,
val fontSize: Int = 16,
val userName: String = "",
val lastLoginTime: Long = 0L
)
使用自动生成的SP类:
class MainActivity : AppCompatActivity() {
private lateinit var settings: AppSettingsSP
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settings = AppSettingsSP(this)
// 读取 - 类型安全,编译器检查
val isDark = settings.isDarkMode
val fontSize = settings.fontSize
// 写入 - 直接赋值
settings.isDarkMode = true
settings.fontSize = 18
}
}
你想想看,以前要写多少代码?现在只需要一个注解,所有SP操作都变成了属性访问。而且类型不对?编译直接报错,根本不会留到运行时。
进阶:支持自定义类型序列化
实际项目中,我们经常需要存储复杂对象。比如用户信息:
data class UserInfo(
val id: Long,
val name: String,
val avatar: String
)
@SharedPref(name = "user_cache")
data class UserCache(
val currentUser: UserInfo? = null, // 需要序列化
val loginCount: Int = 0
)
对于非基本类型,我们需要在处理器中判断:如果是自定义类型,自动生成Gson序列化/反序列化代码。
处理器逻辑扩展:
// 在generateSpClass中增加类型判断
val isBasicType = prop.type in basicTypes
if (isBasicType) {
// 生成基本类型的getter/setter
} else {
// 生成Gson序列化的getter/setter
// getter: Gson().fromJson(sp.getString("currentUser", ""), UserInfo::class.java)
// setter: sp.edit().putString("currentUser", Gson().toJson(value)).apply()
}
避坑指南:我曾经在序列化这里踩过坑——如果数据类改了字段名,旧数据反序列化会失败。建议在fromJson时加上@SerializedName注解,或者用Moshi这种更安全的库。
性能与最佳实践
生成代码的性能怎么样?我直接说结论:和手写SP完全一样。因为生成的代码就是标准的SP API调用,没有任何反射开销。
几点建议:
- 不要存储大对象:SP本质是XML文件,太大影响读写性能。超过10KB的数据建议用Room或文件存储
- 合理使用apply/commit:生成代码默认用
apply(异步),如果需要同步写入,可以加一个@Commit注解参数 - 多进程场景慎用:SP不是线程安全的,多进程读写会有问题。如果必须用,考虑
ContentProvider或MMKV
本章小结
SharedPreferences注解处理器,说白了就是把重复劳动交给机器。你只需要定义数据结构和默认值,剩下的读写代码、类型检查、key管理,全部自动完成。
我个人觉得,这种模式特别适合配置类数据的存储——比如用户设置、缓存标记、功能开关。用注解处理器生成SP代码后,项目里再也没出现过"key拼写错误"这种低级bug。
嗯,如果你还在手写SP,不妨试试这个方案。代码量减少80%,类型安全提升100%。
公众号:蓝海资料掘金营,微信deep3321