Android APT 例項講解
APT(Annotation Processing Tool) 即註解處理器,是一種註解處理工具,用來在編譯期掃描和處理註解,通過註解來生成 Java 檔案。即以註解作為橋樑,通過預先規定好的程式碼生成規則來自動生成 Java 檔案。此類註解框架的代表有 ButterKnife、Dragger2、EventBus 等
Java API 已經提供了掃描原始碼並解析註解的框架,開發者可以通過繼承 AbstractProcessor 類來實現自己的註解解析邏輯。APT 的原理就是在註解了某些程式碼元素(如欄位、函式、類等)後,在編譯時編譯器會檢查 AbstractProcessor 的子類,並且自動呼叫其 process()
一、實現一個輕量的 “ButterKnife”
這裡以 ButterKnife 為實現目標,在講解 Android APT 的內容的同時,逐步實現一個輕量的控制元件繫結框架,即通過註解來自動生成如下所示的 findViewById() 程式碼
package hello.leavesc.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221));
_mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
_mainActivity.btn_serializeAll = (android.widget.Button) (_mainActivity.findViewById(2131165220));
_mainActivity.btn_remove = (android.widget.Button) (_mainActivity.findViewById(2131165219));
_mainActivity.btn_print = (android.widget.Button) (_mainActivity.findViewById(2131165218));
_mainActivity.et_userName = (android.widget.EditText) (_mainActivity.findViewById(2131165246));
_mainActivity.et_userAge = (android.widget.EditText) (_mainActivity.findViewById(2131165245));
_mainActivity.et_singleUserName = (android.widget.EditText) (_mainActivity.findViewById(2131165244));
_mainActivity.et_bookName = (android.widget.EditText) (_mainActivity.findViewById(2131165243));
}
}
複製程式碼
控制元件繫結的方式如下所示
@BindView(R.id.et_userName)
EditText et_userName;
@BindView(R.id.et_userAge)
EditText et_userAge;
@BindView(R.id.et_bookName)
EditText et_bookName;
複製程式碼
1.1、建立 Module
首先在工程中新建一個 Java Library,命名為 apt_processor,用於存放 AbstractProcessor 的實現類。再新建一個 Java Library,命名為 apt_annotation
當中,apt_processor 需要匯入如下依賴
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.auto.service:auto-service:1.0-rc2'
implementation 'com.squareup:javapoet:1.10.0'
implementation project(':apt_annotation')
}
複製程式碼
當中,JavaPoet 是 square 開源的 Java 程式碼生成框架,可以很方便地通過其提供的 API 來生成指定格式(修飾符、返回值、引數、函式體等)的程式碼。auto-service 是由 Google 開源的註解註冊處理器
實際上,上面兩個依賴庫並不是必須的,可以通過硬編碼程式碼生成規則來替代,但還是建議使用這兩個庫,因為這樣程式碼的可讀性會更高,且能提高開發效率
app Module 需要依賴這兩個 Java Library
implementation project(':apt_annotation')
annotationProcessor project(':apt_processor')
複製程式碼
這樣子,我們需要的所有基礎依賴關係就搭建好了
1.2、編寫程式碼生成規則
首先觀察自動生成的程式碼,可以歸納出幾點需要實現的地方:
1、檔案和源 Activity 處在同個包名下
2、類名以 Activity名 + ViewBinding 組成
3、bind() 方法通過傳入 Activity 物件來獲取其宣告的控制元件物件來對其進行例項化,這也是 ButterKnife 要求需要繫結的控制元件變數不能宣告為 private 的原因
package hello.leavesc.apt;
public class MainActivityViewBinding {
public static void bind(MainActivity _mainActivity) {
_mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221));
_mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
...
}
}
複製程式碼
在 apt_processor Module 中建立 BindViewProcessor 類並繼承 AbstractProcessor 抽象類,該抽象類含有一個抽象方法 process() 以及一個非抽象方法 getSupportedAnnotationTypes() 需要由我們來實現
/**
* 作者:leavesC
* 時間:2019/1/3 14:32
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}
複製程式碼
getSupportedAnnotationTypes() 方法用於指定該 AbstractProcessor 的目標註解物件,process() 方法則用於處理包含指定註解物件的程式碼元素
BindView 註解的宣告如下所示,放在 apt_annotation 中,註解值 value 用於宣告 viewId
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
複製程式碼
要自動生成 findViewById() 方法,則需要獲取到控制元件變數的引用以及對應的 viewid,所以需要先遍歷出每個 Activity 包含的所有註解物件
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//獲取所有包含 BindView 註解的元素
Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();
for (Element element : elementSet) {
//因為 BindView 的作用物件是 FIELD,因此 element 可以直接轉化為 VariableElement
VariableElement variableElement = (VariableElement) element;
//getEnclosingElement 方法返回封裝此 Element 的最裡層元素
//如果 Element 直接封裝在另一個元素的宣告中,則返回該封裝元素
//此處表示的即 Activity 類物件
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
if (variableElementMap == null) {
variableElementMap = new HashMap<>();
typeElementMapHashMap.put(typeElement, variableElementMap);
}
//獲取註解值,即 ViewId
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int viewId = bindAnnotation.value();
//將每個包含了 BindView 註解的欄位物件以及其註解值儲存起來
variableElementMap.put(viewId, variableElement);
}
...
return true;
}
複製程式碼
當中,Element 用於代表程式的一個元素,這個元素可以是:包、類、介面、變數、方法等多種概念。這裡以 Activity 物件作為 Key ,通過 map 來儲存不同 Activity 下的所有註解物件
獲取到所有的註解物件後,就可以來構造 bind() 方法了
MethodSpec 是 JavaPoet 提供的一個概念,用於抽象出生成一個函式時需要的基礎元素,直接看以下方法應該就可以很容易理解其含義了
通過 addCode() 方法把需要的引數元素填充進去,迴圈生成每一行 findView 方法
/**
* 生成方法
*
* @param typeElement 註解物件上層元素物件,即 Activity 物件
* @param variableElementMap Activity 包含的註解物件以及註解的目標物件
* @return
*/
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
//方法引數名
String parameter = "_" + StringUtils.toLowerCaseFirstChar(className.simpleName());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(className, parameter);
for (int viewId : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(viewId);
//被註解的欄位名
String name = element.getSimpleName().toString();
//被註解的欄位的物件型別的全名稱
String type = element.asType().toString();
String text = "{0}.{1}=({2})({3}.findViewById({4}));";
methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
}
return methodBuilder.build();
}
複製程式碼
完整的程式碼宣告如下所示
/**
* 作者:leavesC
* 時間:2019/1/3 14:32
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> hashSet = new HashSet<>();
hashSet.add(BindView.class.getCanonicalName());
return hashSet;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//獲取所有包含 BindView 註解的元素
Set<? extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class);
Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>();
for (Element element : elementSet) {
//因為 BindView 的作用物件是 FIELD,因此 element 可以直接轉化為 VariableElement
VariableElement variableElement = (VariableElement) element;
//getEnclosingElement 方法返回封裝此 Element 的最裡層元素
//如果 Element 直接封裝在另一個元素的宣告中,則返回該封裝元素
//此處表示的即 Activity 類物件
TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement);
if (variableElementMap == null) {
variableElementMap = new HashMap<>();
typeElementMapHashMap.put(typeElement, variableElementMap);
}
//獲取註解值,即 ViewId
BindView bindAnnotation = variableElement.getAnnotation(BindView.class);
int viewId = bindAnnotation.value();
//將每個包含了 BindView 註解的欄位物件以及其註解值儲存起來
variableElementMap.put(viewId, variableElement);
}
for (TypeElement key : typeElementMapHashMap.keySet()) {
Map<Integer, VariableElement> elementMap = typeElementMapHashMap.get(key);
String packageName = ElementUtils.getPackageName(elementUtils, key);
JavaFile javaFile = JavaFile.builder(packageName, generateCodeByPoet(key, elementMap)).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 生成 Java 類
*
* @param typeElement 註解物件上層元素物件,即 Activity 物件
* @param variableElementMap Activity 包含的註解物件以及註解的目標物件
* @return
*/
private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
//自動生成的檔案以 Activity名 + ViewBinding 進行命名
return TypeSpec.classBuilder(ElementUtils.getEnclosingClassName(typeElement) + "ViewBinding")
.addModifiers(Modifier.PUBLIC)
.addMethod(generateMethodByPoet(typeElement, variableElementMap))
.build();
}
/**
* 生成方法
*
* @param typeElement 註解物件上層元素物件,即 Activity 物件
* @param variableElementMap Activity 包含的註解物件以及註解的目標物件
* @return
*/
private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {
ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
//方法引數名
String parameter = "_" + StringUtils.toLowerCaseFirstChar(className.simpleName());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(className, parameter);
for (int viewId : variableElementMap.keySet()) {
VariableElement element = variableElementMap.get(viewId);
//被註解的欄位名
String name = element.getSimpleName().toString();
//被註解的欄位的物件型別的全名稱
String type = element.asType().toString();
String text = "{0}.{1}=({2})({3}.findViewById({4}));";
methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
}
return methodBuilder.build();
}
}
複製程式碼
1.3、註解繫結效果
首先在 MainActivity 中宣告兩個 BindView 註解,然後 Rebuild Project,使編譯器根據 BindViewProcessor 生成我們需要的程式碼
public class MainActivity extends AppCompatActivity {
@BindView(R.id.tv_hint)
TextView tv_hint;
@BindView(R.id.btn_hint)
Button btn_hint;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
複製程式碼
rebuild 結束後,就可以在 generatedJava 資料夾下看到 MainActivityViewBinding 類自動生成了
此時有兩種方式可以用來觸發 bind() 方法
- 在 MainActivity 方法中直接呼叫 MainActivityViewBinding 的 bind() 方法
- 因為 MainActivityViewBinding 的包名路徑和 Activity 是相同的,所以也可以通過反射來觸發 MainActivityViewBinding 的 bind() 方法
/**
* 作者:leavesC
* 時間:2019/1/3 14:34
* 描述:
* GitHub:https://github.com/leavesC
* Blog:https://www.jianshu.com/u/9df45b87cfdf
*/
public class ButterKnife {
public static void bind(Activity activity) {
Class clazz = activity.getClass();
try {
Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
Method method = bindViewClass.getMethod("bind", activity.getClass());
method.invoke(bindViewClass.newInstance(), activity);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製程式碼
兩種方式各有優缺點。第一種方式在每次 build project 後才會生成程式碼,在這之前無法引用到對應的 ViewBinding 類。第二種方式可以用固定的方法呼叫方式,但是相比方式一,反射會略微多消耗一些效能
但這兩種方式的執行結果是完全相同的
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainActivityViewBinding.bind(this);
tv_hint.setText("leavesC Success");
btn_hint.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "Hello", Toast.LENGTH_SHORT).show();
}
});
}
複製程式碼
二、物件 持久化+序列化+反序列化 框架
通過第一節的內容,讀者應該瞭解到了 APT 其強大的功能了 。這一節再來實現一個可以方便地將 物件進行持久化+序列化+反序列 的框架
2.1、確定目標
通常,我們的應用都會有很多配置項需要進行快取,比如使用者資訊、設定項開關、伺服器IP地址等。如果採用原生的 SharedPreferences 來實現的話,則很容易就寫出如下醜陋的程式碼,不僅需要維護多個數據項的 key 值,而且每次存入和取出資料時都會有一大片重複的程式碼,不易維護
SharedPreferences sharedPreferences = getSharedPreferences("SharedPreferencesName", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("IP", "192.168.0.1");
editor.commit();
String userName = sharedPreferences.getString("userName", "");
String ip = sharedPreferences.getString("IP", "");
複製程式碼
因此,這裡就來通過 APT 來實現一個可以方便地對資料進行 持久化+序列化+反序列化 的框架,具體的目標有以下幾點:
1、可以將 Object 進行序列化,並且提供反序列化為 Object 的方法
2、Object 的序列化結果可以持久化儲存到本地
3、持久化資料時需要的唯一 key 值由框架內部自動進行維護
4、序列化、反序列化、持久化的具體過程由框架外部實現,框架只負責搭建操作邏輯
目標1可以通過 Gson 來實現,目標2則可以通過使用騰訊開源的 MMKV 框架來實現,需要匯入以下兩個依賴
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.tencent:mmkv:1.0.16'
複製程式碼
2.2、效果預覽
這裡先預先看下框架的使用方式。新的註解以 Preferences 命名,假設 User 類中有三個欄位值需要進行本地快取,因此都為其加上 Preferences 註解
public class User {
@Preferences
private String name;
@Preferences
private int age;
@Preferences
private Book book;
...
}
複製程式碼
而我們要做的,就是通過 APT 自動為 User 類來生成一個 UserPreferences 子類,之後的資料快取操作都是通過 UserPreferences 來進行
快取整個物件
User user = new User();
UserPreferences.get().setUser(user);
複製程式碼
快取單個屬性值
String userName = et_singleUserName.getText().toString();
UserPreferences.get().setName(userName);
複製程式碼
獲取快取的物件
User user = UserPreferences.get().getUser();
複製程式碼
移除快取的物件
UserPreferences.get().remove();
複製程式碼
可以看到,整個操作都是十分的簡潔,之後就來開工吧
2.3、實現操作介面
為了實現目標4,需要先定義好操作介面,並由外部傳入具體的實現
public interface IPreferencesHolder {
//序列化
String serialize(String key, Object src);
//反序列化
<T> T deserialize(String key, Class<T> classOfT);
//移除指定物件
void remove(String key);
}
複製程式碼
以上三個操作對於框架內部來說應該是唯一的,因此可以通過單例模式來全域性維護。APT 生成的程式碼就通過此入口來呼叫 持久化+序列化+反序列化 方法
public class PreferencesManager {
private IPreferencesHolder preferencesHolder;
private PreferencesManager() {
}
public static PreferencesManager getInstance() {
return PreferencesManagerHolder.INSTANCE;
}
private static class PreferencesManagerHolder {
private static PreferencesManager INSTANCE = new PreferencesManager();
}
public void setPreferencesHolder(IPreferencesHolder preferencesHolder) {
this.preferencesHolder = preferencesHolder;
}
public IPreferencesHolder getPreferencesHolder() {
return preferencesHolder;
}
}
複製程式碼
在 Application 的 onCreate() 方法中傳入具體的實現
PreferencesManager.getInstance().setPreferencesHolder(new PreferencesMMKVHolder());
複製程式碼
public class PreferencesMMKVHolder implements IPreferencesHolder {
@Override
public String serialize(String key, Object src) {
String json = new Gson().toJson(src);
MMKV kv = MMKV.defaultMMKV();
kv.putString(key, json);
return json;
}
@Override
public <T> T deserialize(String key, Class<T> classOfT) {
MMKV kv = MMKV.defaultMMKV();
String json = kv.decodeString(key, "");
if (!TextUtils.isEmpty(json)) {
return new Gson().fromJson(json, classOfT);
}
return null;
}
@Override
public void remove(String key) {
MMKV kv = MMKV.defaultMMKV();
kv.remove(key);
}
}
複製程式碼
2.4、編寫程式碼生成規則
一樣是需要繼承 AbstractProcessor 類,子類命名為 PreferencesProcessor
首先,PreferencesProcessor 類需要生成一個序列化整個物件的方法。例如,需要為 User 類生成一個子類 UserPreferences ,UserPreferences 包含一個 setUser(User instance) 方法
public String setUser(User instance) {
if (instance == null) {
PreferencesManager.getInstance().getPreferencesHolder().remove(KEY);
return "";
}
return PreferencesManager.getInstance().getPreferencesHolder().serialize(KEY, instance);
}
複製程式碼
對應的方法生成規則如下所示。可以看出來,大體規則還是和第一節類似,一樣是需要通過字串來拼接出完整的程式碼。當中,T 都是替代符,作用類似於 MessageFormat
/**
* 構造用於序列化整個物件的方法
*
* @param typeElement 註解物件上層元素物件,即 Java 物件
* @return
*/
private MethodSpec generateSetInstanceMethod(TypeElement typeElement) {
//頂層類類名
String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement);
//方法名
String methodName = "set" + StringUtils.toUpperCaseFirstChar(enclosingClassName);
//方法引數名
String fieldName = "instance";
MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addParameter(ClassName.get(typeElement.asType()), fieldName);
builder.addStatement("if ($L == null) { $T.getInstance().getPreferencesHolder().remove(KEY); return \"\"; }", fieldName, serializeManagerClass);
builder.addStatement("return $T.getInstance().getPreferencesHolder().serialize(KEY, $L)", serializeManagerClass, fieldName);
return builder.build();
}
複製程式碼
此外,還需要一個用於反序列化本地快取的資料的方法
public User getUser() {
return PreferencesManager.getInstance().getPreferencesHolder().deserialize(KEY, User.class);
}
複製程式碼
對應的方法生成規則如下所示
/**
* 構造用於獲取整個序列化物件的方法
*
* @param typeElement 註解物件上層元素物件,即 Java 物件
* @return
*/
private MethodSpec generateGetInstanceMethod(TypeElement typeElement) {
//頂層類類名
String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement);
//方法名
String methodName = "get" + StringUtils.toUpperCaseFirstChar(enclosingClassName);
MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(typeElement.asType()));
builder.addStatement("return $T.getInstance().getPreferencesHolder().deserialize(KEY, $L.class)", serializeManagerClass, enclosingClassName);
return builder.build();
}
複製程式碼
為了實現目標三(持久化資料時需要的唯一 key 值由框架內部自動進行維護),在持久化時使用的 key 值由當前的 包名路徑+類名 來決定,由此保證 key 值的唯一性
例如,UserPreferences 類快取資料使用的 key 值是
private static final String KEY = "leavesc.hello.apt.model.UserPreferences";
複製程式碼
對應的方法生成規則如下所示
/**
* 定義該註解類在序列化時使用的 Key
*
* @param typeElement 註解物件上層元素物件,即 Java 物件
* @return
*/
private FieldSpec generateKeyField(TypeElement typeElement) {
return FieldSpec.builder(String.class, "KEY")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("\"" + typeElement.getQualifiedName().toString() + SUFFIX + "\"")
.build();
}
複製程式碼
其他相應的 get 和 set 方法生成規則就不再贅述了,有興趣研究的同學可以下載原始碼閱讀
2.5、實際體驗
修改 MainActivity 的佈局
public class MainActivity extends AppCompatActivity {
@BindView(R.id.et_userName)
EditText et_userName;
@BindView(R.id.et_userAge)
EditText et_userAge;
···
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ButterKnife.bind(this);
MainActivityViewBinding.bind(this);
btn_serializeAll.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String userName = et_userName.getText().toString();
String ageStr = et_userAge.getText().toString();
int age = 0;
if (!TextUtils.isEmpty(ageStr)) {
age = Integer.parseInt(ageStr);
}
String bookName = et_bookName.getText().toString();
User user = new User();
user.setAge(age);
user.setName(userName);
Book book = new Book();
book.setName(bookName);
user.setBook(book);
UserPreferences.get().setUser(user);
}
});
btn_serializeSingle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String userName = et_singleUserName.getText().toString();
UserPreferences.get().setName(userName);
}
});
btn_remove.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
UserPreferences.get().remove();
}
});
btn_print.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
User user = UserPreferences.get().getUser();
if (user == null) {
tv_hint.setText("null");
} else {
tv_hint.setText(user.toString());
}
}
});
}
}
複製程式碼
資料的整個存取過程自我感覺還是十分的簡單的,不用再自己去維護臃腫的 key 表,且可以做到存取路徑的唯一性,還是可以提高一些開發效率的