从Room到SharedPreference

在众多的 Android app 持久化方案挑选较合适的一种尤为重要,框架的体量要合适需求的体量,前者过大会拖累开发进度,后者过大则缺乏灵活性

关于Room

Room是android官方开发的一个操作Sqlite的框架,它可以像Retroift一样通过注解和接口方便地对数据库增删查改

public interface UserDao {
    @Query("SELECT * FROM user")
    LiveData 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() {
            @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之所以能实现如此方便的功能,因为它实现了两个要求:

  1. 是一个被观察者,LiveData或者Flowable/Observable
  2. 一旦数据变化,被观察者就会通知观察者更新

但是,有些数据没必要用如此繁琐的Room框架

对于有大量数据需要增删查改兼排列的,用Room比较方便。但是我们只是想存储简单的用户信息,或者10来条历史记录,就没必要了,你要建立表的关联,你要写SQL,还要处理Room编译时报的一堆蛋疼的错误(貌似Room是一个印度妹子开发的)!

其实我们可以在项目中同时存在几种存储方案,以适应不同的需求。

  • 对于大量的数据,而且实体间是有关联的,用Room性能会好点,Room还支持外键约束
  • 对于小量的数据,十来条或者甚至只有一条的,用下面的SharedPreference合适

让SharedPreference实现Room一样的功能!

将对象存储在SharedPreference

把要存储对象(比如这里的User)转换为json字符串,然后存储在相应的键的值上

用LiveData观察SharedPreference某个键的值

技术的关键点是SharedPreference可以通过注册监听器订阅和解除订阅

/**
 * 创建一个可以观察SharedPreference Key 对应的数据的变化的LiveData
 *
 * @param 
 */
abstract static class PrefLiveData extends LiveData {

    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  LiveData objectLiveData(
    SharedPreferences pref, String key, Class tClass) {
    return new PrefLiveData(pref, key, null) {
        @Override
        protected T getPrefValue(
            SharedPreferences pref, String key, T defValue) {
            return getObject(pref, key, tClass);
        }
    };
}

/**
 * 直接获取Pref中的对象
 */
@Nullable
public static  T getObject(SharedPreferences pref, String key, Class 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  LiveData> objectListLiveData(
    SharedPreferences pref, 
    String key, 
    Class type) {
    return new PrefLiveData>(pref, key, null) {
        @Override
        protected List getPrefValue(
            SharedPreferences pref, 
            String key, 
            List defValue) {
            return getObjectList(pref, key, type);
        }
    };
}

/**
  * 帮助GSON解析列表范型数据
  */
static class GenericOf implements ParameterizedType {

    private final Class type;

    GenericOf(Class 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  List getObjectList(
    SharedPreferences pref, 
    String key, 
    Class 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  boolean insert(
    SharedPreferences pref, 
    String key, Class type, 
    T object, 
    int index)
    
// 同时还有insertFirst,insertLast队头插入,队尾插入

这里我把它设计得尽量和SQL语法一样,用了接口Where

/**
  * where条件测试
  */
public interface Where {
    boolean where(T object);
}

继续

/**
  * 删除符合条件{@code where}的条目{@code count}个
  * 
  * @return 删除的数量
  */
@WorkerThread
public static  int delete(
    SharedPreferences pref, 
    String key, 
    Class type,
    Where where,
    int count
    )
 // 同时还有deleteOnce,碰到where返回为True的就不再继续查找下去了,达到优化性能的效果
 // deleteAll删除key下面的所有数据

这里我把它设计得尽量和SQL语法一样,用了接口Update

/**
* 更新操作
*/
public interface Update {
    T update(T object);
}

继续

/**
  * 当条件符合{@code where}, 则执行{@code update}
  *
  * @param count 指定要更新的数目
  * @return 多少行更新了
  */
  @WorkerThread
  public static  int update(
      SharedPreferences pref,
      String key,
      Class type,
      Update update,
      Where 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

你可能感兴趣的:(从Room到SharedPreference)