Componentized Architecture Design: gradle Componentized Configuration, Introduction and Use of APT+javapoet

Blog Home Page

Reference resources:

gradle componentized configuration

The configuration of Android component development is inseparable from the gradle build tool, which makes the project possible.The core of gradle is groovy scripting language. Groovy scripting is Java-based and extends java, so gradle needs to rely on JDK and Groovy libraries.

A brief introduction to gradle

Gradle syntax: Explain the journey of component design starting with the log output of gradle

// The first way to print strings
println("hello, gradle!")

// The second way to print strings
println "hello2, gradle!"

From these two types of string output, it can be seen that the method does not write parentheses or semicolons after a sentence, which is a groovy feature.

You can use the println function to output logs in the build.gradle file in your Android project.Then view the log of build output from Build->Toggle view

Custom Properties

Gradle can add additional custom attributes, implemented through the ext attribute.Start by creating a new config.gradle file and customizing the isRelease property for dynamic switching: Componentized/Integrated

ext {
    // false: Componentized mode (sub-modules can run independently)
    // true: integrated mode (packages the entire project apk, sub-modules cannot run independently)
    isRelease = true
}

So how do I use this config file?The config.gradle file needs to be referenced in the project's root build.gradle file by apply front

// build.gradle

// Can be referenced by apply from
apply from: 'config.gradle'

Then use custom attributes in the build.gradle file of the app application project

// build.gradle

// With custom attributes, attributes need to be written in ${}
println "${rootProject.ext.isRelease}"

Basic configuration of config.gradle file

Create a new shop Library module with the following project structure:

Looking at the build.gradle file in the app Application module and the shop library module, we find that the configuration of the red box above is similar. Can we configure it in a modular way, using the gradle custom attribute to extract the common configuration and put it in a separate file for other builds to reference?The answer is yes.

Next, the configuration of the red box is related to the Android version. You can define a Map collection to store the related attribute information, and configure it as follows:

// config.gradle

ext {
    // Define Map access version-related information, key name can be arbitrary
    versions = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.2",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]
}

Then access the build.gradle file in the app Application module and the shop library module to access the properties defined in the Map collection, such as the custom properties in the Map in the app Application module build.gradle

// Get Map
def versions = rootProject.ext.versions

android {
    // Access values directly from map.key
    compileSdkVersion versions.compileSdkVersion
    buildToolsVersion versions.buildToolsVersion
    defaultConfig {
        applicationId "com.example.modular.todo"
        minSdkVersion versions.minSdkVersion
        targetSdkVersion versions.targetSdkVersion
        versionCode versions.versionCode
        versionName versions.versionName
    }
}

1. applicationId Configuration
Switching from a component module to an integrated module requires setting the applicationId in order for the component module to run independently

So you also need to configure different applicationId properties

// config.gradle

ext {
    // Set different applicationId s when switching between componentization and integration
    appId = [
            app : "com.example.modular.todo",
            shop: "com.example.modular.shop"
    ]
}

Use in build.gradle file of app Application module

def appId = rootProject.ext.appId
android {
    defaultConfig {
        applicationId appId.app
    }
}

2. Switching between production and formal environment configuration in code
Sometimes you need to switch between production and formal environment configurations in your code, such as the URL of a network request.Android provides us with custom BuildConfig functionality.

// config.gradle

ext {
    baseUrl = [
            debug  : "https://127.0.0.1/debug ", //beta URL
            release: "https://127.0.0.1/relase "//Official Version URL"
    ]
}

Configuration via buildConfigField in the app Application module build.gradle file is accessible in code through BuildConfig.baseUrl.

// build.gradle

def baseUrl = rootProject.ext.baseUrl
android {
    buildTypes {
        debug {
            // void buildConfigField(
            //            @NonNull String type,
            //            @NonNull String name,
            //            @NonNull String value) { }
            buildConfigField("String", "baseUrl", "\"${baseUrl.debug}\"")
        }

        release {
            buildConfigField("String", "baseUrl", "\"${baseUrl.release}\"")
        }
    }
}

3. dependencies Dependency Configuration

// config.gradle

ext {
    appcompatVersion = "1.0.2"
    constraintlayoutVersion = "1.1.3"
    dependencies = [
            appcompat       : "androidx.appcompat:appcompat:${appcompatVersion}",
            constraintlayout: "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
    ]

    tests = [
            "junit"        : "junit:junit:4.12",
            "espresso"     : "androidx.test.espresso:espresso-core:3.1.1",
            "androidJunit": "androidx.test.ext:junit:1.1.0"
    ]
}

