咱们的任务,不是去发现一些别人尚未发现的东西。
而是针对全部人都看见的东西作一些从未有过的思考。 --鲁迅java
经历过多个项目或者维护一些比较老的项目的小伙伴可能会发现,在操做数据和文件这一方面(SharedPreferences文件,File文件,数据库)一般咱们会用一个工具类去完成,好比 SPUtils、FileUtils、XXXDaoManager... 之类的,里面会是一些静态方法去一个个实现具体的操做,看起来没啥问题,用得还挺爽。git
那么问题来了,随着项目的迭代和人员的变换,你会发现这类型的工具类愈来愈多,由于不一样的人他们有本身用习惯的代码,好比我如今的项目里面操做SharedPreferences文件的类就有 SpUtils,ContentUtil.getSp(),XXApplication.getContext().getSP(),还有直接用不封装的。操做 File 文件的类就有 FileUtils,CommonUtils 等,数据库就一个表一个 Manager 类。因此维护起来很是的麻烦。github
自从看了 Room 的源码后发现,原来操做数据库也能够封装得这么好,那么能不能也把 SharedPreferences 文件和 File 文件也模仿一下 Room 去封装成那样用呢,这样作的好处:数据库
那么文件存储跟数据库有什么类似之处: 保存文件的文件夹能够表明是一个数据库,里面的一个文件表明一张表,若是存储数据是用 key-value 形式的话,key 就是字段,value 就是值,这样就关联起来了。json
这里主要大概讲讲设计思路,若是不是很清楚 Room 实现原理和 APT 相关知识的朋友建议先了解一下。
完整的代码在这里:ElegantDataantd
首先,提出愿景。我但愿是这样使用的:ide
public interface SharedPreferencesInfo {
String keyUserName = "";
}
复制代码
定义一个接口,里面定义一些字段,字段的类型就是保存的类型。以上面代码为例,在使用的时候,会自动生成 putKeyUserName() 和 getKeyUserName 方法并自动存在 SharedPreferences 文件或 File 文件中。这样只须要维护好这个接口类就行了,维护成本很低,达到了想要的效果。工具
要自动生成代码,实现方式就选用 APT 去实现。 (关于 APT 网上有不少文章,这里就不具体将怎么去生成代码了)ui
首先定义一个注解,这个注解是加在接口上面的,由于只须要维护一个接口类,全部这个注解应该要能够定义文件的名称,以及要把数据存在 SharedPreferences 文件仍是 File 文件中,因此这样写:this
@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 这个库。在上面所说的定义好接口类后,接下来定义一个抽象类并继承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 注解标记的字段,将不会被解析:
@ElegantEntity(fileName = "UserInfo_Preferences")
public interface SharedPreferencesInfo extends ISharedPreferencesInfoDao {
String keyUserName = "";
@IgnoreField
int keyUserSex = 0;
}
复制代码
Rebuild 后,keyUserSex 会被忽略,相关字段的方法不会被生成。
被 @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 注解用来标注实体类,若是你须要往文件中存入实体类,那么须要加上这个注解,不然会出错。
@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个问题还没实现:
这两个问题后面会完善。
项目地址:ElegantData