Search code examples
javaandroidannotationsdagger-2

What is @AutoAnnotation for? How could it be used?


In https://dagger.dev/multibindings.html, I read about @AutoAnnotation. It has a reference to https://github.com/google/auto/blob/master/value/src/main/java/com/google/auto/value/AutoAnnotation.java.

It is also mentioned in https://github.com/google/auto/blob/57dfad360306619a820e6aae4a14a1aa67c29299/value/userguide/howto.md#annotation

I read about it, can't get to understand it.

I manage to access it from my Android code using

implementation 'com.google.auto.value:auto-value:1.5.2'
kapt 'com.google.auto.value:auto-value:1.5.2'

And also

android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true

But I don't understand how it could be used. Is there any good tutorial of using it?


Solution

  • AutoAnnotation automatically generates a class that implements an annotation interface in the same way the JDK does.

    Dagger map keys

    When using a Multibindings map through Dagger that uses a custom annotation as its key, Dagger will install the instance T or Provider Provider<T> into the returned map, using the annotation instance itself as the key. To make this clearer:

    @MapKey
    @interface YourAnnotation {
      String foo();
    }
    
    @Provides @YourAnnotation(foo="bar") YourClass getYourClassForBar() { /* ... */ }
    
    // Dagger will create a multibinding that would allow you to inject this:
    @Inject Map<YourAnnotation, YourClass> map;
    

    If the only thing that matters here is foo, you could also use unwrapKeys to make the map keyed by String instead of YourAnnotation, but let's assume you want this because you want YourAnnotation to have multiple values in the future. But where do the implementations of YourAnnotation come from, and how are you supposed to call get on the map?

    Annotations at runtime

    When you annotate a Java element (frequently a class, method, or field) Java will return a particular implementation of that class's annotation. From the Java tutorial:

    @interface ClassPreamble {
       String author();
       String date();
       int currentRevision() default 1;
       String lastModified() default "N/A";
       String lastModifiedBy() default "N/A";
       // Note use of array
       String[] reviewers();
    }
    
    // [...]
    
    @ClassPreamble (
       author = "John Doe",
       date = "3/17/2002",
       currentRevision = 6,
       lastModified = "4/12/2004",
       lastModifiedBy = "Jane Doe",
       // Note array notation
       reviewers = {"Alice", "Bob", "Cindy"}
    )
    public class Generation3List extends Generation2List {/* ... */}
    

    In this usage, Generation3List has one Annotation of type ClassPreamble. If the annotation is retained at runtime (i.e. ClassPreamble itself is annotated with @Retention(RUNTIME)), you can get to it via Generation3List.class.getAnnotations() or Generation3List.class.getAnnotation(ClassPreamble.class). (There are also declared counterparts that handle superclass annotations differently.)

    Once you get to an instance of ClassPreamble, you can use the methods like author() and date() to retrieve the data out of the class. However, ClassPreamble behaves as an interface, and the implementation of that annotation is internal to the VM. That makes it more difficult to create your own arbitrary instance of ClassPreamble at runtime.

    Conforming annotation implementations

    Because YourAnnotation and ClassPreamble are interfaces, you could just create an implementation. However, that implementation is unlikely to have matching implementations of equals and hashCode compared to the VM's implementation, because the implementation may vary between JREs and may also vary in Android. However, the implementation of equals and hashCode is actually very closely prescribed in the docs for Annotation:

    The hash code of an annotation is the sum of the hash codes of its members (including those with default values), as defined below: The hash code of an annotation member is (127 times the hash code of the member-name as computed by String.hashCode()) XOR the hash code of the member-value, as defined below [...]

    Returns true if the specified object represents an annotation that is logically equivalent to this one. In other words, returns true if the specified object is an instance of the same annotation type as this instance, all of whose members are equal to the corresponding member of this annotation, as defined below [...]

    It is possible to manually implement these rules, but it would be difficult to do so, and it would also impose a burden if the structure of YourAnnotation or ClassPreamble were to change. Though there are reflective solutions to this problem, AutoAnnotation generates the code for a conforming implementation automatically:

    public class YourAnnotations {
      @AutoAnnotation public static YourAnnotation yourAnnotation(String foo) {
        return new AutoAnnotation_YourAnnotations_yourAnnotation(foo);
      }
    }
    
    public class ClassPreambles {
      @AutoAnnotation public static ClassPreamble classPreamble(
          String author,
          String date,
          int currentRevision,
          String lastModified,
          String lastModifiedBy,
          String[] reviewers) {
        return new AutoAnnotation_ClassPreambles_classPreamble(
            author,
            date,
            currentRevision,
            lastModified,
            lastModifiedBy,
            reviewers);
      }
    }
    

    With AutoAnnotation's generated implementation, you can call get on the map that Dagger Multibindings generates (or provide test implementations you control) without having to deal with Annotation-specific hashCode XORs or equals rules. This is useful beyond Dagger and tests, but because Dagger uses annotation instances in its maps, it makes sense that you might need to use AutoAnnotation to create similar instances.