像操做Room同样操做SharedPreferences和File文件

导读

咱们的任务,不是去发现一些别人尚未发现的东西。
而是针对全部人都看见的东西作一些从未有过的思考。 --鲁迅java

问题

经历过多个项目或者维护一些比较老的项目的小伙伴可能会发现,在操做数据和文件这一方面(SharedPreferences文件,File文件,数据库)一般咱们会用一个工具类去完成,好比 SPUtils、FileUtils、XXXDaoManager... 之类的,里面会是一些静态方法去一个个实现具体的操做,看起来没啥问题,用得还挺爽。git

那么问题来了,随着项目的迭代和人员的变换,你会发现这类型的工具类愈来愈多,由于不一样的人他们有本身用习惯的代码,好比我如今的项目里面操做SharedPreferences文件的类就有 SpUtils,ContentUtil.getSp(),XXApplication.getContext().getSP(),还有直接用不封装的。操做 File 文件的类就有 FileUtils,CommonUtils 等,数据库就一个表一个 Manager 类。因此维护起来很是的麻烦。github

思考

自从看了 Room 的源码后发现,原来操做数据库也能够封装得这么好,那么能不能也把 SharedPreferences 文件和 File 文件也模仿一下 Room 去封装成那样用呢,这样作的好处:数据库

  1. 能够去掉工具类死版的写法,让操做这些文件变得更加面向对象,更加灵活。
  2. 封装过程当中能够学到 APT 相关的知识
  3. 或者有些人认为这是瞎折腾,用工具类不就行了,但正如导读所说的,最后在这过程当中学到的才是本身的。

那么文件存储跟数据库有什么类似之处: 保存文件的文件夹能够表明是一个数据库,里面的一个文件表明一张表,若是存储数据是用 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

接下来讲说 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个问题还没实现:

  1. 读写文件权限动态申请,这个还须要本身作
  2. 结合 RxJava 和 LiveData

这两个问题后面会完善。

项目地址:ElegantData

相关文章
相关标签/搜索