build.gradle file reference in app module

// build.gradle

def supports = rootProject.ext.dependencies
def tests = rootProject.ext.tests

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // standard notation
    // implementation group: 'androidx.appcompat', name: 'appcompat', version: '1.0.2'
//    implementation supports.appcompat
//    implementation supports.constraintlayout

    // Support dependency
    supports.each { key, value -> implementation value }

    testImplementation tests.junit
    androidTestImplementation tests.espresso
    androidTestImplementation tests.androidJunit
}

4. Signature Configuration
Note when configuring signatures: signingConfigs must be written before buildTypes

android {
    // Signature Configuration (Invisible pit: must be written before buildTypes)
    signingConfigs {
        debug {
            // Tiankeng: Wrong fill, compilation failed and no problem found
            storeFile file('/Users/xujinbing839/.android/debug.keystore')
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
        release {
            // Signature Certificate File
            storeFile file('/Users/xujinbing839/work/mycode/todo/todo_modular/keystore/modular')            
            storeType "modular" // Type of signing certificate            
            storePassword "123456" // Password for signing certificate file        
            keyAlias "modular" // Key Alias in Signing Certificate  
            keyPassword "123456" // The password of the key in the signing certificate
            v2SigningEnabled true // Whether to turn on V2 packaging
        }
    }

    buildTypes {
        debug {
            // Set signature information on build type
            signingConfig signingConfigs.debug
        }

        release {
            // Set signature information on build type
            signingConfig signingConfigs.release
        }
    }
}

Other Configurations

android {
    defaultConfig {
        // Open Subcontracting
        multiDexEnabled true
        // Set Subpackage Configuration
        // multiDexKeepFile file('multidex-config.txt')

        // Generate a png picture of a specified dimension from an svg picture
        // vectorDrawables.generatedDensities('xhdpi','xxhdpi')
        // Use support-v7 compatibility (version 5.0 and above)
        vectorDrawables.useSupportLibrary = true
        // Keep only specified and default resources
        resConfigs('zh-rCN')

        // Configure the so library CPU architecture (real machine: arm, simulator: x86)
        // x86  x86_64  mips  mips64
        ndk {
            //abiFilters('armeabi', 'armeabi-v7a')
            // For simulator startup
            abiFilters('x86', 'x86_64')
        }
    }

    // AdbOptions can add configurations to adb operation options
    adbOptions {
        // Configure operation timeout in milliseconds
        timeOutInMs = 5 * 1000_0

        // Option Configuration for the adb install command
        installOptions '-r', '-s'
    }
    // Configuration for dx operations, accepts a closure of type DexOptions, provided by DexOptions
    dexOptions {
        // Configure the maximum heap memory allocated for dx command execution
        javaMaxHeapSize "4g"
        // Configuring whether to pre-execute the dex Libraries project will increase the incremental build speed after opening, but will affect the clean build speed, default true
        preDexLibraries = false
        // Configure whether to turn on jumbo mode by forcing it on for more than 65535 to build successfully
        jumboMode true
        // Configure the number of threads Gradle uses to run the dx command
        threadCount 8
        // Configure multidex parameters
        additionalParameters = [
                '--multi-dex', // Multi-dex Subcontracting
                '--set-max-idx-number=50000', // Maximum number of methods per package
                // '--main-dex-list='+'/multidex-config.txt', //packaged into the file list of the main classes.dex
                '--minimal-main-dex'
        ]
    }
    // Run the lint check by executing the gradle lint command, and the default generated report is in outputs/lint-results.html
    lintOptions {
        // Build terminates when a link check error occurs, typically set to false
        abortOnError false
        // Treat warnings as errors (older version: warningAsErros)
        warningsAsErrors false
        // Check for new API s
        check 'NewApi'
    }
}

Componentized Detailed Deployment

Significance of Component Development

What is component development?
Component development is the process of dividing an app into modules, each of which is a component. During the development process, we can make these components depend on each other or debug some components individually, but ultimately when it is published, these components are merged into one apk, which is component development.

Componentized and plug-in development differs slightly:
During plug-in development, the entire app is divided into many modules, including a host and many plug-ins, each of which is an apk (each component module is a lib), and the host APK and the plug-in APK are packaged separately when they are finally packaged.

Why componentize?
1. Development needs
Non-interdependent, interactive, arbitrary combination, highly decoupled
2. Team effectiveness

Differentiation, switching between library and application

Phone Module is different from Android Library:

Phone Module is a stand-alone, compiled into an apk, and the build.gradle file needs to be configured with applicationId; Android Library cannot run independently, it cannot be compiled into a single apk, and the build.gradle file does not need to be configured with applicationId.

Phone Module and Android Library switch:

Here is an example of a sub-module shopping shop:

If the Shopping shop module can compile the apk independently, it needs to switch to Phone Module (i.e. component mode) and switch dynamically through isRelease:

  1. When isRelease is true: integrated mode (that is, the entire project apk can be packaged), sub-module shopping shops cannot run independently
  2. When isRelease is false: Componentized mode, sub-module shop can run independently

Where the isRelease variable is a property defined in config.gradle

// build.gradle

if (isRelease) { // If it is a released version, each module cannot run independently
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

android {
    defaultConfig {
        // If it is an integrated mode, there cannot be an applicationId
        if (!isRelease) applicationId appId.shop
    }
}

When a sub-module shopping shop switches from a library module to an application module, you may need to write test code, such as a startup entry.

Using the sourceSets configuration, put the test code in the debug folder, and when switching to integrated mode, remove all debug code when packaging into an apk (that is, debug code is not packaged into an apk).

android {
    // Configure resource paths, facilitate test environments, package not integrated into formal environments
    sourceSets {
        main {
            if (!isRelease) {
                // If it is a componentized mode, it needs to be run separately
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // Integrated mode, whole project packaging apk
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    // Files in the debug directory do not need to be merged into the main project when releasing
                    exclude '**/debug/**'
                }
            }
        }
    }
}

Componentized Development Specification:

  1. Submodule Shopping Shop: Add a prefix to the src class and res resource commands, shop_, such as shop_activity_home
  2. app module: can not be modified, default

Interaction between Module and Module

How do modules interact with each other (including jumps, parameterizations, etc)?There are many ways:

  1. EventBus is very messy and difficult to maintain
  2. Reflection reflection technology can be successful, but it is expensive to maintain and has a high version @hide limitation
  3. Implicit intent maintenance costs are good, it's cumbersome, and Manifest's action needs to be maintained
  4. BroadCastReceiver needs to be registered dynamically (after 7.0) and the requester sends the broadcast
  5. Class loading requires accurate full class name paths, which are costly to maintain and prone to human error

First implementation: class loading technology interaction

Loading a target class through the forName of the Class requires an accurate knowledge of the full class name path of the target class.

private void jump() {
    try {
        Class<?> targetClass = Class.forName("com.example.modular.shop.ShopActivity");

        Intent intent = new Intent(this, targetClass);
        intent.putExtra("moduleName","app");
        startActivity(intent);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

Second implementation: Global Map records path information

To jump, if you know the Class object of the target class, you can not jump. Next, you just need to solve the Class object lookup of the target class.

You can define a PathBean to encapsulate information about the target class, such as the full path name of the target class, the Class object of the target class

public class PathBean {

    public String path; // Jump Target Full Class Name
    public Class<?> targetClass; // Jump Class Object of Target Class

    public PathBean(String path, Class<?> targetClass) {
        this.path = path;
        this.targetClass = targetClass;
    }
}

There are many PathBean s in a module that can be accessed from a List, and many modules that can be used to differentiate between different modules using Map.

 // key: Module name, such as shop module value: All Activity path information under the module
 private static final Map<String, List<PathBean>> mGroupMap = new HashMap<>();

mGroupMap is a Map, and key is the module name, such as: shop module; value is all Activity path information under that module.

All path information is added to the mGroupMap and the target class object is obtained from the group name (module name) and path name when used.

/**
 * Add path information to the global Map
 */
public static void addGroup(String groupName, String path, Class<?> targetClass) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans == null) {
        pathBeans = new ArrayList<>();
        pathBeans.add(new PathBean(path, targetClass));
        mGroupMap.put(groupName, pathBeans);
    } else {
        pathBeans.add(new PathBean(path, targetClass));
    }
}

/**
 * Get the target class based on the group name and path name
 */
public static Class<?> findTargetClass(String groupName,String path) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans != null) {
        for (PathBean pathBean : pathBeans) {
            if (!TextUtils.isEmpty(path) && path.equalsIgnoreCase(pathBean.path)) {
                return pathBean.targetClass;
            }
        }
    }
    return null;
}

