在Android开发中,SharedPreferences大概是我们常用的最简单的一种键值对(Key-Value)数据存储方式了,任何一个Android开发者都几乎不可能完全不用SharedPreferences。而它的使用确实很简单,不需要去自己操作读写文件,也不需要去写SQLite数据库,只是简单的put/get操作而已。但是,你的使用方法真的科学合理吗?
原始而古老的SharedPreferences使用方法
不经过任何封装的话,原始的SharedPreferences使用起来大概是这样子的:
SharedPreferences sp = getSharedPreferences("SharedPreferencesFileName", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("test_key", "test_value"); editor.commit(); sp.getString("test_key", "default_value");
单纯的看这么一小段代码,似乎不觉得有什么问题,而且用起来确实能得到想要的结果。但如果一个项目里,到处是这样的代码,甚至一个Activity里都有许多这样的地方,那就非常难看了,我说的难看,相信大家都理解是什么意思。所以,我们有必要进行一下封装,写一个SharedPreferences工具类,这样读写数据就不用这么“大张旗鼓”了。
如何设计一个好用的SharedPreferences工具类?
一个好用的SharedPreferences工具类,一般要满足这么几个条件才算是合格的:
- 不需要每次使用都去获取SharedPreferences的实例。getSharedPreferences这个方法,调用一次就可以了。
- 最好不需要获取SharedPreferences.Editor实例,也不需要调用什么commit、apply等方法,直接通过put/get来进行读写。
- 最好能实现一些数据异常处理,如传入的key是null,或者传入的value不符合实际需求等情况下,能有一定的处理。
- 能够保存对象数据,如List和Map这些常用的数据结构,尤其是Json形式的数据。
根据这几个条件,我们已经可以初步写出一个SharedPreferencesUtils工具类了:
public class SpUtils { // 整个工具类没有考虑过懒加载,主要因为这个工具类在App启动后基本上会马上使用,所以懒加载的实际意义不大 private final static String SHARED_PREFERENCES_NAME = "TestSharedPreferencesFile"; // 这里的App.getInstance()就是获取Application的实例 private static SharedPreferences mSharedPreferences = App.getInstance().getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); private static Gson mGson = new Gson(); public static void applyValue(String key, Object value) { if (TextUtils.isEmpty(key)) return; SharedPreferences.Editor mEditor = mSharedPreferences.edit(); if (value instanceof Integer) { mEditor.putInt(key, (Integer) value).apply(); } else if (value instanceof Long) { mEditor.putLong(key, (Long) value).apply(); } else if (value instanceof Float) { mEditor.putFloat(key, (Float) value).apply(); } else if (value instanceof Boolean) { mEditor.putBoolean(key, (Boolean) value).apply(); } else if (value instanceof String) { mEditor.putString(key, (String) value).apply(); } else { try { mEditor.putString(key, mGson.toJson(value)).apply(); } catch (Exception e) { e.printStackTrace(); } } } public static void commitValue(String key, Object value) { if (TextUtils.isEmpty(key)) return; SharedPreferences.Editor mEditor = mSharedPreferences.edit(); if (value instanceof Integer) { mEditor.putInt(key, (Integer) value).commit(); } else if (value instanceof Long) { mEditor.putLong(key, (Long) value).commit(); } else if (value instanceof Float) { mEditor.putFloat(key, (Float) value).commit(); } else if (value instanceof Boolean) { mEditor.putBoolean(key, (Boolean) value).commit(); } else if (value instanceof String) { mEditor.putString(key, (String) value).commit(); } else { try { mEditor.putString(key, mGson.toJson(value)).commit(); } catch (Exception e) { e.printStackTrace(); } } } public static Object getValue(String key, Object defValue) { if (TextUtils.isEmpty(key)) return null; if (defValue instanceof Integer) { return mSharedPreferences.getInt(key, (Integer) defValue); } else if (defValue instanceof Long) { return mSharedPreferences.getLong(key, (Long) defValue); } else if (defValue instanceof Float) { return mSharedPreferences.getFloat(key, (Float) defValue); } else if (defValue instanceof Boolean) { return mSharedPreferences.getBoolean(key, (Boolean) defValue); } else if (defValue instanceof String) { return mSharedPreferences.getString(key, (String) defValue); } else { try { return mGson.fromJson(mSharedPreferences.getString(key, ""), defValue.getClass()); } catch (Exception e) { e.printStackTrace(); return defValue; } } } }
这里我通过Gson来实现对象与字符串的互转,从而实现了对象的数据保存。也因此,没有额外写putStringSet与getStringSet方法,因为直接将Set<String>作为对象来处理就足够了。
SharedPreferences的Key值该如何处理?
刚才写的工具类,使用起来基本上没有问题了。但实际上还存在一个非常现实的问题:各种Key值该怎么处理(或者说如何保存)?
有的人可能不太理解这个问题的含义,说白了,我们通过SharedPreferences保存的数据是持久化的,无论是保存还是读取,都依赖于那个String型的Key,而这个Key该如何保存,就很值得我们思考了。通常这些Key值可以认定为常量(偶尔个别的Key除了原来的字符串,还会带点别的东西如版本号等)。
很多开发习惯很差的同学,Key都是随遇而安的,如直接使用这样的代码:
// 写入的时候就很随意 editor.putString("nickname", "笑哈哈").commit; // 在另外一个地方同样随意的读取 mSharedPreferences.getString("nickname", "默认昵称");
这样做的结果就是,用不了多久他就可能会忘了很多东西,如果在别的页面还要读写该Key的时候,要么全局搜索该Key,要么就出错:是nickname?还是Nickname?还是NickName?这样做的人,绝对不是一个合格的开发者。
所以有经验的同学,都知道最好把SharedPreferences所使用的所有Key,都保存在一起,有新的Key就添加一下,如此显得十分干净简练,不容易出错。是的,这样做确实是对的,也是应该的,但实际上还存在一个问题:你能记得每个Key对应的value类型吗?尤其是,团队开发,有的值是整个App里很多页面都会用到的,你保存的这个值,其他同事知道该怎么读取吗?总不能,我们每添加一个Key,就要去通知所有人吧?不现实,实际上大家很可能也会因为忙碌而忽略你的消息。
那给每个常量再添加一个注释怎么样?例如这样的:
// 昵称,String型,默认值为"尊贵用户" public final static String NICKNAME = "Nickname";
看起来问题解决了,但大部分人的习惯恐怕不太容易改,注释写得再好也得有人看啊。相信Key值一多,团队里就会有人不太注意到这些注释了,所以这种方法可行,但对开发人员无法形成强力的约束,最终效果要看团队的整体素质。
那么有没有更强力一点的约束呢?办法当然是有的,那就是整个工具类对外只暴露set/get方法,连字符串常量也不对外暴露,约束团队使用SharedPreferences必须通过工具类,就这么简单:
// 昵称,String型,默认为"尊贵用户" private final static String TEST_SCORE = "TestScore"; public static void setTestScore(int score) { if (score < 0 || score > 100) score = 0; applyValue(TEST_SCORE, score); } public static int getTestScore() { return (Integer)getValue(TEST_SCORE, 0); }
在这段代码里,只写了一个常量TEST_SCORE,即考试分数。在它的set方法里,会对给定值进行一次判断,因为你一定知道它的取值范围是多少(我这里写成了0到100),所以当遇到一些非法值时就可以直接改成一个默认的值。而get方法直接表明了该Key对应的value是整数型的,避免了因数据类型弄混导致的各种异常。
也许有人会问:这样做会不会导致这个工具类代码特别多?实际上这是一个见仁见智的问题,的确,每个Key键除了一个常量外,还多了一个set方法和一个get方法,看起来繁琐,但带来的好处是所有成员都能对此清晰明了,不容易出错,相比较之下的缺点基本可以忽略了。也有人认为这样会让APK包的方法数增加,但现实情况是一个有一定规模的App已经完全不可能将方法数控制在64K之内了,我觉得这已经不是一个需要太过于操心的问题了。
2020年7月13日 下午2:56 1F
applyValue和commitValue看起来是一样的啊
2020年8月21日 下午5:43 B1
@ 取悦 区别在于最终是commit还是apply的方式去保存数据。