做一個簡單的 APT 小專案——AppShortcut

FlyingSnowBean發表於2017-05-18

最近學習了編譯時註解框架的製作,寫了一個小專案。閱讀本文前希望大家有關於註解的相關知識。

本文介紹一個簡單的編譯時註解小專案的製作過程。專案地址:github.com/wuapnjie/Ea…,我選擇了Android API 25的新功能App Shortcut,使用註解來快速製作一個Shortcut。為什麼選擇Shortcut呢,因為我覺得很多應用只需要使用到靜態載入的Shortcut就好了,而對於靜態載入的Shortcut要寫一個比較長的Xml配置檔案,我覺得特別麻煩。

先來看一下我們實現的效果。

Java程式碼

@ShortcutApplication
@AppShortcut(resId = R.mipmap.ic_launcher,
        description = "First Shortcut")
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
          //API 呼叫
        ShortcutCreator.create(this);
    }
}複製程式碼

效果圖:

做一個簡單的 APT 小專案——AppShortcut

APT簡單介紹

APT,全稱Annotation Processing Tool,它用來在編譯時處理原始碼中的註解資訊,我們可以根據註解來生成一些Java檔案,防止編寫冗餘的程式碼,比如ButterKnife專案,正是利用了APT工具,幫助我們少寫了許多重複冗餘的程式碼。本文中,通過註解來少寫一些配置檔案。

專案結構

本專案共分為4個Module,兩個Java Library module,一個Android Library module和用於演示的Android Application module

  • easyshortcuts-api:Android Library module,用於供客戶端的呼叫。
  • easyshortcuts-annotation:Java Library module,用於提供註解類。
  • easyshortcuts-compiler:Java Library module,用於編寫處理註解並生成相關Processor的註解處理模組
  • 還有一個普通的應用模組

其中easyshortcuts-apieasyshortcuts-compiler模組依賴easyshortcuts-annotation模組。

註解模組的編寫

搭好專案後,第一個動手編寫的應該是easyshortcuts-annotation模組,通過檢視Android Developer官網上的Shortcut介紹後,發現通過Java程式碼,我們只可以通過ShortcutManager生成動態Shortcut,生成一個動態Shortcut的程式碼簡單重複,每個Shortcut需要一個String型別的Id,圖示的ResId,顯示的文字,以及一個Intent的Action欄位。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AppShortcut {
    int resId();

    int rank() default 1;

    String description();

    String action() default Define.SHORTCUT_ACTION;
}複製程式碼

之後我用註解所在類的類名稱作為Shortcut的Id。

這裡,我還建了一個註解ShortcutApplication,是一個沒有任何欄位的註解,這個註解應該用在使用者第一個開啟的Activity。因為這裡使用了動態載入的方式建立Shortcut,所以必須要執行程式碼才可以生成Shortcut,所以應該在Launcher Activity使用。

註解處理器的編寫

確定了註解後,我們要編寫相應的註解處理器來處理註解並生成相應的Java檔案,這裡easyshortcuts-compiler依賴了google的auto-service庫和square的javapoet庫。

compile "com.squareup:javapoet:$rootProject.ext.squareJavaPoetVersion"
compile "com.google.auto.service:auto-service:$rootProject.ext.googleAutoServiceVersion"複製程式碼

其中auto-service庫可以很方便的幫助我們生成配置檔案,javapoet庫可以很方便的幫助我們自動生成Java程式碼。

新建一個繼承自AbstractProcessorShortcutProcessor,下面是一個基本的Processor應有的要素,我們的重點在與process()方法。

//幫助我們生成配置檔案的註解
@AutoService(Processor.class)
public class ShortcutsProcessor extends AbstractProcessor {
    private Filer mFiler;
    private Elements mElementUtils;
    private Messager mMessager;
    ……

    //在初始化時獲得相關幫助物件
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mElementUtils = processingEnvironment.getElementUtils();
        mMessager = processingEnvironment.getMessager();
    }

      //根據相應的註解進行處理
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
           ……
        return true;
    }

      //返回要支援的註解
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ShortcutApplication.class.getCanonicalName());
        types.add(AppShortcut.class.getCanonicalName());
        return types;
    }

      //返回Java語言的支援版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //輔助的日誌列印方法
    private void printNote(String message) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, message);
    }

    private void printError(String error) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, error);
    }
}複製程式碼

