Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multi-module annotation processing in Android Studio

I have a project with multiple modules in Android Studio. A module may have a dependency on another module, for example:

Module PhoneApp -> Module FeatureOne -> Module Services

I've included my annotation processing in the root module but the android-apt annotation processing occurs only at the top most level (PhoneApp) so that it should theoretically have access to all the modules at compile time. However, what I'm seeing in the generated java file is only the classes annotated in PhoneApp and none from the other modules.

PhoneApp/build/generated/source/apt/debug/.../GeneratedClass.java

In the other modules, I am finding a generated file in the intermediates directory that contains only the annotated files from that module.

FeatureOne/build/intermediates/classes/debug/.../GeneratedClass.class
FeatureOne/build/intermediates/classes/debug/.../GeneratedClass.java

My goal is to have a single generated file in PhoneApp that allows me to access the annotated files from all modules. Not entirely sure why the code generation process is running for each and failing to aggregate all annotations at PhoneApp. Any help appreciated.

Code is fairly simple and straight forward so far, checkIsValid() omitted as it works correctly:

Annotation Processor:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    try {

        for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(GuiceModule.class)) {
            if (checkIsValid(annotatedElement)) {
                AnnotatedClass annotatedClass = new AnnotatedClass((TypeElement) annotatedElement);
                if (!annotatedClasses.containsKey(annotatedClass.getSimpleTypeName())) {
                    annotatedClasses.put(annotatedClass.getSimpleTypeName(), annotatedClass);
                }
            }
        }

        if (roundEnv.processingOver()) {
            generateCode();
        }

    } catch (ProcessingException e) {
        error(e.getElement(), e.getMessage());
    } catch (IOException e) {
        error(null, e.getMessage());
    }

    return true;
}

private void generateCode() throws IOException {
    PackageElement packageElement = elementUtils.getPackageElement(getClass().getPackage().getName());
    String packageName = packageElement.isUnnamed() ? null : packageElement.getQualifiedName().toString();

    ClassName moduleClass = ClassName.get("com.google.inject", "Module");
    ClassName contextClass = ClassName.get("android.content", "Context");
    TypeName arrayOfModules = ArrayTypeName.of(moduleClass);

    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("juice")
            .addParameter(contextClass, "context")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(arrayOfModules);

    methodBuilder.addStatement("$T<$T> collection = new $T<>()", List.class, moduleClass, ArrayList.class);

    for (String key : annotatedClasses.keySet()) {

        AnnotatedClass annotatedClass = annotatedClasses.get(key);
        ClassName className = ClassName.get(annotatedClass.getElement().getEnclosingElement().toString(),
                annotatedClass.getElement().getSimpleName().toString());

        if (annotatedClass.isContextRequired()) {
            methodBuilder.addStatement("collection.add(new $T(context))", className);
        } else {
            methodBuilder.addStatement("collection.add(new $T())", className);
        }

    }

    methodBuilder.addStatement("return collection.toArray(new $T[collection.size()])", moduleClass);

    TypeSpec classTypeSpec = TypeSpec.classBuilder("FreshlySqueezed")
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
            .addMethod(methodBuilder.build())
            .build();

    JavaFile.builder(packageName, classTypeSpec)
            .build()
            .writeTo(filer);
}

This is just for a demo of annotation processing that works with Guice, if anyone is curious.

So how can I get all the annotated classes to be included in the generated PhoneApp .java file from all modules?

like image 312
fakataha Avatar asked Jul 20 '16 22:07

fakataha


1 Answers

It's never too late to answer a question on SO, so...

I have faced a very similar complication during one of tasks at work.

And I was able to resolve it.

Short version

All you need to know about generated classes from moduleB in moduleA is package and class name. That can be stored in some kind of MyClassesRegistrar generated class placed in known package. Use suffixes to avoid names clashing, get registrars by package. Instantiate them and use data from them.

Lond version

First of all - you will NOT be able to include your compile-time-only dependency ONLY at topmost module (lets call it "app" module as your typical android project structure does). Annotation processing just does not work that way and, as far as I could find out - nothing can be done about this.

Now to the details. My task was this: I have human-written annotated classes. I'll name them "events". At compile time I need to generate helper-classes for those events to incorporate their structure and content (both statically-available (annotation values, consts, etc) and runtime available (I am passing event objects to those helpers when using latter). Helper class name depends on event class name with a suffix so I don't know it until code generation finished.

So after helpers are generated I create a factory and generate code to provide new helper instance based on MyEvent.class provided. Here's the problem: I only needed one factory in app module, but it should be able to provide helpers for events from library module - this can't be done straightforward.

What I did was:

  1. skip generating factory for modules that my app module depends upon;

  2. in non-app modules generate a so-called HelpersRegistrar implementation(s):

    – they all share same package (you'll know why later);

    – their names don't clash because of suffix (see below);

    – differentiation between app module and library-module is done via javac "-Amylib.suffix=MyModuleName" param, that user MUST set - this is a limitation, but a minor one. No suffix must be specified for app module;

    – HelpersRegistrar generated implementation can provide all I need for future factory code generating: event class name, helper class name, package (these two share package for package-visibility between helper and event) - all Strings, incorporated in POJO;

  3. in app module I generate helpers - as usual, then I obtain HelperRegistrars by their package, instantiate them, run through their content to enrich my factory with code that provides helpers from other modules. All I needed for this was class names and a package.

  4. Voilà! My factory can provide instances of helpers both from app module and from other modules.

The only uncertainty left is order of creating and running processor-class instances in app module and in other modules. I have not found any solid info on this, but running my example shows that compiler (and, therefore, code generation) first runs in module that we depend upon, and then - in app module (otherwise compilation of app module will be f..cked). This gives us reason to expect known order of code processor executions in different modules.

Another, slightly similar, approach is this: skip registrars, generate factories in all modules and write factory in app module to use other factories, that you get and name same way as registrars above.

Example can be seen here: https://github.com/techery/janet-analytics - this is a library where I applied this approach (the one without registrars since I have factories, but that can be not the case for you).

P. S.: suffix param can be switched to simpler "-Amylibraryname.library=true" and factories/registrars names can be autogenerated/incremented

like image 200
Den Drobiazko Avatar answered Sep 28 '22 01:09

Den Drobiazko