导读
我们的任务,不是去发现一些别人还没有发现的东西。
而是针对所有人都看见的东西做一些从未有过的思考。 --鲁迅
问题
经历过多个项目或者维护一些比较老的项目的小伙伴可能会发现,在操作数据和文件这一方面(SharedPreferences文件,File文件,数据库)通常我们会用一个工具类去完成,比如 SPUtils、FileUtils、XXXDaoManager... 之类的,里面会是一些静态方法去一个个实现具体的操作,看起来没啥问题,用得还挺爽。
那么问题来了,随着项目的迭代和人员的变换,你会发现这类型的工具类越来越多,因为不同的人他们有自己用习惯的代码,比如我现在的项目里面操作SharedPreferences文件的类就有 SpUtils,ContentUtil.getSp(),XXApplication.getContext().getSP(),还有直接用不封装的。操作 File 文件的类就有 FileUtils,CommonUtils 等,数据库就一个表一个 Manager 类。所以维护起来非常的麻烦。
思考
自从看了 Room 的源码后发现,原来操作数据库也可以封装得这么好,那么能不能也把 SharedPreferences 文件和 File 文件也模仿一下 Room 去封装成那样用呢,这样做的好处:
- 可以去掉工具类死版的写法,让操作这些文件变得更加面向对象,更加灵活。
- 封装过程中可以学到 APT 相关的知识
- 或者有些人认为这是瞎折腾,用工具类不就好了,但正如导读所说的,最后在这过程中学到的才是自己的。
那么文件存储跟数据库有什么相似之处: 保存文件的文件夹可以代表是一个数据库,里面的一个文件代表一张表,如果存储数据是用 key-value 形式的话,key 就是字段,value 就是值,这样就关联起来了。
开始
这里主要大概讲讲设计思路,如果不是很清楚 Room 实现原理和 APT 相关知识的朋友建议先了解一下。
完整的代码在这里:ElegantData
首先,提出愿景。我希望是这样使用的:
public interface SharedPreferencesInfo {
String keyUserName = "";
}
复制代码
定义一个接口,里面定义一些字段,字段的类型就是保存的类型。以上面代码为例,在使用的时候,会自动生成 putKeyUserName() 和 getKeyUserName 方法并自动存在 SharedPreferences 文件或 File 文件中。这样只需要维护好这个接口类就好了,维护成本很低,达到了想要的效果。
要自动生成代码,实现方式就选用 APT 去实现。 (关于 APT 网上有很多文章,这里就不具体将怎么去生成代码了)
首先定义一个注解,这个注解是加在接口上面的,因为只需要维护一个接口类,所有这个注解应该要可以定义文件的名称,以及要把数据存在 SharedPreferences 文件还是 File 文件中,所以这样写:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ElegantEntity {
int TYPE_PREFERENCE = 0;
int TYPE_FILE = 1;
String fileName() default "";
int fileType() default TYPE_PREFERENCE;
}
复制代码
定义两个方法,两个类型,文件名默认为空,默认存在 SharedPreferences 文件中。
使用效果:
//会生成名为UserInfo_Preferences的sp文件
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo {
String keyUserName = "";
}
//会生成名为CacheFile.txt的File文件
@ElegantEntity(fileName = "CacheFile.txt", fileType = ElegantEntity.TYPE_FILE)
public interface FileCacheInfo extends IFileCacheInfoDao {
int keyPassword = 0;
}
复制代码
接口和注解都定义好了,接下来就按照 APT 的规则去对应的生成相关代码即可。
可问题来了: 在使用 Room 的时候,我们需要定义一个 Dao 接口,里面定义一些增删查改的接口方法,用的时候就直接调用相关的方法即可,这里的接口其实是跟 Dao 接口类似的,但是因为 Dao 接口需要自己定义方法,而我们这里操作文件其实无非只需要 putXXX 方法和 getXXX 方法(大部分情况下),我只想写上字段即可,并不想给每个字段还写上 putXXX 和 getXXX 接口方法,但是不写的话又怎么调用呢?APT 并不能给现有的类添加方法。
想到的解决办法是既然修改不了现有的,那么就根据现有的生成一个有 putXXX 和 getXXX 接口方法的类,然后继承不就好了。
public interface ISharedPreferencesInfoDao {
void putKeyUserName(String value);
String getKeyUserName();
String getKeyUserName(String defValue);
boolean removeKeyUserName();
boolean containsKeyUserName();
boolean clear();
}
复制代码
ISharedPreferencesInfoDao 就是根据 SharedPreferencesInfo 生成的接口类,然后我们修改一下之前的代码:
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo extends ISharedPreferencesInfoDao {
String keyUserName = "";
}
复制代码
这样,SharedPreferencesInfo 就有了对应的接口方法了。
ElegantData
接下来说说 ElegantData 这个库。在上面所说的定义好接口类后,接下来定义一个抽象类并继承ElegantDataBase :
@ElegantDataMark
public abstract class AppDataBase extends ElegantDataBase {
}
复制代码
并且加上 @ElegantDataMark 注解让编译器找到它。Room 的 RoomDataBase 的功能主要是创建数据库,而这里的 ElegantDataBase 功能也是类似的,它主要的作用是创建文件夹。
然后里面我们再对应上面加上两个抽象方法:
@ElegantDataMark
public abstract class AppDataBase extends ElegantDataBase {
public abstract SharedPreferencesInfo getSharedPreferencesInfo();
public abstract FileCacheInfo getFileCacheInfo();
}
复制代码
rebuild 一下看看生成的代码:
public class AppDataBase_Impl extends AppDataBase {
private com.lzx.elegantdata.SharedPreferencesInfo mSharedPreferencesInfo;
private com.lzx.elegantdata.FileCacheInfo mFileCacheInfo;
//该方法主要用于创建文件夹
@Override
protected IFolderCreateHelper createDataFolderHelper(Configuration configuration) {
return configuration.mFactory.create(configuration.context, configuration.destFileDir);
}
//getSharedPreferencesInfo具体实现方法
@Override
public com.lzx.elegantdata.SharedPreferencesInfo getSharedPreferencesInfo() {
if (mSharedPreferencesInfo != null) {
return mSharedPreferencesInfo;
} else {
synchronized (this) {
if (mSharedPreferencesInfo == null) {
SharedPreferences sharedPreferences = getCreateHelper().getContext()
.getSharedPreferences("UserInfo_Preferences", Context.MODE_PRIVATE);
mSharedPreferencesInfo = new SharedPreferencesInfo_Impl(sharedPreferences);
}
return mSharedPreferencesInfo;
}
}
}
//getFileCacheInfo具体实现方法
@Override
public com.lzx.elegantdata.FileCacheInfo getFileCacheInfo() {
if (mFileCacheInfo != null) {
return mFileCacheInfo;
} else {
synchronized (this) {
if (mFileCacheInfo == null) {
IFolderCreateHelper createHelper = getCreateHelper();
mFileCacheInfo = new FileCacheInfo_Impl(createHelper);
}
return mFileCacheInfo;
}
}
}
}
复制代码
抽象方法和接口都会对应的生成实现类,实现类的名字是抽象类或者接口类名字加上 _Impl。
AppDataBase 的实现类 AppDataBase_Impl 定义了两个变量和三个方法,其中 createDataFolderHelper 方法主要是用于创建文件夹的,对于 SharedPreferences 文件我们不需要创建文件夹,所以这方法是针对 File 文件用的。其他方法和变量是根据在 AppDataBase 中定义的抽象方法生成的。
SharedPreferencesInfo 接口的实现类是 SharedPreferencesInfo_Impl,在 getSharedPreferencesInfo 方法中通过单例模式获取。
getFileCacheInfo 也一样。而他们的实现类里面实现的就是接口方法的具体操作了。
如何使用
那么在看了生成的代码后,我想大概都知道是怎么回事了,下面看看如何使用。
首先在 AppDataBase 中使用单例去获取 AppDataBase_Impl 实例,AppDataBase 完整代码:
@ElegantDataMark
public abstract class AppDataBase extends ElegantDataBase {
public abstract SharedPreferencesInfo getSharedPreferencesInfo();
public abstract FileCacheInfo getFileCacheInfo();
private static AppDataBase spInstance;
private static AppDataBase fileInstance;
private static final Object sLock = new Object();
//使用SP文件
public static AppDataBase withSp() {
synchronized (sLock) {
if (spInstance == null) {
spInstance = ElegantData
.preferenceBuilder(ElegantApplication.getContext(), AppDataBase.class)
.build();
}
return spInstance;
}
}
//使用File文件
public static AppDataBase withFile() {
synchronized (sLock) {
if (fileInstance == null) {
String path = Environment.getExternalStorageDirectory() + "/ElegantFolder";
fileInstance = ElegantData
.fileBuilder(ElegantApplication.getContext(), path, AppDataBase.class)
.build();
}
return fileInstance;
}
}
}
复制代码
如果使用 SharedPreferences 文件,调用 ElegantData#preferenceBuilder 方法去构建实例,如果是 File 文件,则使用 ElegantData#fileBuilder 去构建。
两个方法都需要传入上下文和 AppDataBase 的 class。唯一不一样的是使用 File 文件需要先创建文件夹,所以在第二个参数传入的是创建文件夹的路径。
使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//使用 SP 文件存入数据
AppDataBase.withSp().getSharedPreferencesInfo().putKeyUserName("小明");
//使用 File 文件存入数据
AppDataBase.withFile().getFileCacheInfo().putKeyPassword(123456789);
String userName = AppDataBase.withSp().getSharedPreferencesInfo().getKeyUserName();
Log.i("MainActivity", "userName = " + userName);
int password = AppDataBase.withFile().getFileCacheInfo().getKeyPassword();
Log.i("MainActivity", "password = " + password);
}
复制代码
最后看看存储结果吧:
SharedPreferences 文件:
File 文件:
可以看到,如果是存 File 文件的,内容是加密的。
其他注解:
@IgnoreField
被 @IgnoreField 注解标记的字段,将不会被解析:
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo extends ISharedPreferencesInfoDao {
String keyUserName = "";
@IgnoreField
int keyUserSex = 0;
}
复制代码
Rebuild 后,keyUserSex 会被忽略,相关字段的方法不会被生成。
@NameField
被 @NameField 注解标记的字段,可以重命名:
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo extends ISharedPreferencesInfoDao {
String keyUserName = "";
@NameField(value = "sex")
int keyUserSex = 0;
}
复制代码
字段 keyUserSex 解析后生成的 put 和 get 方法是 putSex 和 getSex , 而不是 putUserSex 和 getUserSex。
@EntityClass
@EntityClass 注解用来标注实体类,如果你需要往文件中存入实体类,那么需要加上这个注解,否则会出错。
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo extends ISharedPreferencesInfoDao {
String keyUserName = "";
@EntityClass(value = SimpleJsonParser.class)
User user = null;
}
复制代码
如上所示,@EntityClass 注解需要传入一个 json 解析器,存入实体类的原理是把实体类通过解析器变成 json 字符串存入文件,取出来的时候 通过解析器解析 json 字符串变成实体类。
public class SimpleJsonParser extends JsonParser<User> {
private Gson mGson;
public SimpleJsonParser(Class<User> clazz) {
super(clazz);
mGson = new Gson();
}
@Override
public String convertObject(User object) {
return mGson.toJson(object);
}
@Override
public User onParse(@NonNull String json) {
return mGson.fromJson(json, User.class);
}
}
复制代码
json 解析器需要实现两个方法,convertObject 方法作用是把实体类变成 json 字符串,onParse 方法作用是把 json 字符串变成 实体类。
目前还有2个问题还没实现:
- 读写文件权限动态申请,这个还需要自己做
- 结合 RxJava 和 LiveData
这两个问题后面会完善。
项目地址:ElegantData