Introduction and Use of APT

Introduction to APT

APT (Annotation Processing Tools) is a tool for handling annotations. It detects source code files to find Annotaions in them and uses Annotation for additional processing.Annotation processors can generate additional source files and other files based on Annotaion in the source files (the specific contents of the files are determined by the author of the Annotaion processor). APT also compiles the generated source files and the original source files to generate the class files together.

Popular understanding: Help us generate code and class files according to the rules.

1. APT Core Implementation Principles

The basic principle of compile-time Annotation parsing is that annotations are added to certain code elements, such as types, functions, fields, and so on. At compile-time, the javac compiler examines the subclasses of AbstractProcessor, calls the process function of that type, and then passes all the annotated elements into the process function so that the developer can go where the compiler is concernedFor example, generating new java classes from annotations is the basic principle of open source libraries such as ARouter, Butterknife, Dragger, and so on.

2. Javaa Source File Programming Layer Class File

The tool is a javac tool, and the Annotation Processor is an annotation tool in javac that is used for compile-time scanning and processing.You can think of it as a specific annotation and register your own annotation processor.

3. How to register Annotation Processors
MyProcessor to javac.You must provide a.Jar file.Like other.Jar files, you pack your annotation processor into this file.Also, in your jar, you need to package a specific file, javax.annotation.processing.Processor, to the META-INF/services path.

