The most involved in component-based development is the jump of activities between different modules. The jump can be implemented in various ways:

  • Implicit jump
  • DeepLink
  • reflection

Although there are many ways, but there are some usage or performance problems. For example, an implicit jump requires an action or category for each Activity in the open file, DeepLink requires a scheme and host for each Activity in the open file, and reflection requires a full path for each Activity to fetch the Class file. Frequent reflection can also cause performance problems.

There are two types of hops: internal hops and third-party hops.

A jump within this application

The easiest way to jump in an app is to grab the Activity’s Class object and then jump to it based on the Intent. Since run-time reflection has better performance, compile-time annotations can solve this problem.

The main principles behind compile-time annotations are: By adding custom annotations to an Activity, the AnnotationProcessor takes those annotated Activity Class objects at compile time, automatically generates intermediate Java classes, generates static methods, and generates code in those methods, What this code does is add all the activities into a collection. However, you only generate the code and do not add the Activity to the collection, because the addition takes place at runtime, so you only need to call the generated static method when the program is initialized.

There are mainly the following steps:

1. Create a Java Library-RouteAnnotation

The library’s sole purpose is to declare annotations. There is only one annotation class, which looks like this:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    String value() default "";

}
Copy the code

2. Create Java Library-Routecompiler

The library’s sole purpose is to take all the activities decorated with the Route annotation and generate the Java classes that store the activities into the collection.

To generate Java classes automatically at compile time, we need to define a class that inherits from the system’s AbstractProcessor, and then use two dependencies provided by Google to help simplify the AnnotationProcessor setup. The Javapoet library is needed to generate Java files. And we get a class that has a Route annotation, so we need to rely on our own routeAnnotation. So the Gradle dependencies for this library are as follows:

dependencies { implementation project(path: ': routeAnnotation') annotationProcessor com. Google. Auto. Services: auto - service: 1.0 rc7 'compileOnly 'com. Google. Auto. Services: auto - service - annotations: 1.0 rc7' implementation 'com. Squareup: javapoet: 1.8.0 comes with'}Copy the code

3. Generate Java files in AbstractProcessor

If you’re just generating a Java file in the Processor, it’s a very easy thing to do with Javapoet. But generating a Java class that holds all activities into a collection is not so simple, mainly because the collection is not in the routeCompiler library, but in the routeApi library.

Take a look at the result of the final generated file:

RouteInit class

This class is used to call the add method in the RouteSaving_moduleName. Java class automatically generated for each Module. The final result is as follows, for example, the app main module in the project and two other modules of business logic: Module1 and Module2. Then one RouteSaving file is generated on each Module and the Add method is called on each file.

public final class RouterInit { public static final void init() { RouterSaving_app.add(); RouterSaving_module1.add(); RouterSaving_module2.add(); }}Copy the code

RouterSaving_routeName class

This class is used to generate code that stores each Activity in the Module. For example, if there is only one Activity in module1 that uses the Route annotation, the RouterSaving class will generate code that stores that Activity into the collection.

public final class RouterSaving_module1 {
  public static final void add() {
    com.example.routeapi.Routers.addRoute("test://module1?key1=123&key2=456", Module1Activity.class);
  }
}
Copy the code

So the RouteInit and RouterSaving classes work together like this:

Each Module has a RouterSaving class, and each RouterSaving class collects all activities from the module and generates code to add to the collection.

The RouterInit class calls the addcode of all the RouterSaving classes so that all route-annotated activities from all Modules in the project are stored in the same collection.

All of the above code is generated at compile time; it just generates the code, without actually adding Activity information to the collection. To do this, call RouterInit.init() in the Application onCreate. In this way, the collection stores all of the Activity information, which can be used by simply iterating through matches and then jumping with the Intent.

The general principle is this, but there are still a few small points:

  • RouterInit and RouterSaving_moduleName are automatically generated classes, and they are generated whenever the Module needs them. However, we need to ensure that RouterInit is generated only in the main module of the project, i.e. app, and not in other modules.
  • The generated RouterSaving_moduleName suffix is the name of the module. How do I get the module name

AbstractProcessor is an example of how to get the main module and the name of each module in AbstractProcessor.

In AbstractProcessor, you can override a method and add the desired return value

@Override
public Set<String> getSupportedOptions() {
    Set<String> set = new HashSet<>();
    set.add(MODULE_NAME_KEY);
    set.add(MODULE_MAIN_KEY);
    set.add(MODULES_LIST);
    return set;
}
Copy the code

So what exactly are the return values? Gradle is a set of keys in key-value pairs. These key-value pairs need to be declared in gradle under their respective Modules, as in app:

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [module_name: project.getName(), module_main: "yes", modules_list: "app&module1&module2"]
            }
        }
    }
}
Copy the code

AbstractProcessor init method to get a Map of key-value pairs:

@Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); / / access to configuration parameters in gradle Map < String, the String > options. = processingEnvironment getOptions (); }Copy the code

For example, three key-value pairs are declared in app. The keys are module_name, module_main, and modules_list. Module_name is the name of the module, so it can have a specific value when generating RouterSaving_moduleName.

Module_main Indicates whether the current module is the main module of the project. This is only configured in gradle for the main module. Other modules are not required. Modules_list is the module that uses Route. These modules need to be configured here because RouterInit is only generated in the main project. It needs to get the path and name of each RouterSaving_moduleName internally. The current module cannot get the names of other modules, so you can only configure them here.

