APTでデータクラスからBundleに変換するクラスを自動生成してみた


経緯

Bundleにデータをセットするのがめんどい…
特にキー文字列の定義とか超めんどい…
Serialize、Parcelableとかも正直めんどい…

具体的にはこんな感じ↓↓↓の作業…めんどい…

Bundleに値を詰める作業
final Bundle args = new Bundle();
args.putInt("key_id", value.getId());
args.putString("key_tag", value.getTag());
args.putBoolean("key_enabled", value.isEnabled());
Bundleから値を取り出す作業(Fragment)
final Bundle args = getArguments();
final int id = args.getInt("key_id");
final String tag = args.getString("key_tag");
final boolean isEnabled = args.getBoolean("key_enabled");

やりたいこと

AutoValue的な感じでAnnotation付けるだけでBundleに変えてくれる、Bundleから値を取り出せるようなクラスが自動作成されるとかしたい。具体的には、Annotation Processsing Toolを使って以下の要件を満たすようなクラスを自動生成したい。

  • データクラスに保存したい値にアノテーションを付けてビルドすると自動クラスが生成される
  • データクラスを自動生成したクラスに渡すとBundleに変換される
  • 変換したBundleを再び自動生成したクラスに渡すと元のモデルクラスの値が取れる

実装イメージ

データクラスに保存したい値にアノテーションを付けてビルドすると自動クラスが生成される

Sample(モデルクラス)
@BundleGenerator // クラスを指定
public class Sample {

    private final int mId;

    public Sample(int id) {
        mId = id;
    }

    @BundleSet // 値を指定
    public int getId() { return mId; }
}

ビルドで以下のようなクラスが自動生成される。

SampleBundleGenerator(自動生成クラス)
// データクラス名 + BundleGenerator で自動生成
public class SampleBundleGenerator {
    // モデルをBundleに変換するメソッド
    @NonNull
    public static Bundle bundle(@NonNull Sample target) {
        return bundle(target, new Bundle());
    }

    @NonNull
    public static Bundle bundle(@NonNull Sample target, @NonNull Bundle bundle) {
        bundle.putInt("xxx.xxx.Sample_getId", target.getId());
        return bundle;
    }

    // Bundleから値を取り出すメソッド
    @NonNull
    public static Wrapper restore(@NonNull Bundle bundle) {
        return new Wrapper(bundle);
    }

    public static class Wrapper {
        final Bundle mBundle;

        BundleWrapper(@NonNull Bundle bundle) {
            mBundle = bundle;
        }

        public int getId() {
            return mBundle.getInt("xxx.xxx.Sample_getId");
        }
    }
}

データクラスを自動生成したクラスに渡すとBundleに変換される

データをBundleにセット
Bundle bundle = SampleBundleGenerator.bundle(value);

変換したBundleを再び自動生成したクラスに渡すと元のモデルクラスの値が取れる

Bundleから値を取り出す
SampleBundleGenerator.Wrapper sample = SampleBundleGenerator.restore(getArguments());

成果物

とりあえず実装してみた結果はこちら→ github:BundleGenerator

APT実装方法

詳細は他の記事でもアップされているので大まかな流れだけ紹介。

(1)AndroidStudioで新規プロジェクトを作成(普通にアプリを作る感じ)

(2)新規モジュールで「java library」を追加(モジュール名を processor とかにすると分かりやすい)

(3)ライブラリ側にAbstractProcessorを継承したクラス、及び使用させたいアノテーションのクラスを作る(Processorクラスにてコンパイル時にアノテーションを付けた要素を見つけて実現したいクラスを自動生成する)

@SupportedAnnotationTypes({
    "abj.bundlegenerator.processor.BundleGenerator",
    "abj.bundlegenerator.processor.BundleSet"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BundleGeneratorProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // ここでクラスを生成する
        // 引数のroundEnvから、お目当のアノテーションに該当するElementを取り出して、その情報を使ってクラスを生成する
        // JavaPoetというライブラリを使うとクラス作成が非常に楽にできる
    }
}

(4)javacがフックできるようにProcessorのエントリポイントを定義する。
以下にjavax.annotation.processing.Processor というファイルを作成し、作成したアノテーションプロセッサのクラス名を記述する(上記だと abj.bundlegenerator.processor.BundleGeneratorProcessor )

(5)使用したいモジュール側にライブラリを定義する

build.gradle
dependencies {
    implementation project(':processor')
    annotationProcessor project(':processor')
}

参考

https://qiita.com/LyricalMaestro0/items/9a4e3ec3ea7bda9ee523
https://qiita.com/opengl-8080/items/beda51fe4f23750c33e9
https://qiita.com/shiraji/items/ed674c5883ed0520791b