Aptを使ったAndroidライブラリの作り方

  • 38
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Apt(Annotation Processing Tool) = コンパイル時にアノテーションからソースを自動生成したりするアレ。DaggerやButterKnifeが使ってます。

Aptを使ったライブラリを作ろうと思ったのですが分かりにくかったので、ゼロベースで作っていく方法をまとめました。

作るもの

簡単な例として、POJOにアノテーションを付けると、フィールドをログ出力してくれるようなライブラリを作ってみます。

こんな感じにモデルにアノテーションを付けると、

MyModel.java
@Loggable
public class MyModel {
    @LogField
    public String foo;
    @LogField
    public int bar;

    public MyModel(String foo, int bar) {
        this.foo = foo;
        this.bar = bar;
    }
}

こんな感じでログが出せるライブラリ。

MyModel myModel = new MyModel("Hello World", 123);

// MyModelLoggerはライブラリで自動生成されるクラス
// "foo=Hello World, bar=123" と出力する
MyModelLogger.log(myModel);

今回作ったものはここにあります

作り方

Android Studioでプロジェクトを作成

  • Android applicationで作成。適当にBlank Activityを作っときます。
  • パッケージ名はサンプルアプリのパッケージ名になるので、com.kobakei.modelloggerexample としました。
  • これでappというモジュールが生成されます。これはライブラリのサンプルとして使います。

プロジェクト直下のbuild.gradleを修正

buildscriptに、android-aptを追加する

build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'

        // ここを追加
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

libraryモジュールを追加

  • Android Libraryを選んで作成します
  • パッケージ名はライブラリのパッケージ名になるので、com.kobakei.modelloggerとします
  • build.gradleを修正
library/build.gradle
apply plugin: 'com.android.library'
// ここを追加 'com.android.library'より下に書く
apply plugin: 'android-apt'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    // compilerから参照するので、コメントアウト
    //compile 'com.android.support:appcompat-v7:23.1.1'
}
  • アノテーションクラスを作成します。
    • クラスにつける@Loggableと、フィールドにつける@LogField
    • @Targetは、そのアノテーションがどのタイプに付けれるか(TYPE = クラス、FIELD = フィールド)
    • アノテーションは引数をとったりデフォルト値を定義したりできるが、ここでは使わないので説明を省略。詳しくは http://www.ne.jp/asahi/hishidama/home/tech/java/annotation.html を参考に。
Loggable.java
@Target(ElementType.TYPE)
public @interface Loggable {
}
LogField.java
@Target(ElementType.FIELD)
public @interface LogField {
}

compilerモジュールを追加

  • Java Libraryを選んで作成(Android Libraryではないので注意!
  • パッケージ名はlibraryと同じく、com.kobakei.modellogger
  • compiler/build.gradleを以下のように修正
compiler/build.gradle
apply plugin: 'java'

// ここを追加
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    // 以下を追加
    compile project(":library")
    compile 'com.squareup:javapoet:1.2.0' // Javaのソースを出力するためのライブラリ
    compile 'com.google.auto.service:auto-service:1.0-rc2' // META-INFを自動生成するためのライブラリ
    compile 'com.google.android:android:4.1.1.4' // compilerはpure java moduleなので、Android frameworkのクラスを操作する場合はこれが必要
}

// ここを追加
// libaryモジュールにいるアノテーションクラスをcompilerで使うため
sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', '../library/src/main/java']
        }
    }
}
  • 以下のように、AbstractProcessorを継承したクラスを作成
    • アノテーション
      • META-INF出力用に、@AutoServiceを付けておく
      • @SupportedAnnotationTypesには、サポートするアノテーションのクラス名を渡す
      • @SupportedSourceVersionには、Javaのバージョンを指定(7にする)
    • processメソッドの中で、Javaのソースを出力する
      • ここでは、@Loggableなクラスを見つけて、その中の@LogFieldなフィールドをLogCatに出力するクラスをsquare/javapoetを使って出力しています