Knowledge Point Details

1. jar

  • com.google.auto.service:auto-service Google provided java generated source code library
  • com.squareup:javapoet provides various API s to generate java code files in various postures

2. @AutoService

This is an annotation introduced in other annotation processors.The AutoService Annotation Processor was developed by Google to generate META-INF/services/javax.annotation.processing.Processor files.We can use annotations in annotation processors.Very convenient

3. Structural Language

It is also a structured language for java source files.An interface for describing structural language elements is provided in the JDK.

During annotation processing, we scan all java source files.Each part of the source code is an Element of a specific type.In other words: Element represents an element of a program, such as a package, class, or method.Each element represents a static, language-level component.

package com.example; // PackageElement

public class Foo { // TypeElement
    
   private int a; // VariableElement
   private Foo other; // VariableElement

   public Foo {} // ExecutableElement

   public void setA( // ExecutableElement
      int newA // VariableElement ) {}
}

Element Program Elements

PackageElement represents a package element.

TypeElement represents a class or interface program element.

ExecutableElement represents a method, constructor, or initializer (static or instance) of a class or interface, including a comment type element.

VariableElement represents a field, enumeration constant, method or constructor parameter, local variable, resource variable, or exception parameter.

TypeParameterElement represents the formal type parameter of a generic class, interface, method, or constructor element.

Types
A tool class for working with TypeMirror

Filer
With Filer you can create java files

AbstractProcessor Core API

1. process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

Equivalent to the main function () of each processor.Here you can write your code to scan, evaluate, and process annotations, as well as generate java files.Enter the parameter RoundEnvironment, which allows you to query for annotated elements that contain specific annotations.

2. getSupportedAnnotationTypes()

To which annotation is this annotation processor registered.Note that its return value is a collection of strings containing the legal full name of the type of annotation the processor wants to process.In other words, here you define which annotations your annotation processor registers on.

3. getSupportedSourceVersion()

Used to specify the version of java you are using.Usually SourceVersion.latestSupported() is returned here.You can also return to SourceVersion.RELEASE_6 if you have good reasons to support java 6 only

4. getSupportedOptions()

Option parameters used to specify annotation processor processing.Option parameter values need to be configured in the gradle file

// Configure option parameter values in the gradle file (for APT parameter reception)
// Remember: must be written under defaultConfig node
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}

These API s can also be specified in annotations:

// AutoService is a fixed way of writing, just add a comment
// AutoService Annotation Processor can be automatically generated from @AutoService in auto-service for registration
// Used to generate META-INF/services/javax.annotation.processing.Processor file
@AutoService(Processor.class)
// Allow/support annotation types for annotation processor processing (new annotation module)
@SupportedAnnotationTypes({"com.example.modular.annotations.ARouter"})
// Specify JDK Compiled Version
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// Annotate parameters received by the processor
@SupportedOptions("moduleName")
public class ARouterProcessor extends AbstractProcessor {
  // ignore
}

Processing Environment Core API

// ignore
public class ARouterProcessor extends AbstractProcessor {
   // Operating Element Tool Class (classes, functions, attributes are Elements)
    private Elements elementUtils;

