共享参数SharedPreferences是Android最简单的数据存储方式,常用于存取“Key-Value”键值对数据。在使用共享参数之前,要先调用getSharedPreferences方法声明文件名与操作模式,示例代码如下:
SharedPreferences sps = getSharedPreferences("share", Context.MODE_PRIVATE);
该方法的第一个参数是文件名,例子中的share表示当前的共享参数文件是share.xml;第二个参数是操作模式,一般填MODE_PRIVATE表示私有模式。
共享参数若要存储数据则需借助于Editor类,示例的Java代码如下:
SharedPreferences.Editor editor = sps.edit(); editor.putString("name", "阿四"); editor.putInt("age", 25); editor.putBoolean("married", false); editor.putFloat("weight", 50f); editor.commit();
使用共享参数读取数据则相对简单,直接调用其对象的get方法即可获取数据,注意get方法的第二个参数表示默认值,示例的Java代码如下:
String name = sps.getString("name", ""); int age = sps.getInt("age", 0); boolean married = sps.getBoolean("married", false); float weight = sps.getFloat("weight", 0);
从上述数据读写的代码可以看出,共享参数的存取操作有些繁琐,因此实际开发常将共享参数相关操作提取到一个工具类,在新的工具类中封装SharedPreferences的常用操作,下面便是一个共享参数工具类的Java代码例子:
public class SharedUtil { private static SharedUtil mUtil; private static SharedPreferences mShared; public static SharedUtil getIntance(Context ctx) { if (mUtil == null) { mUtil = new SharedUtil(); } mShared = ctx.getSharedPreferences("share", Context.MODE_PRIVATE); return mUtil; } public void writeShared(String key, String value) { SharedPreferences.Editor editor = mShared.edit(); editor.putString(key, value); editor.commit(); } public String readShared(String key, String defaultValue) { return mShared.getString(key, defaultValue); } }
有了共享参数工具类,外部读写SharedPreferences就比较方便了,比如下面的Java代码,无论是往共享参数写数据还是从共享参数读数据,均只要一行代码:
//调用工具类写入共享参数 SharedUtil.getIntance(this).writeShared("name", "阿四"); //调用工具类读取共享参数 String name = SharedUtil.getIntance(this).readShared("name", "");
不过这个工具类并不完善,因为它只支持字符串String类型的数据读写,并不支持整型、浮点数、布尔型等其它类型的数据读写。另外,如果外部需要先读取某个字段的数值,等处理完了再写回共享参数,则使用工具类也要两行代码(一行读数据、一行写数据),依旧有欠简洁。找毛病其实都是容易的,如果仍然使用Java编码,能完善的就完善,不能完善的也不必苛求了。
之所以挑Java实现方式的毛病,倒不是因为看它不顺眼整天吹毛求疵,而是因为Kotlin有更好的解决办法。为了趁热打铁方便比较两种方式的优劣,下面开门见山直接给出Kotlin封装共享参数的实现代码例子:
class Preference<T>(val context: Context, val name: String, val default: T) : ReadWriteProperty<Any?, T> { val prefs: SharedPreferences by lazy { context.getSharedPreferences("default", Context.MODE_PRIVATE) } override fun getValue(thisRef: Any?, property: KProperty<*>): T { return findPreference(name, default) } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { putPreference(name, value) } private fun <T> findPreference(name: String, default: T): T = with(prefs) { val res: Any = when (default) { is Long -> getLong(name, default) is String -> getString(name, default) is Int -> getInt(name, default) is Boolean -> getBoolean(name, default) is Float -> getFloat(name, default) else -> throw IllegalArgumentException("This type can be saved into Preferences") } return res as T } private fun <T> putPreference(name: String, value: T) = with(prefs.edit()) { //putInt、putString等方法返回Editor对象 when (value) { is Long -> putLong(name, value) is String -> putString(name, value) is Int -> putInt(name, value) is Boolean -> putBoolean(name, value) is Float -> putFloat(name, value) else -> throw IllegalArgumentException("This type can be saved into Preferences") }.apply() //commit方法和apply方法都表示提交修改 } }
外部在使用该工具类之时,可在Activity代码中声明来自于Preference的委托属性,委托属性一旦声明,则它的初始值便是从共享参数读取的数值;后续代码若给委托属性赋值,则立即触发写入动作,把该属性的最新值保存到共享参数中。于是外部操作共享参数的某个字段,真正要书写的仅仅是下面的一行委托属性声明代码:
//声明字符串类型的委托属性 private var name: String by Preference(this, "name", "") //声明整型数类型的委托属性 private var age: Int by Preference(this, "age", 0)
既然Kotlin对共享参数的处理也如此传神,那么大家肯定很好奇,这个高大上的Preference究竟运用了哪些黑科技呢?且待笔者下面细细道来:
一、模板类
因为共享参数允许保存的数据类型包括整型、浮点数、字符串等等,所以Preference定义成模板类,具体的参数类型在调用之时再指定。
除却代表模板类泛型的T,该类中还有两个与之相似的元素,分别是Any和*,各自表示不同的涵义。下面简单说明一下T、Any和*三者之间的区别:
1、T是抽象的泛型,在模板类中用来占位子,外部调用模板类时才能确定T的具体类型;
2、Any是Kotlin的基本类型,所有Kotlin类都从Any派生而来,故而它相当于Java里面的Object;
3、*星号表示一个不确定的类型,同样也是在外部调用时才能确定,这点跟T比较像,但T出现在模板类的定义中,而*与模板类无关,它出现在单个函数定义的参数列表中,因此星号相当于Java里面的问号?;
二、委托属性/属性代理
注意到外部利用Preference声明参数字段时,后面跟着表达式“by Preference(...)”,这个by表示代理动作,早在第五章的“5.3.4 接口代理”就介绍了如何让类通过关键字by实现指定接口的代理,当时举例说明给不同的鸟类赋予不同的动作。第五章的例子是接口代理或称类代理,而这里则为属性代理,所谓属性代理,是说该属性的类型不变,但是属性的读写行为被后面的类接管了。
为什么需要接管属性的读写行为呢?举个例子,市民每个月都要交电费,自己每月跑去电力营业厅交钱显然够呛,于是后来支持在电力网站上自助缴费,然而上网缴费仍显麻烦,因为需要用户主动上网付费,要是用户忘记就不好办了。所以很多银行都推出了“委托代扣”的业务,只要用户跟银行签约并指定委托扣费的电力账户,那么在每个月指定时间,银行会自动从用户银行卡中扣费并缴纳给指定的电力账户,如此省却了用户的人工操作。
现实生活中的委托扣费场景,对应到共享参数这里,开发者的人工操作指的是手工编码从SharedPreferences类读取数据和保存数据,而自动操作指的是约定代理的属性自动通过模板类Preference<T>完成数据的读取和保存,也就是说,Preference<T>接管了这些属性的读写行为,接管后的操作则是模板类的getValue和setValue方法。属性被接管的行为叫做属性代理,而被代理的属性称作委托属性。
三、关键字lazy
模板类Preference<T>声明了一个共享参数的prefs对象,其中用到了关键字lazy,lazy的意思是懒惰,表示只在该属性第一次使用时执行初始化。联想到Kotlin还有类似的关键字名叫lateinit,意思是延迟初始化,加上lazy可以归纳出Kotlin变量的三种初始化操作,具体说明如下:
1、声明时赋值:这是最常见的变量初始化,在声明某个变量时,立即在后面通过等号“=”给它赋予具体的值。
2、lateinit延迟初始化:变量声明时没有马上赋值,但该变量仍是个非空变量,何时初始化由开发者编码决定。
3、lazy首次使用时初始化:声明变量时指定初始化动作,但该动作要等到变量第一次使用时才进行初始化。
此处的prefs对象使用lazy规定了属性值在首次使用时初始化,且初始化动作通过by后面的表达式来指定,即“{ context.getSharedPreferences("default", Context.MODE_PRIVATE) }”。连同大括号在内的这个表达式,其实是个匿名实例,它内部定义了prefs对象的初始化语句,并返回SharedPreferences类型的变量值。
四、with函数
with函数的书写格式形如“with(函数头语句) { 函数体语句 }”,看这架势,with方法的函数语句分为两部分,详述如下:
1、函数头语句:头部语句位于紧跟with的圆括号内部。它先于函数体语句执行,并且头部语句返回一个对象,函数体语句在该对象的命名空间中运行;即体语句可以直接调用该对象的方法,而无需显式指定该对象的实例名称。
2、函数体语句:体语句位于常规的大括号内部。它要等头部语句执行完毕才会执行,同时体语句在头部语句返回对象的命名空间中运行;即体语句允许直接调用头部对象的方法,而无需显式指定该对象的实例名称。
综上所述,在模板类Preference<T>的编码过程中,联合运用了Kotlin的多项黑科技,方才实现了优于Java的共享参数操作方式。