MyProcessor.java
@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.kobakei.modellogger.Loggable", "com.kobakei.modellogger.LogField"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyProcessor extends AbstractProcessor {

    private Filer filer;
    private Messager messager;
    private Elements elements;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.messager = processingEnv.getMessager();
        this.filer = processingEnv.getFiler();
        this.elements = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Class<Loggable> loggableClass = Loggable.class;
        for (Element element : roundEnv.getElementsAnnotatedWith(loggableClass)) {
            ElementKind kind = element.getKind();
            if (kind == ElementKind.CLASS) {
                try {
                    generateLogger(element);
                } catch (IOException e) {
                    this.messager.printMessage(Diagnostic.Kind.ERROR, "IO error");
                }
            } else {
                this.messager.printMessage(Diagnostic.Kind.ERROR, "Type error");
            }
        }
        return true;
    }

    /**
     * エレメントからLoggerのソースを出力する
     */
    private void generateLogger(Element element) throws IOException {

        String packageName = elements.getPackageOf(element).getQualifiedName().toString();
        ClassName modelClass = ClassName.get(packageName, element.getSimpleName().toString());

        // logメソッドの定義
        // LogFieldアノテーション付きのフィールドを見つけて、ログの作成式に追加
        Class<LogField> logFieldClass = LogField.class;
        String message = null;
        for (Element el : element.getEnclosedElements()) {
            if (el.getAnnotation(logFieldClass) != null) {
                String fieldName = el.getSimpleName().toString();
                if (message == null) {
                    message = String.format("\"%s = \" + model.%s", fieldName, fieldName);
                } else {
                    message += String.format(" + \" %s = \" + model.%s ", fieldName, fieldName);
                }
            }
        }
        String tag = element.getSimpleName().toString();
        ClassName logClass = ClassName.get("android.util", "Log");
        MethodSpec logMethod = MethodSpec.methodBuilder("log")
                .addParameter(modelClass, "model")
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addStatement("$T.v(\"" + tag + "\", " + message + ")", logClass) // Log.v("modelclass", "hoge=" + hoge)
                .build();

        // クラスの定義
        String className = element.getSimpleName() + "Logger";
        TypeSpec logger = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(logMethod)
                .build();

        // ファイルへ出力
        JavaFile.builder(packageName, logger)
                .build()
                .writeTo(filer);
    }
}

appモジュール(サンプルアプリ)を修正

  • build.gradleを、compiler, libraryモジュールを使うように変更
app/build.gradle
apply plugin: 'com.android.application'
// ここを追加
apply plugin: 'android-apt'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.kobakei.modelloggerexample"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'

    // ここを追加
    compile project(':library')
    apt project(':compiler')
}

  • サンプルアプリに以下のようなモデルクラスを追加
MyModel.java
@Loggable
public class MyModel {
    @LogField
    public String foo;
    @LogField
    public int bar;

    public MyModel(String foo, int bar) {
        this.foo = foo;
        this.bar = bar;
    }
}

ビルドしてみる

  • Android Studioでビルドしてみる
  • 成功すれば、app/build/generated/source/apt/以下にMyModelLogger.javaが生成されているはず

出力されたクラスを使ってみる

  • MainActivityのonCreateに以下を追加して再ビルド
MainActivity.java
@Override
public void onCreate(Bundle savedInstanceState) {
    //...

    MyModel myModel = new MyModel("Hello World", 123);
    MyModelLogger.log(myModel);
}

これでLogCatに出力されればOK!

(おまけ)aptを使ったライブラリをJitpack.ioで配布する

Jitpackのドキュメントの手順を、libraryとcompilerの両方に対してやればよいだけ

ライブラリのユーザーは、以下のようにして使います。コロンの位置がプロジェクト名の後ろなので注意。

app/build.gradle
dependencies {
    // com.github.USERNAME.PROJECTNAME:MODULENAME:VERSION
    compile 'com.github.kobakei.ModelLogger:library:0.0.2'
    apt 'com.github.kobakei.ModelLogger:compiler:0.0.2'
}

参考にしたリポジトリ