process()方法中,要獲取所有有相關注解的Element,並獲取每個註解中附帶的欄位,最後根據這些資訊生成一個Java檔案。為了更好的獲取儲存這些欄位,我建立了一個model類Shortcut

public class Shortcut {
    private int mResId;
    private int mRank;
    private String mDescription;
    private String mAction;
    private TypeElement mTypeElement;

    public Shortcut(Element element) {
        mTypeElement = (TypeElement) element;
        AppShortcut appShortcut = mTypeElement.getAnnotation(AppShortcut.class);
        mResId = appShortcut.resId();
        mRank = appShortcut.rank();
        mDescription = appShortcut.description();
        mAction = appShortcut.action();
    }

      //相關的getXXX()方法
      ……

}複製程式碼

之後在process()方法中遍歷所有帶有相關注解的Element,並生成model物件

for (Element element : roundEnvironment.getElementsAnnotatedWith(AppShortcut.class)) {
     //檢查註解所標註的元素是否為我們需要
     if (!isValid(element)) {
         return false;
     }
     //解析這個element並生成相應的Shortcut物件
     parseShortcut(element);
}複製程式碼

最後根據所有Shortcut物件生成Java檔案

mShortcutClass.generateCode().writeTo(mFiler);複製程式碼

生成程式碼我使用Javapoet,可以很方便的生成程式碼,以下程式碼通過檢視Javapoet的README就可以很快理解。

public JavaFile generateCode() {
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("create")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(CONTEXT, "context")
                .addStatement("$T shortcutManager = context.getSystemService($T.class)", SHORTCUT_MANAGER, SHORTCUT_MANAGER)
                .addStatement("$T.Builder builder",SHORTCUT_INFO)
                .addStatement("$T intent",INTENT);

        for (Shortcut shortcut : mShortcuts) {
            methodBuilder.
                    addStatement("builder = new $T.Builder(context,$S)", SHORTCUT_INFO, shortcut.getTypeElement().getSimpleName().toString())
                    .addStatement("intent = new $T(context, $T.class)", INTENT, TypeName.get(shortcut.getTypeElement().asType()))
                    .addStatement("intent.setAction($S)", shortcut.getAction())
                    .addStatement("builder.setIntent(intent)")
                    .addStatement("builder.setShortLabel($S)", shortcut.getDescription())
                    .addStatement("builder.setLongLabel($S)", shortcut.getDescription())
                    .addStatement("builder.setRank($L)", shortcut.getRank())
                    .addStatement("builder.setIcon($T.createWithResource(context, $L))", ICON, shortcut.getResId())
                    .addStatement("shortcutManager.addDynamicShortcuts(singletonList(builder.build()))");
        }

        TypeSpec shortcutClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + SUFFIX)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(CREATOR)
                .addMethod(methodBuilder.build())
                .build();

        String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile
                .builder(packageName, shortcutClass)
                .addStaticImport(Collections.class, "singletonList")
                .build();
}複製程式碼

提供呼叫介面

寫完了註解的直譯器後,我們每次編譯都生成了一個Java類檔案,但是我們並沒有呼叫它,我們要提供一個介面來呼叫,本專案中提供了這樣一個靜態方法

ShortcutCreator.create(this);複製程式碼
public class ShortcutCreator {
    public static void create(Context context) {
        try {
            Class<?> targetClass = context.getClass();
            Class<?> creatorClass = Class.forName(targetClass.getName() + "$$Shortcut");
            Creator creator = (Creator) creatorClass.newInstance();
            creator.create(context);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}複製程式碼

由於我們利用APT自動生成的Java類的類名稱是知道的且提供了預設的無引數構造器,所以我們很容易生成一個物件,並呼叫其相關方法來生成相應的Shortcut。

總結

編譯時註解可以大大加快我們的開發效率,希望大家可以多製作一些編譯時註解的庫來造福廣大開發者,讓大家少些許多簡單重複的程式碼。最後附上原始碼地址:github.com/wuapnjie/Ea…,希望對大家有所幫助。

相關文章