    // Type (class information) tool class, containing tool methods for manipulating TypeMirror
    private Types typeUtils;

    // Messager is used to report errors, warnings, and other prompts
    private Messager messager;

    // File Generator Class/Resource, Filter used to create new source, class, and auxiliary files
    private Filer filer;

    // Module name, passed in from getOptions to get build.gradle
    private String moduleName;

    // This method is mainly used for some initialization operations, and the Processing Environment parameter of this method gives you a few useful tool classes
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // ProceingEnv is a parent protected property that can be used directly.
        // This is actually the parameter ProcessingEnvironment of the init method
        // processingEnv.getMessager(); //reference source 64 lines
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        typeUtils = processingEnvironment.getTypeUtils();

       // Get the parameters passed in by build.gradle through Processing Environment
        Map<String, String> options = processingEnvironment.getOptions();
        if (options != null && !options.isEmpty()) {
            moduleName = options.get("moduleName");
            // Pit: Diagnostic.Kind.ERROR, exception ends automatically, not as good as Log.e in Android
            messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName);
        }
    }
}

RoundEnvironment Core API

// ignore
public class ARouterProcessor extends AbstractProcessor {
      /**
     * Equivalent to main function, start processing annotations
     * Annotate the core approach of the processor, handle specific annotations, and generate Java files
     *
     * @param set              A set of nodes supporting processing annotations is used (annotations are written above the class)
     * @param roundEnvironment The current or previous running environment through which you can find annotations.
     * @return true Indicates that subsequent processors will no longer process (process completed)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // Get all class nodes with ARouter annotations
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // Traverse all class nodes
        for (Element element : elements) {
            // ignore
        }
        // ignore
        return true;
    }
}

Element Core API

APT usage

Compatibility of development environment

https://github.com/google/auto

1. Android Studio 3.2.1 + Gradle 4.10.1 Critical Version

dependencies {
     // Register notes and generate configuration information for META-INF, rc2 has pits after gradle5.0
     // As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
     implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

2. Android Studio 3.4.1 + Gradle 5.1.1 is downward compatible

dependencies {
    // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
}

Use APT technology to help us generate code

1. ARouter annotations

Create a new java library project named annotations.Then create the ARouter annotation

/**
 * <ul>
 * <li>@Target(ElementType.TYPE)   // Interfaces, Classes, Enumerations, Comments </li>
 * <li>@Target(ElementType.FIELD) // Constants of attributes, enumerations </li>
 * <li>@Target(ElementType.METHOD) // Method </li>
 * <li>@Target(ElementType.PARAMETER) // Method parameter </li>
 * <li>@Target(ElementType.CONSTRUCTOR)  // Constructor </li>
 * <li>@Target(ElementType.LOCAL_VARIABLE)// Local variable </li>
 * <li>@Target(ElementType.ANNOTATION_TYPE)// The note is used on another note </li>
 * <li>@Target(ElementType.PACKAGE) // Package </li>
 * <li>@Retention(RetentionPolicy.RUNTIME) <br>Annotations will exist in the class byte code file and can be retrieved by reflection when the jvm is loaded </li>
 * </ul>
 *
 * Life cycle: SOURCE < CLASS < RUNTIME
 * 1,In general, if you need to get annotation information dynamically at runtime, annotate with RUNTIME
 * 2,To do some preprocessing at compile time, such as ButterKnife, annotate with CLASS.Annotations will exist in the class file but will be discarded at runtime
 * 3,Do some checking, such as @Override, annotate with the SOURCE source code.Comment only exists at source level and is discarded at compile time
 */
@Target(ElementType.TYPE) // This annotation works on the class
@Retention(RetentionPolicy.CLASS) // Some preprocessing is done at compile time.Annotations will exist in the class file
public @interface ARouter {

    // Detailed routing path (required), such as "app/MainActivity"
    String path();

    // Routing group name (optional, intercepted from path if not filled in)
    String group() default "";
}

2. Custom ARouterProcessor Annotation Processor
Create a new java library project named compiler.Create an ARouterProcessor class that inherits AbstractProcessor.

In order to generate code using APT technology, first you need to design the code template that we want to generate. The code below is generated by APT.

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        if (path.equals("/shop/ShopActivity")) {
            return ShopActivity.class;
        }
        return null;
    }
}

Implement the process method in the ARouterProcessor class, process the supported annotations, and generate the code we want.

