導讀
我們的任務,不是去發現一些別人還沒有發現的東西。
而是針對所有人都看見的東西做一些從未有過的思考。 --魯迅
問題
經歷過多個專案或者維護一些比較老的專案的小夥伴可能會發現,在運算元據和檔案這一方面(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