在众多的 Android app 持久化方案挑选较合适的一种尤为重要,框架的体量要合适需求的体量,前者过大会拖累开发进度,后者过大则缺乏灵活性
关于Room
Room是android官方开发的一个操作Sqlite的框架,它可以像Retroift一样通过注解和接口方便地对数据库增删查改
public interface UserDao {
@Query("SELECT * FROM user")
LiveData<User> findUser();
@Update()
void updateUser(User user)
}
有趣的是,如果将数据库查询结果以LiveData或者Flowable形式返回时。后续如果对查询对应的表进行编辑时,订阅LiveData或Flowable的观察者可以收到更新后的内容
public class MainActivity extends AppCompatActivity {
@Override
public void onCreate() {
super.onCreate();
//... Dao的初始化等等...
// 订阅user
userDao.findUser().observe(this, new Observer<User>() {
@Override
public void onChanged(String s) {
((TextView)findViewByid(R.id.user_name)).setText(user.name)
}
});
findViewById(R.id.insert).setOnClickListener() {
// 更新User,然后UserName的TextView会显示为Mike
userDao.updateUser(new User("Mike"))
}
}
}
这个特性可非常强大,想想下面情况:
UserDetail页面显示用户信息,UserEdit页面编辑用户信息。当UserDetail页面切换到UserEdit页面后,在上面修改了用户信息,返回UserDetail页面要将修改后的用户信息显示出来
以前的我们的做法是通过startActivityForResult将修改后的User返回给前一个页面,这就意味着当前页面要知道前一个页面是什么,耦合性非常强。再如UserDetail页面和UserEdit页面之间隔了好几个页面的话,UserEdit页面就要一个一个页面往回传给UserDetail页面
而现在,只要UserDetail页面订阅了userDao.findUser()
,UserEdit页面调用userDao.updateUser()
,就能完成两个页面User内容的同步,无论之间隔了多少个页面
Room之所以能实现如此方便的功能,因为它实现了两个要求:
- 是一个被观察者,LiveData或者Flowable/Observable
- 一旦数据变化,被观察者就会通知观察者更新
但是,有些数据没必要用如此繁琐的Room框架
对于有大量数据需要增删查改兼排列的,用Room比较方便。但是我们只是想存储简单的用户信息,或者10来条历史记录,就没必要了,你要建立表的关联,你要写SQL,还要处理Room编译时报的一堆蛋疼的错误(貌似Room是一个印度妹子开发的)!
其实我们可以在项目中同时存在几种存储方案,以适应不同的需求。
- 对于大量的数据,而且实体间是有关联的,用Room性能会好点,Room还支持外键约束
- 对于小量的数据,十来条或者甚至只有一条的,用下面的SharedPreference合适
让SharedPreference实现Room一样的功能!
将对象存储在SharedPreference
把要存储对象(比如这里的User)转换为json字符串,然后存储在相应的键的值上
用LiveData观察SharedPreference某个键的值
技术的关键点是SharedPreference可以通过注册监听器订阅和解除订阅
/**
* 创建一个可以观察SharedPreference Key 对应的数据的变化的LiveData
*
* @param <T>
*/
abstract static class PrefLiveData<T> extends LiveData<T> {
private SharedPreferences mPref;
private String mKey;
private T mDefValue;
private SharedPreferences.OnSharedPreferenceChangeListener mListener =
(pref, key) -> {
if (Objects.equals(key, mKey)) {
setValue(getPrefValue(mPref, mKey, mDefValue));
}
};
/**
* @param sharedPreferences SharedPreference对象
* @param key 对应的SharedPreference Key
* @param defValue 默认指
*/
PrefLiveData(SharedPreferences sharedPreferences, String key, T defValue) {
mPref = sharedPreferences;
mKey = key;
mDefValue = defValue;
}
@Override
protected void onActive() {
super.onActive();
// 先立即将值发送给观察者
setValue(getPrefValue(mPref, mKey, mDefValue));
// 注册监听器,当值修改时通知观察者
mPref.registerOnSharedPreferenceChangeListener(mListener);
}
@Override
protected void onInactive() {
super.onInactive();
// 解除监听器,防止内存泄漏
mPref.unregisterOnSharedPreferenceChangeListener(mListener);
}
/**
* 如何从SharedPreference获取数据
*/
protected abstract T getPrefValue(SharedPreferences pref, String key, T defValue);
}
这里我们定义了一个类,用LiveData观察某个键的值,但是没有定义如何去读取这个值,因为getPrefValue
是抽象的
接下来实现这个抽象类,用Gson去获取这个值,将其转换为我们想要的对象T
@SuppressWarnings("SpellCheckingInspection")
private static final Gson mGSON = new Gson();
/**
* 创建LiveData用于观察对象
*/
@NonNull
public static <T> LiveData<T> objectLiveData(
SharedPreferences pref, String key, Class<T> tClass) {
return new PrefLiveData<T>(pref, key, null) {
@Override
protected T getPrefValue(
SharedPreferences pref, String key, T defValue) {
return getObject(pref, key, tClass);
}
};
}
/**
* 直接获取Pref中的对象
*/
@Nullable
public static <T> T getObject(SharedPreferences pref, String key, Class<T> tClass) {
String json = pref.getString(key, null);
if (json == null) {
return null;
}
try {
return mGSON.fromJson(json, tClass);
} catch (JsonSyntaxException ignored) {
return null;
}
或列表
列表需要用TypeToken处理列表范型
/**
* 创建LiveData用于观察对象列表
*/
@NonNull
public static <T> LiveData<List<T>> objectListLiveData(
SharedPreferences pref,
String key,
Class<T> type) {
return new PrefLiveData<List<T>>(pref, key, null) {
@Override
protected List<T> getPrefValue(
SharedPreferences pref,
String key,
List<T> defValue) {
return getObjectList(pref, key, type);
}
};
}
/**
* 帮助GSON解析列表范型数据
*/
static class GenericOf<T> implements ParameterizedType {
private final Class<T> type;
GenericOf(Class<T> type) {
this.type = type;
}
@Override
@NonNull
public Type[] getActualTypeArguments() {
return new Type[]{type};
}
@Override
@NonNull
public Type getRawType() {
return List.class;
}
@Override
public Type getOwnerType() {
return null;
}
}
/**
* 直接获取Pref中的对象列表
*/
@SuppressWarnings("unchecked")
@Nullable
public static <T> List<T> getObjectList(
SharedPreferences pref,
String key,
Class<T> type) {
String json = pref.getString(key, null);
if (json == null) {
return null;
}
try {
return mGSON.fromJson(json, new GenericOf(type));
} catch (JsonSyntaxException ignored) {
return null;
}
}
支持增删改SharedPreference
之所以要使用数据库,其中之一的原因是它可以增删查改。
下面来设计SharedPreference增删查改接口,具体实现限于篇幅就不在这里列出了,大家想去了解的话可以去我的GitHub仓库查看
增
/**
* 添加{@code object}, 到列表位置{@code index},如果位置为负数,则表示倒数第几位,
* -1代表倒数第一位,-2代表倒数第二位
*
* @return 如果索引不在范围,或者{@code key}对应的位置没有列表存在,返回false,否则为true
*/
@WorkerThread
public static <T> boolean insert(
SharedPreferences pref,
String key, Class<T> type,
T object,
int index)
// 同时还有insertFirst,insertLast队头插入,队尾插入
删
这里我把它设计得尽量和SQL语法一样,用了接口Where<T>
/**
* where条件测试
*/
public interface Where<T> {
boolean where(T object);
}
继续
/**
* 删除符合条件{@code where}的条目{@code count}个
*
* @return 删除的数量
*/
@WorkerThread
public static <T> int delete(
SharedPreferences pref,
String key,
Class<T> type,
Where<T> where,
int count
)
// 同时还有deleteOnce,碰到where返回为True的就不再继续查找下去了,达到优化性能的效果
// deleteAll删除key下面的所有数据
改
这里我把它设计得尽量和SQL语法一样,用了接口Update<T>
/**
* 更新操作
*/
public interface Update<T> {
T update(T object);
}
继续
/**
* 当条件符合{@code where}, 则执行{@code update}
*
* @param count 指定要更新的数目
* @return 多少行更新了
*/
@WorkerThread
public static <T> int update(
SharedPreferences pref,
String key,
Class<T> type,
Update<T> update,
Where<T> where,
int count)
// 和delete一样,有updateOnce和updateAny
以上内容就此告一段落,LiveData依旧不如RxJava灵活,但是比RxJava方便,拿起就用,自动和生命周期组件解除订阅
但今后有时间还会发布RxJava版本,还有基于Kotlin扩展函数的版本,将大大减低代码量,加快开发速度
完整代码可到我的Github查看
https://gist.github.com/lvsecoto/be5aea0d89beb10b8946dbdacac0ee4e
单元测试
什么?你对我写出来的代码不放心?没问题,在此奉上以上代码的单元测试
https://gist.github.com/lvsecoto/be5aea0d89beb10b8946dbdacac0ee4e#file-sharedpreferencedaotest
网友评论