/**
 *
 * Equivalent to main function, start processing annotations
 *  Annotate the core approach of the processor, handle specific annotations, and generate Java files
 *
 * @param set              A collection that supports annotation types, such as: @ARouter annotations
 * @param roundEnvironment Current or previous runtime environment where you can find annotations
 * @return true Indicates that subsequent processors will no longer process (process completed)
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // Set collection is the set of supported annotations, such as ARouter annotations
    if (set.isEmpty()) return false;

    // Get all elements commented by the @ARouter comment
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // Get the package node from the class node (full path name, such as com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // Get a simple class name annotated by @ARouter
            String simpleName = element.getSimpleName().toString();

            // Note: Package name: The annotated class name of com.example.modular.shop: ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "Package name:" + packageName + " Annotated class name:" + simpleName);

            // Final generated class file name
            String finalClassName = "ARouter$$" + simpleName;
            try {
                // Create a new source file and return a JavaFileObject object to allow writing to it
                JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);

                // Get the Writer for this file object, turn on writing
                Writer writer = sourceFile.openWriter();

                writer.write("package " + packageName + ";\n");
                writer.write("public class " + finalClassName + " {\n");
                writer.write("public static Class<?> findTargetClass(String path) {\n");

                // Get ARouter annotations
                ARouter aRouter = element.getAnnotation(ARouter.class);

                writer.write("if (path.equals(\"" + aRouter.path() + "\")) {\n");

                writer.write("return " + simpleName + ".class;\n");

                writer.write("}\n");

                writer.write("return null;\n");

                writer.write("}\n}");

                // Close Write Stream
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

Add dependencies to the build.gradle file when using, such as in the Shopping shop module

dependencies {
    implementation project(path: ':ARouter:annotations')
    annotationProcessor project(path: ':ARouter:compiler')
}

Then add the ARouter annotation on the ShopActivity class

@ARouter(path = "/shop/ShopActivity")
public class ShopActivity extends AppCompatActivity {}

Finally build -> Make Project, the code we want will be generated.

Note: If Chinese scrambling occurs, add the following configuration to build.gradle:

// java console output Chinese scrambling
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

APT + javapoet

javapoet is an open source java code generation framework introduced by square, which provides java api generation.java source files.This framework is very practical and is also our customary java object-oriented OOP syntax.It can be easily used to generate code for annotations.This way of automating code generation allows us to replace tedious, repetitive work in a simpler and elegant way.

Project Home Page and Source Code
https://github.com/square/jav...

Dependent on javapoet Libraries

dependencies {
    // Help us generate java code through class calls
    implementation 'com.squareup:javapoet:1.11.1'
}

javapoet 8 Common Classes

Let's start with a simple example from the javapoet website, which generates the HelloWorld class

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

Generate the above code using javapoet:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

Eight commonly used classes are provided in javapoet jar

javapoet string formatting rules

A literal quantity of $L, such as "int value=$L", 10
 The $S string, such as: $S,'hello'
$T class, interface, such as: $T, MainActivity
 The $N variable, such as user.$N, name

Next, generate the ARouter$$ShopActivity class

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        return path.equals("/shop/ShopActivity") ? ShopActivity.class : null;
    }
}

Generate ARouter$$ShopActivity class using javapoet

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // Get all elements commented by the @ARouter comment
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // Get the package node from the class node (full path name, such as com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // Get a simple class name annotated by @ARouter
            String simpleName = element.getSimpleName().toString();

            // Note: Package name: The annotated class name of com.example.modular.shop: ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "Package name:" + packageName + " Annotated class name:" + simpleName);

            // Final generated class file name
            String finalClassName = "ARouter$$" + simpleName;

            ARouter aRouter = element.getAnnotation(ARouter.class);

            ClassName targetClassName = ClassName.get((TypeElement) element);
            // Build Method Body
            MethodSpec findTargetClass = MethodSpec.methodBuilder("findTargetClass")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(Class.class) // Return value Class<?>
                    .addParameter(String.class, "path")
                    .addStatement("return path.equals($S) ? $T.class : null", aRouter.path(), targetClassName)
                    .build();

            // Build Class
            TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(findTargetClass)
                    .build();

            // Generate Files
            JavaFile javaFile = JavaFile.builder(packageName, finalClass)
                    .build();

            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

If my article is helpful to you, you might as well give me a pat on the back (^^)

Tags: Android Gradle Java Google

Posted on Tue, 03 Dec 2019 16:08:13 -0500 by hnxuying