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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章