優雅的使用 Android SharePreference

一種簡單優雅的方法來使用Android SharePreference,基於編譯時註解自動生成噁心的代碼。 githubgit

配置Hannibai

  1. 若是您的項目使用 Gradle 構建, 只須要在您的build.gradle文件添加以下到 dependencies :github

    compile 'com.kevin:hannibai:0.5.1'
    annotationProcessor 'com.kevin:hannibai-compiler:0.5.1'
    複製代碼
  2. 引入JSON序列化json

    因爲能夠保存對象甚至集合到SharePreference,根據項目使用引入具體轉換器。bash

    1. Gsonide

      compile 'com.kevin:hannibai-converter-gson:0.2.6'
      複製代碼
    2. Jackson工具

      compile 'com.kevin:hannibai-converter-jackson:0.2.6'
      複製代碼
    3. FastJsongradle

      compile 'com.kevin:hannibai-converter-fastjson:0.2.6'
      複製代碼
    4. LoganSquareui

      compile 'com.kevin:hannibai-converter-logansquare:0.2.6'
      複製代碼
  3. 這裏僅僅實現了Gson、Jackson、FastJson及LoganSquare的實現,聰明的你確定會本身擴展,或者通知我去擴展。this

簡單使用

  1. 在Application 中初始化加密

    Hannibai.init(this);
    if (debug) {
        Hannibai.setDebug(true);
    }
    Hannibai.setConverterFactory(GsonConverterFactory.create());
    複製代碼
  2. 建立一個類,使用SharePreference進行註解

    @SharePreference
    public class AppPreference {
    }
    複製代碼
  3. 加入一個成員變量

    這裏使用public修飾,固然你也可使用privateprotected或者不寫,任何一種姿式均可以。

    @SharePreference
    public class AppPreference {
        public String name;
    }
    複製代碼
  4. Build —> Rebuild Project

    這個過程會自動生成一堆你以爲寫起來很噁心的東西。

  5. 在代碼中使用

    // 獲取AppPreference操做類
    AppPreferenceHandle preferenceHandle = Hannibai.create(AppPreferenceHandle.class);
    
    // 設置name到SharePreference
    preferenceHandle.setName("Kevin");
    // 從SharePreference中獲取name
    String name = preferenceHandle.getName();
    Toast.makeText(this, "name = " + name, Toast.LENGTH_SHORT).show();
    複製代碼

是否是跟簡單的就完成了保存數據到SharePreference,已經從SharePreference讀取數據,以前噁心的一堆東西已經替你偷偷生成並且藏起來啦~

