咱們的任務,不是去發現一些別人尚未發現的東西。
而是針對全部人都看見的東西作一些從未有過的思考。 --魯迅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