Android uses APT technology to generate code at compile time _android

Source: Internet
Author: User
Tags getmessage modifier

APT (Annotation processing Tool) can parse annotations at code compile time and generate new Java files, reducing manual code input. Now there are many mainstream libraries are used APT, such as Dagger2, Butterknife, EVENTBUS3, and so on, we have to keep up with the trend of the Times na! (ง ̀_ ́) ง

The following is a simple View injection project viewfinder to introduce APT related content, simple implementation is similar to butterknife two kinds of annotation @BindView and @OnClick.

Project Address: Https://github.com/brucezz/ViewFinder

The approximate project structure is as follows:

    1. Viewfinder-annotation-Annotation Related module
    2. Viewfinder-compiler-note Processor module
    3. VIEWFINDER-API related Modules
    4. Sample-Example Demo module

Achieve the goal

In a typical Android project, a large number of interfaces will be written, so you will often write code repeatedly, such as:

TextView Text = (TextView) Findviewbyid (r.id.tv);
Text.setonclicklistener (New View.onclicklistener () {
 @Override public
 void OnClick (view view) {
  //On Click
 }
});

Write such a long and no brain code every day, can still play happily. So, I'm going to replace this repetitive work by viewfinder this project, simply by labeling annotations. Annotations are made through the control ID, and @onclick can annotate the same method for multiple controls. Just like the following:

@BindView (r.id.tv) TextView Mtextview;
@OnClick ({r.id.tv, r.id.btn}) public
void Onsomethingclick () {
 //On Click
}

Define Annotations

Create the module viewfinder-annotation, type Java Library, to define the annotations required for the project.

Two annotation @BindView and @OnClick are required in viewfinder. Implemented as follows:

@Retention (Retentionpolicy.class)
@Target (elementtype.field) public
@interface BindView {
 int value () ;
}


@Retention (Retentionpolicy.class)
@Target (elementtype.method) public
@interface OnClick {
 int[] Value ();
}

@BindView need to annotate the member variable and receive an int type parameter, @OnClick need to annotate the method and receive a set of int type parameters, which is equivalent to a set of View-specific click-Response events.

Writing APIs

Create the module viewfinder, type the Android Library. Define the API in this module, which is to determine how to let others use our project.

First you need an API main entry that provides a static method to call directly, like this:

Viewfinder.inject (this);

At the same time, you need to provide overloaded injection methods for different goals, such as activity, Fragment, and View, and eventually call the inject () method. There are three parameters:

    1. Host represents the class in which the annotation View variable is located, which is the annotation class
    2. Source indicates where the view is found, activity & View itself can be found, Fragment need to find it in their own itemview
    3. Provider is an interface that defines how different objects (such as activity, view, and so on) can be found in the target view, and the provider interface is implemented in the project respectively for activity and view.
public class Viewfinder {

 private static final Activityprovider provider_activity = new Activityprovider ();
 private static final Viewprovider Provider_view = new Viewprovider ();

 public static void inject [activity activity] {
  inject (activity, activity, provider_activity);
 }
 public static void inject (view view) {
  //For View
  inject (view, view);
 }
 public static void inject (Object host, view view) {
  //For fragment
  inject (host, view, Provider_view);
 } Public
 static void inject (object host, Object source, Provider Provider) {
  //you to implement?
 }
}

So what do the inject () methods write?

First we need an interface Finder, then generate a corresponding inner class for each annotation class and implement the interface, and then implement the concrete injection logic. In the inject () method, the caller's corresponding Finder implementation class is first found, and then the specific logic inside is invoked to achieve the injection purpose.

The interface Finder is designed as follows:

