像操做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

相關文章
相關標籤/搜索