针对Android平台键值对的持久化存储,虽然Jetpack出了新的DataStore
,但实际项目中SharedPreferences
还是有大量使用,本文结合以前的使用经验给出一种极简且优雅且安全的实践。(示例项目见 https://gitee.com/spectre1225/neo-preference-demo)
SharedPreferences
的基本读写代码如下:
preferences.edit().putInt("intKey", 1).apply();//写
preferences.getInt("intKey", 0);//读,0为默认值
代码中直接这么用的话,键会很不好管理,不清楚一个键值对到底有多少地方使用,当键发生改变需要修改的时候,也容易遗漏。于是就有了以下改进:
public interface XXXConfig{
String KEY_PROPERTY_AA = "key_aa";
String KEY_PROPERTY_BB = "key_bb";
String KEY_PROPERTY_CC = "key_cc";
//more key.......
}
//使用的地方
preferences.edit().putInt(XXXConfig.KEY_PROPERTY_AA, 1).apply();//写
preferences.getInt(XXXConfig.KEY_PROPERTY_AA, 0);//读,0为默认值
但这样写仍然有问题,就是缺少值类型的约束:一个key对应的value,可能有很多种类型。这种情况下,需要额外的注释或文档来记录每一个key对应的value的类型信息。于是,有人想到可以像JavaBean一样,采用getter和setter方法的形式:
public class XXXConfig {
private SharedPreferences preferences;
private String KEY_PROPERTY_AA = "key_aa";
private String KEY_PROPERTY_BB = "key_bb";
//中间省略初始化......
public int getPropertyAA() {
return preferences.getInt(KEY_PROPERTY_AA, 0);
}
public void setPropertyAA(int value) {
preferences.edit().putInt(KEY_PROPERTY_AA, value);
}
public int getPropertyBB() {
return preferences.getInt(KEY_PROPERTY_BB, 0);
}
public void setPropertyBB(int value) {
preferences.edit().putInt(KEY_PROPERTY_BB, value);
}
}
这种写法改进了类型安全,但每次新增就需要写一个属性和两个方法,过程比较繁琐。理想情况,我还是希望像写文档一样只需要写下面这样的信息:
属性1:类型 int
属性2:类型 String
然后使用的地方可以直接取值。因此,就有了下面介绍的新的封装方法:暂且称为NeoPreference。
首先,我们需要需要创建一个inferface
来继承Config
接口,这个新的接口对应一个SharedPreferences
,默认接口名即为SharedPreferences
的名称,例如:
public interface DemoConfig extends Config {
}
这里的DemoConfig
即为SharedPreferences
的名称。有时候我们想要自己另外指定名称,则可以使用Config.Name
注解:
@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
}
这个时候SharedPreferences
名称就是my_demo_config
。
然后我们就可以通过ConfigManager
来获取DemoConfig
的实例:
DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
到目前为止还没有什么新鲜的,接下来我们往里面添加新的配置项/属性:
@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
Property<Integer> versionCode();
}
在上述基础上,只需要添加一行代码,就添加了新的键值对:key的值为versionCode
,value的类型为Integer
。然后我们的读写代码可以这么写:
DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
Integer versionCode = config.versionCode().get();//读
config.versionCode().set(versionCode + 1);//写
如果我们想要单独定key的名字,我们可以使用对应属性的注解:
@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code")
Property<Integer> versionCode();
}
我们还可以指定值的范围和默认值:
@Config.Name("my_demo_config")
public interface DemoConfig extends Config {
@IntItem(key = "my_version_code", start = 1, to = 10000, defaultValue = 1)
Property<Integer> versionCode();
}
这样,在值不符合规范的时候会抛出异常:
DemoConfig config = ConfigManager.getInstance().getConfig(DemoConfig.class);
config.versionCode().set(-1);//throw exeception
这个工具的API除了ConfigManager
类以外主要分两部分:Property
类以及类型对应的注解。
ConfigManager
是单例实现,维护一个SharedPreferences
和Config
的注册表,提供getConfig
和addListener
两个方法。
以下是getConfig
方法签名:
public <P extends Config> P getConfig(Class<P> pClass);
public <P extends Config> P getConfig(Class<P> pClass, int mode);
参数pClass
是继承Config
类的接口class,可选参数mode
对应SharedPreferences
的mode。
addListener
的方法监听指定preferenceName
中内容的变化,签名如下:
public void addListener(String preferenceName, WeakReference<Listener> listenerRef);
public void addListener(LifecycleOwner lifecycleOwner, String preferenceName, Listener listener);
第一个方法接受一个Listener
的弱引用,需要调用者自己持有监听器的引用,自己管理生命周期,否则可能被回收。第二个方法不采用弱引用参数,而是额外添加LifecycleOwner
,这个监听器的声明周期采用LifecycleOwner
对应的生命周期。
Property
类接口说明Property
接口包括:
public final String getKey();//获取属性对应的key
public T get(T defValue); //获取属性值,defValue为默认值
public T get(); //获取属性值,采用缺省默认值
public void set(T value); //设置属性值
public Optional<T> opt(); //以Optional的形式返回属性值
public final void addListener(WeakReference<Listener<T>> listenerRef) //类似ConfigManager,不过只监听该属性的值变化
public final void addListener(LifecycleOwner owner, Listener<T> listener)//类似ConfigManager,不过只监听该属性的值变化
泛型参数支持Long
、Integer
、Float
、Boolean
、String
、Set
等SharedPreferences
支持的几种类型,以及额外的Serializable
。
这些注解对应SharedPreferences
支持的几种类型(其中description
字段暂时不用)。
@interface StringItem {
String key() default "";
boolean supportEmpty() default true;
String[] valueOf() default {};
String defaultValue() default "";
String description() default "";
}
@interface BooleanItem {
String key() default "";
boolean defaultValue() default false;
String description() default "";
}
@interface IntItem {
String key() default "";
int defaultValue() default 0;
int start() default Integer.MIN_VALUE;
int to() default Integer.MAX_VALUE;
int[] valueOf() default {};
String description() default "";
}
@interface LongItem {
String key() default "";
long defaultValue() default 0;
long start() default Long.MIN_VALUE;
long to() default Long.MAX_VALUE;
long[] valueOf() default {};
String description() default "";
}
@interface FloatItem {
String key() default "";
float defaultValue() default 0;
float start() default -Float.MIN_VALUE;
float to() default Float.MAX_VALUE;
float[] valueOf() default {};
String description() default "";
}
@interface StringSetItem {
String key() default "";
String[] valueOf() default {};
String description() default "";
}
@interface SerializableItem {
String key() default "";
Class<?> type() default Object.class;
String description() default "";
}
见:https://gitee.com/spectre1225/neo-preference-demo