The principle and points to note are described above, and the contents of the final AnnotationProcessor are given below:

@AutoService(Processor.class) public class RouteAnnotationProcessor extends AbstractProcessor { private static final String MODULE_NAME_KEY = "module_name"; private static final String MODULE_MAIN_KEY = "module_main"; private static final String MODULES_LIST = "modules_list"; private static final String MODULE_MAIN_VALUE = "yes"; Private Filer Filer; Private Messager Messager; // Name of each module private String moduleName; Module private String moduleMain; // All modules use ampersand strings, such as "app&module&module2" private String modules; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); filer = processingEnvironment.getFiler(); messager = processingEnvironment.getMessager(); / / access to configuration parameters in gradle Map < String, the String > options. = processingEnvironment getOptions (); if (options ! = null && ! options.isEmpty()) { moduleName = options.get(MODULE_NAME_KEY); moduleMain = options.get(MODULE_MAIN_KEY); modules = options.get(MODULES_LIST); } } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {// If a module has a Route annotation it goes to the process method, otherwise generateInit() is not generateInit(); generateList(roundEnvironment); return true; } /** * Generate RouteInit file * Generate a static method in this file that calls RouterSaving_moduleName. Add () for each module that uses Route. * Code is generated to store Activity information into the collection in RouterSaving_moduleName. * Note that: This is only done at compile time, the collection is actually stored at run time, Therefore, the init method of the Routers will be dynamically executed */ private void generateInit() {// RouterInit if is generated for the master module (module_main_value.equals (moduleMain)) {// Declare init method methodSpec.Builder initMethod = in RouterInit MethodSpec.methodBuilder("init") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); // Init calls RouterSaving_ add on all Modules if (modules! = null && !" ".equals(modules)) { String[] moduleList = modules.split("&"); if (moduleList.length > 0) { for (String module : moduleList) { initMethod.addStatement("RouterSaving_" + module + ".add()"); } } } // shengm TypeSpec routerInit = TypeSpec.classBuilder("RouterInit") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(initMethod.build()) .build(); try { JavaFile.builder("com.example.route", routerInit) .build() .writeTo(filer); } catch (Exception e) { e.printStackTrace(); }}} /** * Generate a static method in this file that iterates how many activities in the current Module are modified by the Route annotation. Then get the path and class objects for the current Activity * and store them in the collection. The static method is called in the RouteInit class above, */ private void generateList(RoundEnvironment RoundEnvironment) {methodSpec.builder initMethod = MethodSpec.methodBuilder("add") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(Route.class); for (Element element : elements) { Route annotation = element.getAnnotation(Route.class); String path = annotation.value(); ClassName className = ClassName.get((TypeElement) element); initMethod.addStatement("com.example.routeapi.Routers.addRoute($S, $T.class)", path, className); } TypeSpec routerInit = TypeSpec.classBuilder("RouterSaving_" + moduleName) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(initMethod.build()) .build(); try { JavaFile.builder("com.example.route", routerInit) .build() .writeTo(filer); } catch (Exception e) { e.printStackTrace(); } } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new HashSet<>(); / / specified under the current module supports only the Route annotation set. The add (Route. Class. GetCanonicalName ()); return set; } @Override public Set<String> getSupportedOptions() { Set<String> set = new HashSet<>(); set.add(MODULE_NAME_KEY); set.add(MODULE_MAIN_KEY); set.add(MODULES_LIST); return set; }}Copy the code

4. Create the Android Library-RouteAPI

The library’s job is to provide specific apis for external calls. Because this library is used for external calls, the collection of activities that are stored is in here, not in the routeAnnotation library and not in the routeCompiler library. As mentioned above, the RouterInit and RouterSaving_moduleName classes simply generate code to add activities to the collection, which must be enabled at runtime.

The Library provides a set of Routers, which have two functions:

  • init

One of the things init does is get an Application, and if the context is not passed in, use the Application jump. Another function is to enable adding Actiivty to the collection. Instead of calling RouterInit’s init method directly, because we can’t access it, we just reflect it back to RouterInit and call its init method. In this case, you only need to call phones.init () in the onCreate method of Application.

@Override
public void onCreate() {
    super.onCreate();
    Routers.init(this);
}
Copy the code
  • navigation

When we store an Activity, we store the path in the annotation. When we jump, we pass in a path. At this point, we match the host. The Intent is used to convey the path of the Intent.

public void navigation(Context context, String path) { for (RouterInfo routerInfo : list) { if (routerInfo.getPath().equals(path)) { Intent intent = new Intent(context, routerInfo.getActivity()); intent.putExtra(RAW_URL, path); context.startActivity(intent); break; }}}Copy the code

The third-party application is forwarded to this application

If you open a page in your application, you need to use DeepLink. If you have multiple pages, you need to set scheme and host for each page. This is a hassle. Other applications pull up the page uniformly, and the page pulls up the specific page according to the URI. Pulling up the specific page is the jump logic inside the application above.

DeepLink activities:

public class RouterActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); Uri uri = getIntent().getData(); Routers.getInstance().navigation(this, uri.toString()); finish(); }}Copy the code

Because there is a lot of code, not all of it is listed in this blog post. Here is the Demo Router on Git

reference

Alibaba ARouter: github.com/alibaba/ARo…

ActivityRouter:github.com/mzule/Activ…