1. 程式人生 > >Android APT 例項講解

Android APT 例項講解

APT(Annotation Processing Tool) 即註解處理器,是一種註解處理工具,用來在編譯期掃描和處理註解,通過註解來生成 Java 檔案。即以註解作為橋樑,通過預先規定好的程式碼生成規則來自動生成 Java 檔案。此類註解框架的代表有 ButterKnife、Dragger2、EventBus

Java API 已經提供了掃描原始碼並解析註解的框架,開發者可以通過繼承 AbstractProcessor 類來實現自己的註解解析邏輯。APT 的原理就是在註解了某些程式碼元素(如欄位、函式、類等)後,在編譯時編譯器會檢查 AbstractProcessor 的子類,並且自動呼叫其 process()

方法,然後將添加了指定註解的所有程式碼元素作為引數傳遞給該方法,開發者再根據註解元素在編譯期輸出對應的 Java 程式碼

一、實現一個輕量的 “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() 方法了

MethodSpecJavaPoet 提供的一個概念,用於抽象出生成一個函式時需要的基礎元素,直接看以下方法應該就可以很容易理解其含義了

通過 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() 方法

  1. MainActivity 方法中直接呼叫 MainActivityViewBindingbind() 方法
  2. 因為 MainActivityViewBinding 的包名路徑和 Activity 是相同的,所以也可以通過反射來觸發 MainActivityViewBindingbind() 方法
/**
 * 作者: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;
    }

}
複製程式碼

ApplicationonCreate() 方法中傳入具體的實現

 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 類生成一個子類 UserPreferencesUserPreferences 包含一個 setUser(User instance) 方法

    public String setUser(User instance) {
        if (instance == null) {
            PreferencesManager.getInstance().getPreferencesHolder().remove(KEY);
            return "";
        }
        return PreferencesManager.getInstance().getPreferencesHolder().serialize(KEY, instance);
    }
複製程式碼

對應的方法生成規則如下所示。可以看出來,大體規則還是和第一節類似,一樣是需要通過字串來拼接出完整的程式碼。當中,L、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();
    }

複製程式碼

其他相應的 getset 方法生成規則就不再贅述了,有興趣研究的同學可以下載原始碼閱讀

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 表,且可以做到存取路徑的唯一性,還是可以提高一些開發效率的

有興趣看具體實現的可以點傳送門:Android_APT

更多的開發知識點可以看這裡:Java_Kotlin_Android_Learn