Public interface Finder<t> {
 void inject (T host, Object source, Provider Provider);

For example, generating a mainactivity$ $Finder for mainactivity, initializing the View of its annotations, and setting the click events are basically the same as the repetitive code we write in common.

public class mainactivity$ $Finder implements finder<mainactivity> {
 @Override public
 void inject (final Mainactivity host, Object source, Provider Provider) {
  Host.mtextview = (TextView) provider.findview (source, 2131427414));
  Host.mbutton = (Button) (Provider.findview (source, 2131427413));
  Host.medittext = (edittext) (Provider.findview (source, 2131427412));
  View.onclicklistener Listener;
  Listener = new View.onclicklistener () {
   @Override public
   void OnClick (view view) {
    host.onbuttonclick (); c11/>}
  };
  Provider.findview (source, 2131427413). Setonclicklistener (listener);
  Listener = new View.onclicklistener () {
   @Override public
   void OnClick (view view) {
    host.ontextclick (); c18/>}
  };
  Provider.findview (source, 2131427414). Setonclicklistener (listener);
 }

All right, all annotation classes have an inner class named xx$ $Finder. We first get the class object of the corresponding inner class by the name of the annotation class, then instantiate the concrete object and invoke the injection method.

public class Viewfinder {

 //same as above

 private static final map<string, finder> finder_map = new HashMap <> ();

 public static void inject (object host, Object source, Provider Provider) {
  String className = Host.getclass (). GetName ( );
  try {
   Finder finder = finder_map.get (className);
   if (finder = = null) {
    class<?> finderclass = class.forname (ClassName + "$ $Finder");
    Finder = (Finder) finderclass.newinstance ();
    Finder_map.put (className, finder);
   Finder.inject (host, source, provider);
  catch (Exception e) {
   throw new RuntimeException ("Unable to inject for" + className, E);}}}


In addition, a bit of reflection is used in the code, so in order to improve efficiency, avoid every injection to find the Finder object, here with a Map of the first object to find the cache, and then use it directly from the map to take.

In this way, the API module design is basically done, and the next step is to generate a finder inner class for each annotation class through the annotation processor.

Create a note processor

Creates a module Viewfinder-compiler, which is a Java Library and implements a note processor.

This module needs to add some dependencies:

Compile project (': Viewfinder-annotation ')
compile ' com.squareup:javapoet:1.7.0 '
compile ' Com.google.auto.service:auto-service:1.0-rc2 '
    1. Because you need to use the annotations defined above, you certainly rely on viewfinder-annotation.
    2. Javapoet is a box company out of another good use to the explosion of trousers, provides a variety of APIs so that you use all kinds of posture to generate Java code files, avoid the embarrassment of freehand stitching strings.
    3. Auto-service is a Google pants, mainly used for annotation Processor, to generate Meta-inf configuration information.

Here's how to create our processor viewfinderprocessor.

@AutoService (processor.class) public class Viewfinderprocesser extends Abstractprocessor {/** * uses Google's auto-ser Vice Library can automatically generate meta-inf/services/javax.annotation.processing.processor files * * Private Filer Mfiler; File-related auxiliary class private Elements melementutils; Element-related auxiliary class private messager Mmessager;
  Log-related auxiliary classes @Override public synchronized void init (Processingenvironment processingenv) {super.init (processingenv);
  Mfiler = Processingenv.getfiler ();
  Melementutils = Processingenv.getelementutils ();
 Mmessager = Processingenv.getmessager (); /** * @return Specify which annotations should be registered with the annotation processor/@Override public set<string> getsupportedannotationtypes () {set<st
  ring> types = new linkedhashset<> ();
  Types.add (BindView.class.getCanonicalName ());
  Types.add (OnClick.class.getCanonicalName ());
 return types; /** * @return Specifies the Java version to use.
  Usually returns sourceversion.latestsupported (). * * @Override public sourceversion getsupportedsourceversion () {return sourceversIon.latestsupported (); @Override public Boolean process (set&lt. Extends typeelement> annotations, roundenvironment roundenv) {/to P
 Rocess annotations return false;

 }
}

Use @AutoService to annotate this processor, and you can automatically generate configuration information.

The init () can be initialized to get some useful tool classes.

Returns the collection of annotations to be processed in the Getsupportedannotationtypes () method.

Returns the Java version in the Getsupportedsourceversion () method.

These methods are basically fixed, plays are in the process () method.

Here's a little bit of element-related concepts that will be used later.

Element elements, each part of the source code is a specific element type, representing the package, class, method, and so on, specifically see the Demo.

Package com.example;

public class Foo {//typeelement

 private int A;//variableelement
 private Foo Other;//variableelement

 Publ IC Foo () {}//Executeableelement public

 void SetA (//executeableelement
   int Newa//typeelement
 ) {
 }
   }

These element elements, equivalent to the DOM tree in XML, can access its parent element or child element through an element.

Element.getenclosingelement ()//Get parent element
Element.getenclosedelements ()//Get child elements

The whole process of annotation processor is no different from ordinary Java program, we can use object-oriented thought and design pattern, encapsulate related logic into model, make process clearer and simpler. The annotation member variable, the click Method and the entire annotation class are encapsulated into different model respectively.

public class Bindviewfield {
 private variableelement mfieldelement;
 private int mresid;

 Public Bindviewfield (element element) throws IllegalArgumentException {
  if (Element.getkind ()!= Elementkind.field {
   throw new IllegalArgumentException (
    String.Format ("Only fields can is annotated with @%s") BindView.class.getSimpleName ());
  }

  Mfieldelement = (variableelement) element;
  BindView BindView = mfieldelement.getannotation (bindview.class);
  Mresid = Bindview.value ();
 }
 Some Getter methods
}

The main thing is to validate the element type when initializing, and then get the value of the annotation, and provide several getting methods. The Onclickmethod package is similar.

public class Annotatedclass {public typeelement mclasselement;
 Public list<bindviewfield> Mfields;
 Public list<onclickmethod> Mmethods;

 Public Elements melementutils; Omit some easy methods public javafile Generatefinder () {//Method inject (final T host, Object source, Provider Provider) Methodspec.builder Injectmethodbuilder = Methodspec.methodbuilder ("inject"). Addmodifiers (Modifier.PUBLIC ). Addannotation (Override.class). Addparameter (Typename.get (Mclasselement.astype ()), "host", modifier.final). Add

  Parameter (Typename.object, "source"). Addparameter (Typeutil.provider, "PROVIDER"); for (Bindviewfield field:mfields) {//Find views Injectmethodbuilder.addstatement ("host. $N = ($T) (Provider.findvi
  EW (source, $L)) ", Field.getfieldname (), Classname.get (Field.getfieldtype ()), Field.getresid ()); 
  } if (Mmethods.size () > 0) {injectmethodbuilder.addstatement ("$T listener", Typeutil.android_on_click_listener); for (ONclickmethod method:mmethods) {//DECLARE Onclicklistener anonymous class TypeSpec listener = TYPESPEC.ANONYMOUSC Lassbuilder (""). Addsuperinterface (Typeutil.android_on_click_listener). Addmethod (Methodspec.methodbuilder (" OnClick "). Addannotation (Override.class). Addmodifiers (Modifier.public). Returns (Typename.void). ADDPA Rameter (Typeutil.android_view, "VIEW"). Addstatement ("host. $N ()", Method.getmethodname ()). Build (). Build ()
   ;
   Injectmethodbuilder.addstatement ("Listener = $L", listener); for (int id:method.ids) {//Set listeners Injectmethodbuilder.addstatement ("Provider.findview" (source, $L). setOn
   Clicklistener (Listener) ", id); }//Generate whole class TypeSpec Finderclass = Typespec.classbuilder (mclasselement.getsimplename () + "$ $Finder" ). Addmodifiers (Modifier.public). Addsuperinterface (Parameterizedtypename.get (Typeutil.finder, TypeName.get ( Mclasselement.astype ())). Addmethod (InjectmethodbuilDer.build ()). build ();
  String PackageName = melementutils.getpackageof (mclasselement). Getqualifiedname (). toString ();
 Generate File return Javafile.builder (PackageName, Finderclass). Build ();

 }
}

Annotatedclass represents an annotation class with two lists containing the member variables and methods of the annotation. In the Generatefinder () method, follow the template designed in the previous section to generate code using the Javapoet API. This part does not have any special posture, according to the Javapoet document to be good, the document writes very carefully.

There are many places where you need to use the type of object, the normal type can be

ClassName Get (String PackageName, String simplename, String ... simplenames)

Incoming package name, class name, internal class name, you can get the type you want (you can refer to the Typeutil class in the project).

If you use a generic type, you can use the
Parameterizedtypename Get (ClassName rawtype, TypeName ... typearguments)

It's good to pass in the specific class and the generic type.

After these model is OK, the process () method is very refreshing. Use the Roundenvironment parameter to query the element that is annotated by a particular annotation, then parse into the specific model, and finally generate the code output to the file.

@AutoService (processor.class) public class Viewfinderprocesser extends Abstractprocessor {private map<string, Annot

 atedclass> Mannotatedclassmap = new hashmap<> (); @Override public Boolean process (SET&LT; extends typeelement> annotations, roundenvironment roundenv) {//process (

  Would be called several times mannotatedclassmap.clear ();
   try {processbindview (roundenv);
  Processonclick (ROUNDENV);
   catch (IllegalArgumentException e) {error (E.getmessage ()); return true; Stop process} for (Annotatedclass annotatedClass:mAnnotatedClassMap.values ()) {try {info ("Generating f
    Ile for%s ", Annotatedclass.getfullclassname ());
   Annotatedclass.generatefinder (). WriteTo (Mfiler);
    catch (IOException e) {error ("Generate file failed, Reason:%s", E.getmessage ());
   return true;
 } return true; } private void Processbindview (Roundenvironment roundenv) throws IllegalArgumentException {for (Element Element:rou NdenV.getelementsannotatedwith (Bindview.class)) {Annotatedclass Annotatedclass = Getannotatedclass (Element);
   Bindviewfield field = new Bindviewfield (element);
  Annotatedclass.addfield (field);  } private void Processonclick (Roundenvironment roundenv) {//Same as Processbindview ()} private Annotatedclass
  Getannotatedclass (element Element) {Typeelement classelement = (typeelement) element.getenclosingelement ();
  String FullClassName = Classelement.getqualifiedname (). toString ();
  Annotatedclass Annotatedclass = Mannotatedclassmap.get (FullClassName);
   if (Annotatedclass = = null) {Annotatedclass = new Annotatedclass (classelement, melementutils);
  Mannotatedclassmap.put (FullClassName, Annotatedclass);
 return annotatedclass;

 }
}

The annotation element is parsed first and placed in the corresponding annotation class object, and the method generation file is finally called. Model's code will also add some validation code to determine whether the annotation element is reasonable, the data is normal, and then throw an exception, the processor can print out after receiving the error prompts, and then directly return true to end processing.

At this point, the annotation processor is also basically completed, detailed reference to the project code.

Actual project use

Create module sample, an ordinary Android module, to demonstrate the use of viewfinder.

Add in Build.gradle under the entire project

Classpath ' com.neenbedankt.gradle.plugins:android-apt:1.8 '

Then add in the Build.gradle under sample module

Apply plugin: ' Com.neenbedankt.android-apt '

Add dependencies at the same time:

Compile project (': Viewfinder-annotation ')
Compile project (': Viewfinder ')
Apt project (': Viewfinder-compiler ')

Then casually create a layout, casually add a few controls, you can experience annotations.

public class Mainactivity extends Appcompatactivity {
 @BindView (r.id.tv) TextView Mtextview;
 @BindView (R.ID.BTN) Button Mbutton;
 @BindView (r.id.et) edittext medittext;

 @OnClick (r.id.btn) public
 void OnButtonClick () {
  Toast.maketext (this, OnButtonClick, Toast.length_short). Show ();
 }

 @OnClick (r.id.tv) public
 void Ontextclick () {
  Toast.maketext (this, ' Ontextclick ', toast.length_short). Show ();
 }

 @Override
 protected void onCreate (Bundle savedinstancestate) {
  super.oncreate (savedinstancestate);
  Setcontentview (r.layout.activity_main);
  Viewfinder.inject (this);
 }


This time build project, you can see the generated mainactivity$ $Finder class, and then run the project ran. Every time you change the annotation, build the project.

All done ~

This project is also a toy-grade apt project to learn how to write apt projects. The sense APT project is more about how to design the architecture, how to call between classes, what code to generate, and what API to call. Finally, the annotation processor is used to parse the annotation, and then the Javapoet is used to generate the specific code.

Ideas are more important than implementations, and design is more ingenious than code.

Reference

    1. Annotation-processing-tool detailed explanation (strongly recommended)
    2. How Android writes a project based on compile-time annotations
    3. Javapoet Document
    4. Butterknife (good code structure design)

Through this article, I hope to help you develop the Android application, thank you for your support for this site!

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.