進階使用

  1. 初始化

    在進行初始化的時候有以下兩個方法

    Hannibai.init(this);
    複製代碼
    Hannibai.init(this, false);
    複製代碼

    這兩個方法的區別就是是否對數據進行加密,若是第二個參數爲true則表明要進行加密存儲。默認爲true,這樣能夠減少文件的大小。

  2. 獲取操做類

    有兩種方式,一種是獲取通用的,另外一種是能夠傳入ID參數,爲不一樣用戶創建不一樣SharePreference文件。

    假設類爲AppPreference:

    1. 通用

      AppPreferenceHandle preferenceHandle = Hannibai.create(AppPreferenceHandle.class);
      複製代碼
    2. 區分用戶

      AppPreferenceHandle preferenceHandle = Hannibai.create(AppPreferenceHandle.class, "ID_123");
      複製代碼
  3. 生成的方法

    假設你的成員變量是name,那麼會生成如下方法:

    1. 判斷SharePreference中是否包含name

      @DefString("")
      public boolean containsName() {
      	return Hannibai.contains1(mSharedPreferencesName, mId, "name", "");
      }
      複製代碼
    2. SharePreference中獲取name

      @DefString("")
      public String getName() {
      	return Hannibai.get1(mSharedPreferencesName, mId, "name", "");
      }
      複製代碼
    3. 設置name值到SharePreference

      @Apply
      public void setName(final String name) {
      	Hannibai.set1(mSharedPreferencesName, mId, "name", -1L, false, name);
      }
      複製代碼
    4. SharePreference中移除name

      @Apply
      public void removeName() {
      	Hannibai.remove1(mSharedPreferencesName, mId, "name");
      }
      複製代碼
    5. SharePreference中移除全部數據

      @Apply
      public void removeAll() {
      	Hannibai.clear(mSharedPreferencesName, mId);
      }
      複製代碼
  4. 支持的類型

    類型 sample
    String String name;
    int int age;
    Integer Integer age;
    long long timestamp;
    Long Long timestamp;
    float float salary;
    Float Float salary;
    double double salary;
    Double Double salary;
    User User user;
    List<xxx> List userList;
    Map<xxx, xxx> Map<String, User> userMap;
    Set<xxx> Set userSet;
    XXX<xxx> 只支持一級泛型,List<List<String>> 這種是不支持的。
  5. 設置默認值

    @DefString("zwenkai")
    public String name;
    複製代碼

    支持

    類型 sample
    DefString @DefString("zwenkai")
    DefInt @DefInt(18)
    DefBoolean @DefBoolean(true)
    DefLong @DefLong(123456789)
    DefFloat @DefFloat(123.45F)
  6. 設置過時時間

    默認不會過時

    @Expire(value = 3, unit = Expire.Unit.MINUTES)
    public long salary;
    複製代碼

    能夠設置更新數據時從新倒計時:

    @Expire(value = 5, unit = Expire.Unit.MINUTES, update = true)
    public long salary;
    複製代碼

    支持

    單位 sample
    毫秒 @Expire(value = 1, unit = Expire.Unit.MILLISECONDS)
    @Expire(value = 1, unit = Expire.Unit.SECONDS)
    @Expire(value = 1, unit = Expire.Unit.MINUTES)
    小時 @Expire(value = 1, unit = Expire.Unit.HOURS)
    @Expire(value = 1, unit = Expire.Unit.DAYS)
  7. 設置提交類型

    提交類型有CommitApply兩種,默認爲Apply

    1. Commit

      @Commit
      public String userName;
      複製代碼
    2. Apply

      @Apply
      public String userName;
      複製代碼
  8. 支持RxJava

    有些時候,Observable對象更好操做,那麼你只須要一個註解@RxJava就能搞定。

    使用以下:

    @RxJava
    public String name;
    複製代碼

    生成方法:

    @DefString("")
    public Observable<String> getName1() {
        return Observable.create(
                new ObservableOnSubscribe<String>() {
                    @Override
                    public void subscribe(ObservableEmitter<String> e) throws Exception {
                        e.onNext(getName());
                        e.onComplete();
                    }
                }
        )
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
    複製代碼

    使用:

    preferenceHandle.getName1().subscribe(new Consumer<String>() {
        @Override
        public void accept(String name) throws Exception {
            Toast.makeText(MainActivity.this, "name = " + name, Toast.LENGTH_SHORT).show();
        }
    });
    複製代碼

    有人說這生成的是RxJava2啊,我還在用RxJava1咋辦?彆着急大兄弟,添給註解添加version屬性配置便可。

    生成方法:

    @DefString("")
    public Observable<String> getName1() {
        return Observable.create(
                new Observable.OnSubscribe<String>() {
                    @Override
                    public void call(Subscriber<? super String> subscriber) {
                        subscriber.onNext(getName());
                        subscriber.onCompleted();
                    }
                }
        )
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
    複製代碼

    有人又說了,你這個getName1()感受不爽,我想換個名字。添加註解屬性就能夠啦。

    @RxJava(suffix = "牛")
    public String name;
    複製代碼

    那麼在使用的時候就是這樣的:

    preferenceHandle.getName牛().subscribe(new Consumer<String>() {
    	@Override
    	public void accept(String name) throws Exception {
    		Toast.makeText(MainActivity.this, "name = " + name, Toast.LENGTH_SHORT).show();
    	}
    });
    複製代碼

原理

原理比較簡單,相信聰明的你早就想到啦。不就是編譯時註解搞的鬼嘛,恭喜你答對了。

  1. 全部的數據都封裝到BaseModel中而後轉換爲JSON存儲字符串到SharePreference

    final class BaseModel<T> {
    
        public long createTime;
        public long updateTime;
        public long expireTime;
        public long expire;
        public T data;
    	
    	// ... ...
    
    }
    複製代碼

    經過幾個時間字段來標記過時信息。

  2. 仿照Retrofit定義的JSON轉換接口

    因爲你們項目中使用JSON轉換工具存在差別,有人喜歡使用GSON,有同窗以爲FastJson是速度最快的,也有同窗感受Jackson最優。這裏均可以根據本身的狀況靈活配置,固然若是使用其餘的你也能夠本身去寫轉換器,或者告訴我我去添加支持。

    public interface Converter<F, T> {
    
        T convert(F value) throws Exception;
    
        interface Factory {
            <F> Converter<F, String> fromType(Type fromType);
    
            <T> Converter<String, T> toType(Type toType);
        }
    
    }
    複製代碼

    初始化依舊模仿:

    Hannibai.setConverterFactory(GsonConverterFactory.create());
    複製代碼
  3. 生成接口

    1. 首先生成IHandle接口

      只有一個removeAll()方法,其實主要是爲了混淆好配置而進行的抽取。

      public interface IHandle {
      	@Apply
      	void removeAll();
      }
      複製代碼
    2. 而後根據AppPreference生成AppPreferenceHandle接口

      這裏爲對AppPreference變量生成操做方法。

      public interface AppPreferenceHandle extends IHandle {
      
        @DefString("zwenkai")
        boolean containsName();
      
        @DefString("zwenkai")
        String getName();
      
        @Apply
        void setName(final String name);
      
        @Apply
        void removeName();
      }
      複製代碼
    3. 最後根據AppPreference生成AppPreferenceHandleImpl

      該類爲AppPreference接口的實現以及單例模式的封裝。

    final class AppPreferenceHandleImpl implements AppPreferenceHandle, IHandle {
    	private final String mSharedPreferencesName = "com.haha.hannibaitest.AppPreference";
    
    	private final String mId;
    
    	private AppPreferenceHandleImpl() {
    		this.mId = "";
    	}
    
    	public AppPreferenceHandleImpl(String id) {
    		this.mId = id;
    	}
    
    	public static AppPreferenceHandleImpl getInstance() {
    		return Holder.INSTANCE;
    	}
    
    	@DefString("zwenkai")
    	public boolean containsName() {
    		return Hannibai.contains1(mSharedPreferencesName, mId, "name", "zwenkai");
    	}
    
    	@DefString("zwenkai")
    	public String getName() {
    		return Hannibai.get1(mSharedPreferencesName, mId, "name", "zwenkai");
    	}
    
    	@Apply
    	public void setName(final String name) {
    		Hannibai.set1(mSharedPreferencesName, mId, "name", -1L, false, name);
    	}
    
    	@Apply
    	public void removeName() {
    		Hannibai.remove1(mSharedPreferencesName, mId, "name");
    	}
    
    	@Apply
    	public void removeAll() {
    		Hannibai.clear(mSharedPreferencesName, mId);
    	}
    
    	private static class Holder {
    		private static final AppPreferenceHandleImpl INSTANCE = new AppPreferenceHandleImpl();
    	}
    }
    複製代碼
  4. 數據加密

    數據加密比較簡單,就是對SharePreferenceKey對應的Value進行亦或運算。加密解密的方法爲同一個,很巧妙,有興趣的同窗能夠研究下。

    static final String endecode(String input) {
    	char[] key = "Hannibai".toCharArray();
    	char[] inChars = input.toCharArray();
    	for (int i = 0; i < inChars.length; i++) {
    		inChars[i] = (char) (inChars[i] ^ key[i % key.length]);
    	}
    	return new String(inChars);
    }
    複製代碼

    爲何以前說這樣加密能夠減少存儲文件的大小呢?

    未加密:

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
    	<string name="name">{&quot;data&quot;:&quot;Kevin&quot;,&quot;createTime&quot;:1509687733860,&quot;expire&quot;:-1,&quot;expireTime&quot;:0,&quot;updateTime&quot;:1509946500232}</string>
    </map>
    複製代碼

    加密:

    <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
    <map>
        <string name="name">3C CSj* CEj   =! LSSTYqWVY^QRQ~QBL :LTDSMK-5%LTYNC8 -CT_\RXP|WYV]VPQ5</string>
    </map>
    複製代碼

    其實主要是把"的轉義字符&quot;給替換掉了,壓縮點在這裏。

  5. 具體操做類

    在生成的操做類中,能夠看到都是調用了Hannibai類的方法,那麼這裏面是怎麼封裝的呢?

    public final class Hannibai {
    
    	static boolean debug = false;
    
    	public static final void init(Context context) {
    		RealHannibai.getInstance().init(context, true);
    	}
    
    	public static final void init(Context context, boolean encrypt) {
    		RealHannibai.getInstance().init(context, encrypt);
    	}
    
    	public static final void setDebug(boolean debug) {
    		Hannibai.debug = debug;
    	}
    
    	public static final <T> T create(final Class<T> preference) {
    		return RealHannibai.getInstance().create(preference);
    	}
    
    	public static final <T> T create(final Class<T> preference, String id) {
    		return RealHannibai.getInstance().create(preference, id);
    	}
    
    	public static final void setConverterFactory(Converter.Factory factory) {
    		RealHannibai.getInstance().setConverterFactory(factory);
    	}
    
    	public static final <T> boolean contains1(String name, String id, String key, T defValue) {
    		return RealHannibai.getInstance().contains(name, id, key, defValue.getClass());
    	}
    
    	public static final <T> boolean contains2(String name, String id, String key, Type type) {
    		return RealHannibai.getInstance().contains(name, id, key, type);
    	}
    
    	public static final <T> T get1(String name, String id, String key, T defValue) {
    		return RealHannibai.getInstance().get(name, id, key, defValue, defValue.getClass());
    	}
    
    	public static final <T> T get2(String name, String id, String key, Type type) {
    		return RealHannibai.getInstance().get(name, id, key, null, type);
    	}
    
    	public static final <T> void set1(String name, String id, String key, long expire, boolean updateExpire, T newValue) {
    		RealHannibai.getInstance().set1(name, id, key, expire, updateExpire, newValue);
    	}
    
    	public static final <T> boolean set2(String name, String id, String key, long expire, boolean updateExpire, T newValue) {
    		return RealHannibai.getInstance().set2(name, id, key, expire, updateExpire, newValue);
    	}
    
    	public static final void remove1(String name, String id, String key) {
    		RealHannibai.getInstance().remove1(name, id, key);
    	}
    
    	public static final boolean remove2(String name, String id, String key) {
    		return RealHannibai.getInstance().remove2(name, id, key);
    	}
    
    	public static final void clear(String name, String id) {
    		RealHannibai.getInstance().clear(name, id);
    	}
    
    }
    複製代碼

    好吧,Hannibai類能夠說啥事沒幹,大部分工做是對RealHannibai的封裝。

  6. 不騙你,真的操做類

    1. 如何獲取定義變量操做類的?

      經過以前的介紹,定義的AppPreference首先生成AppPreferenceHandle接口,而後生成AppPreferenceHandle接口的實現類AppPreferenceHandleImpl

      在使用AppPreference的時候是這樣的:

      AppPreferenceHandle preferenceHandle = Hannibai.create(AppPreferenceHandle.class);
      複製代碼

      那是怎麼經過接口獲取的實現類呢?在RealHannibai中:

      final public <T> T create(final Class<T> preference) {
      	Utils.validateHandleInterface(preference);
      	try {
      		return (T) Class.forName(preference.getName() + "Impl")
      				.getMethod("getInstance")
      				.invoke(null);
      		} catch (Exception e) {
      			Log.e(TAG, "Something went wrong!");
      			throw new RuntimeException(e);
      	}
      }
      複製代碼

      跟簡單,就是根據AppPreferenceHandle拼接Impl找到AppPreferenceHandleImpl類,而後反射調用它的getInstance()靜態方法。

      獲取帶id的實現類:

      final public <T> T create(final Class<T> preference, String id) {
      	Utils.validateHandleInterface(preference);
      	try {
      		return (T) Class.forName(preference.getName() + "Impl")
                  .getConstructor(String.class)
                  .newInstance(id);
      	} catch (Exception e) {
      		Log.e(TAG, "Something went wrong!");
          	throw new RuntimeException(e);
      	}
      }
      複製代碼

      這個也是拼接到類名,而後反射它的構造方法獲取實例。

    2. 判斷是否包含Key

      首先獲取Key對應的Value,若是Value爲空則不包含Key,若是Value不爲空則將數據轉換爲BaseModel實體看是否過時,過時也爲不包含,不過時則爲包含該Key

      final <T> boolean contains(String name, String id, String key, Type type) {
          String value = getSharedPreferences(name, id).getString(key, null);
          if (value == null || value.length() == 0) {
              if (Hannibai.debug)
                  Log.d(TAG, String.format("Value of %s is empty.", key));
              return false;
          } else {
              ParameterizedType parameterizedType = type(BaseModel.class, type);
              BaseModel<T> model = null;
              try {
                  model = (BaseModel<T>) getConverterFactory().toType(parameterizedType).convert(mEncrypt ? Utils.endecode(value) : value);
              } catch (Exception e) {
                  if (mEncrypt) {
                      Log.e(TAG, "Convert JSON to Model failed,will use unencrypted retry again.");
                  } else {
                      Log.e(TAG, "Convert JSON to Model failed,will use encrypted retry again.");
                  }
                  if (Hannibai.debug) {
                      e.printStackTrace();
                  }
                  try {
                      model = (BaseModel<T>) getConverterFactory().toType(type).convert(mEncrypt ? value : Utils.endecode(value));
                  } catch (Exception e1) {
                      Log.e(TAG, "Convert JSON to Model complete failure.");
                      if (Hannibai.debug) {
                          e1.printStackTrace();
                      }
                  }
              }
      
              if (null == model) {
                  return false;
              }
      
              if (model.dataExpired()) {
                  if (Hannibai.debug)
                      Log.d(TAG, String.format("Value of %s is %s expired, return false.", key, model.data));
                  return false;
              } else {
                  return true;
              }
          }
      }
      複製代碼

      這裏進行了解密的嘗試,好比以前配置的爲加密,存儲了數據,而後又配置了未加密,這時按照配置讀取是錯誤的,配置說未加密其實是加密的,這裏進行了容錯處理。

    3. 獲取Key對應Value

      首先獲取Key對應的Value,若是Value爲空則返回默認值,若是Value不爲空則將數據轉換爲BaseModel實體看是否過時,過時也返回默認值,不過時則返回BaseModel中對應數據。

      final <T> T get(String name, String id, String key, T defValue, Type type) {
          if (Hannibai.debug) Log.d(TAG, String.format("Retrieve the %s from the preferences.", key));
          String value = getSharedPreferences(name, id).getString(key, null);
          if (value == null || value.length() == 0) {
              if (Hannibai.debug)
                  Log.d(TAG, String.format("Value of %s is empty, return the default %s.", key, defValue));
              return defValue;
          } else {
              ParameterizedType parameterizedType = type(BaseModel.class, type);
              BaseModel<T> model = null;
              try {
                  model = (BaseModel<T>) getConverterFactory().toType(parameterizedType).convert(mEncrypt ? Utils.endecode(value) : value);
              } catch (Exception e) {
                  if (mEncrypt) {
                      Log.e(TAG, "Convert JSON to Model failed,will use unencrypted retry again.");
                  } else {
                      Log.e(TAG, "Convert JSON to Model failed,will use encrypted retry again.");
                  }
                  if (Hannibai.debug) {
                      e.printStackTrace();
                  }
                  try {
                      model = (BaseModel<T>) getConverterFactory().toType(type).convert(mEncrypt ? value : Utils.endecode(value));
                  } catch (Exception e1) {
                      Log.e(TAG, String.format("Convert JSON to Model complete failure, will return the default %s.", defValue));
                      if (Hannibai.debug) {
                          e1.printStackTrace();
                      }
                  }
              }
      
              if (null == model) {
                  return defValue;
              }
      
              if (Hannibai.debug) {
                  Log.d(TAG, String.format("Value of %s is %s, create at %s, update at %s.", key, model.data, model.createTime, model.updateTime));
                  if (!model.dataExpired()) {
                      if (model.expire > 0) {
                          Log.d(TAG, String.format("Value of %s is %s, Will expire after %s seconds.", key, model.data, (model.expireTime - System.currentTimeMillis()) / 1000));
                      } else {
                          Log.d(TAG, String.format("Value of %s is %s.", key, model.data));
                      }
      
                  }
              }
              if (model.dataExpired()) {
                  if (Hannibai.debug)
                      Log.d(TAG, String.format("Value of %s is %s expired, return the default %s.", key, model.data, defValue));
                  return defValue;
              } else {
                  return model.data;
              }
          }
      }
      複製代碼
    4. 設置Key對應值

      首先獲取Key對應的Value,若是Value不爲空,轉換爲BaseModel實體,更新對應數據,過時時間信息,而後轉化爲JSON字符串存儲,若是Value爲空則建立BaseModel並轉化爲JSON字符串存儲。

      private final <T> SharedPreferences.Editor set(String name, String id, String key, long expire, boolean updateExpire, T newValue) throws Exception {
          if (Hannibai.debug) Log.d(TAG, String.format("Set the %s value to the preferences.", key));
          BaseModel<T> model = null;
          ParameterizedType type = type(BaseModel.class, newValue.getClass());
          SharedPreferences sharedPreferences = getSharedPreferences(name, id);
          String value = sharedPreferences.getString(key, null);
          if (value != null && value.length() != 0) {
              try {
                  model = (BaseModel<T>) getConverterFactory().toType(type).convert(mEncrypt ? Utils.endecode(value) : value);
              } catch (Exception e) {
                  if (mEncrypt) {
                      Log.e(TAG, "Convert JSON to Model failed,will use unencrypted retry again.");
                  } else {
                      Log.e(TAG, "Convert JSON to Model failed,will use encrypted retry again.");
                  }
                  if (Hannibai.debug) {
                      e.printStackTrace();
                  }
                  try {
                      model = (BaseModel<T>) getConverterFactory().toType(type).convert(mEncrypt ? value : Utils.endecode(value));
                  } catch (Exception e1) {
                      Log.e(TAG, "Convert JSON to Model complete failure.");
                      if (Hannibai.debug) {
                          e1.printStackTrace();
                      }
                  }
              }
              if (null == model) {
                  model = new BaseModel<>(newValue, expire);
              } else {
                  if (model.dataExpired()) {
                      model = new BaseModel<>(newValue, expire);
                      if (Hannibai.debug)
                          Log.d(TAG, String.format("Value of %s is %s expired", key, model.data));
                  } else {
                      model.update(newValue, updateExpire);
                  }
              }
          } else {
              model = new BaseModel<>(newValue, expire);
          }
          String modelJson = getConverterFactory().fromType(type).convert(model);
          return sharedPreferences.edit().putString(key, mEncrypt ? Utils.endecode(modelJson) : modelJson);
      }
      複製代碼

混淆

若是使用了混淆,添加以下到混淆配置文件

-dontwarn com.kevin.hannibai.**
-keep class com.kevin.hannibai.** { *; }
-keep class * implements com.kevin.hannibai.IHandle { *; }
-keep @com.kevin.hannibai.annotation.SharePreference class * { *; }
複製代碼
相關文章
相關標籤/搜索