What is ARouter?

ARouter is an Open source Android routing framework of Ali. It is a routing framework that helps Android App to be componentized. It supports routing, communication and decoupling between modules. Combining routing can realize componentization.

ARouter access means north

Complete Arouter access guide, heavy users of Arouter can skip and look straight ahead

  • First, use aroutter-register at the root of the build.gradle setting
apply plugin: 'com.alibaba.arouter'
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "com.alibaba:arouter-register:?"}}Copy the code
  • Second, create Baselib and add Dependencies
api 'com.alibaba:arouter-api:x.x.x'
Copy the code
  • Third, create a component module, such as the Login or Setting component
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // Replace with the latest version, but the API needs attention
    // To match with the compiler, use the latest version to ensure compatibility
    //compile 'com.alibaba: aroutert-api: X.X.X
    api project(path: ':baselib')
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'. }Copy the code
  • Step four, annotate the @route registration page
// Add a note to the routing page (mandatory)
// The path must have at least two levels, /xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {... }Copy the code
  • Step 5: Initialize
if (isDebug()) {           // These two lines must be written before init, otherwise these configurations will be invalid during init
    ARouter.openLog();     // Prints logs
    ARouter.openDebug();   // Enable debug mode (if running in InstantRun mode, you must enable debug mode! The online version needs to be closed, otherwise there are security risks)
}
ARouter.init(mApplication); // As early as possible, it is recommended to initialize in the Application
Copy the code
  • Step 6, use ARouter
ARouter.getInstance().build("/test/activity").navigation();
Copy the code

What are the advantages of ARouter over traditional Intents

Advantages of traditional Intents

  • lightweight
  • simple

Disadvantages of traditional Intents

  • The jump procedure cannot be controlled once invokedstartActivity(Intent)Then the system executes, the intermediate process cannot intervene
  • A jump failure cannot be caught, degraded, and an exception is thrown when a problem occurs
  • Shows severe coupling in intents due to direct class dependencies
startActivity(new Intent(MainActivity.this, LoginActivity.class));// Rely heavily on LoginActivity
Copy the code
  • Centralized management of rules occurs in implicit Intents, which leads to difficulties in collaboration and needs to be configured in the Manifest, resulting in poor scalability
// Implicit is stronger than explicit. It is possible to jump between two unclosed submodules, which cannot be done because explicit cannot introduce packages
Intent intent = new Intent();
intent.setClassName(MainActivity.this."com.cnn.loginplugin.ui.login.LoginActivity");// Set the package path
startActivity(intent);
Copy the code

ARouter advantages

  • Intermodule communication (principles later)
  • Support for URL Redirectbuild("/test/activity").navigation()
  • Support interceptors
// The classic application is to handle the login event during the jump, so there is no need to repeat the login check on the target page
Interceptors are executed between jumps, and multiple interceptors are executed in order of priority
@interceptor (priority = 8, name = "test Interceptor ")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {... callback.onContinue(postcard);// Return the control after processing
    // Callback.onInterrupt (new RuntimeException(" I think it's a little weird ")); // Interrupt the routing process

    // At least one of the above two must be called, otherwise the route will not continue
    }

    @Override
    public void init(Context context) {
    // Interceptor initialization will call this method only once when the SDK is initialized}}Copy the code
  • Parameter injection, @Autowired annotation implementation, more convenient, need to cooperateARouter.getInstance().inject(this);Used together
		@Autowired
    public String name;
    @Autowired
    int age;
    // Map the different parameters in the URL by name
    @Autowired(name = "girl") 
    boolean boy;
    // Support parsing custom objects, using JSON pass in URL
    @Autowired
    TestObj obj;
// Pass the implementation of List and Map using withObject
    // Serializable interface implementation class (ArrayList/HashMap)
    // The place where the object is received cannot be marked with the specific implementation class type
    // Only List or Map should be marked, otherwise the type in serialization will be affected
    Other similar cases need to be treated in the same way
    @Autowired
    List<TestObj> list;
    @Autowired
    Map<String, List<TestObj>> map;
Copy the code
  • Redirect to external urls
<activity android:name=".SchemeFilterActivity">
            <! -- Scheme -->
            <intent-filter>
                <data
                    android:host="www.nativie.com"
                    android:scheme="arouter"/>
                <action android:name="android.intent.action.VIEW"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
</activity>
Copy the code
  • Simple demo, making do simple static server interface, and deploy to oslanka. The dead simple. IO/statichtml….
<html>
<body>
<p><a href="http://www.360.com/">Test jump</a> </p>
<p><a href="arouter://www.nativie.com/login/login">Go to Android-arouter</a></p>
<p><a href="arouter://www.nativie.com/login/login?username=admin&password=123456">Jump to android-arouter with parameters</a></p>
<p><a href="arouter://www.nativie.com/setting/setting">Jump to the Android-Arouter Settings screen</a></p>
<p><a href="arouter://www.nativie.com/web/web">Jump to the Android-Arouter Settings screen</a></p>
<p><a href="arouter://www.nativie.com/test/test">Jump to the wrong android-arouter path</a></p>
</body>
</html>
Copy the code

About interceptors

  • Interceptors (intercepting jump procedures, aspect oriented programming)
  • What is aspect Oriented Programming AOP? AOP is an abbreviation of Aspect Oriented Programming, which means: Aspect Oriented Programming. It is a technique to achieve unified maintenance of program functions by means of precompilation and dynamic proxy during run time. AOP is a continuation of OOP, a hot topic in software development, an important content in Spring framework, and a derivative paradigm of functional programming. Using AOP can isolate each part of the business logic, so that the coupling degree between each part of the business logic is reduced, the reusability of the program is improved, and the efficiency of development is improved
// Interceptors are executed before the jump, and multiple interceptors are executed in order of priority
@interceptor (priority = 8, name = "test Interceptor ")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {... callback.onContinue(postcard);// Return the control after processing
    // Callback.onInterrupt (new RuntimeException(" I think it's a little weird ")); // Interrupt the routing process

    // At least one of the above two must be called, otherwise the route will not continue
    }

    @Override
    public void init(Context context) {
    // Interceptor initialization will call this method only once when the SDK is initialized}}Copy the code

Dynamic routing

  • Dynamic registration of routing information is applicable to some plug-in architecture apps and scenarios that require dynamic registration of routing information. Dynamic registration of routing information can be realized through the interface provided by ARouter. The target page and service can be annotated without @route annotation
ARouter.getInstance().addRouteGroup(new IRouteGroup() {
        @Override
        public void loadInto(Map<String, RouteMeta> atlas) {
            atlas.put("/dynamic/activity".// path
                RouteMeta.build(
                    RouteType.ACTIVITY,         // Routing information
                    TestDynamicActivity.class,  // The Class of the target
                    "/dynamic/activity".// Path
                    "dynamic".// Group, try to keep the same as the first paragraph of path
                    0.// Priority, not currently in use
                    0                           // Extra, used to mark the page)); }});Copy the code

ARouter detailed API


// Build standard routing requests and specify groups
ARouter.getInstance().build("/home/main"."ap").navigation();
// Build standard routing requests and parse them directly through the Uri
Uri uri;
ARouter.getInstance().build(uri).navigation();

// Build the standard routing request, startActivityForResult
// The first parameter of navigation must be the Activity and the second parameter must be the RequestCode
ARouter.getInstance().build("/home/main"."ap").navigation(this.5);

/ / specify the Flag
ARouter.getInstance()
    .build("/home/main")
    .withFlags();
    .navigation();

/ / get fragments
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
                    
// Object pass
ARouter.getInstance()
    .withObject("key".new TestObj("Jack"."Rose"))
    .navigation();

// Use the green channel (skip all interceptors)
ARouter.getInstance().build("/home/main").greenChannel().navigation();

Copy the code

The principle of exploring

  • Arouter. init, by obtaining/ data/app/package/base. The apkTo filter the classes generated by ARouter, as shown below.

  • For the Activity type, jumpARouter.getInstance().build("/login/login").navigation();, the final implementation is as follows:
**
     * Start activity
     *
     * @see ActivityCompat
     */
    private void startActivity(int requestCode, Context currentContext, Intent intent, Postcard postcard, NavigationCallback callback) {
        if (requestCode >= 0) {  // Need start for result
            if (currentContext instanceof Activity) {// Start context as an Activity
                ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
            } else {
              // When starting context as Application, requestCode is not supported
                logger.warning(Consts.TAG, "Must use [navigation(activity, ...)]  to support [startActivityForResult]"); }}else {// Start context as Application
            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
        }

        if ((-1! = postcard.getEnterAnim() && -1! = postcard.getExitAnim()) && currentContextinstanceof Activity) {    // Old version.
            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
        }

        if (null! = callback) {// Navigation over.callback.onArrival(postcard); }}Copy the code
  • How do two unrelated modules jump? We found that the context used in the final execution of startActivity is Application. The idea is that when a sub-module starts another uncorrelated sub-module, it returns the execution right to the main process/main program to handle

  • AROUTER_GENERATE_DOC=”enable” to generate arouter-map-of-xx.json and three Java files
// Update build.gradle with AROUTER_GENERATE_DOC = enable
// The generated document path: build/generated/ap_generated_sources/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]}}}}// How to generate the ARouter mapping? Three files are Generated
//ARouter$$Group$$login
//ARouter$$Providers$$loginplugin
//ARouter$$Root$$loginplugin
Copy the code

    atlas.put("/login/login", RouteMeta.build(RouteType.ACTIVITY, LoginActivity.class, "/login/login"."login".new java.util.HashMap<String, Integer>(){{put("password".8); put("username".8); }} -1, -2147483648));

//map Saves the mapping
//static Map<String, RouteMeta> routes = new HashMap<>();
Copy the code
  • How are these three files generated? APT is short for Annotation Processing Tool. It parses the annotations specified in the code at compile time and then does some other Processing (such as generating a new Java file via Javapoet). ARouter uses two librariesauto-service javapoetTo implement the injection from annotations to code, whereauto-serviceFor the library of the annotation processor,javapoetFor code generator

Learn APT by example

First of all, let’s look at meta-annotations, meta-annotations.

  • @Target
  TYPE, // Class, interface, enumeration class
  FIELD, // Member variables (including: constants)
  METHOD, // Member methods
  PARAMETER, / / method parameters
  CONSTRUCTOR, // The constructor
  LOCAL_VARIABLE, // Local variables
  ANNOTATION_TYPE, / / comment
  PACKAGE, // Can be used to decorate: packages
  TYPE_PARAMETER, // The type parameter is new in JDK 1.8
  TYPE_USE // Use types anywhere new in JDK 1.8` ` ` -@RetentionThe Java SOURCE is reserved only during the compilation of this compilation unit and is not written to the Class file. CLASS, is retained during compilation and is written to the CLASS file, but the JVM does not need to load the CLASS as visible at runtime (reflected visible) == is not visible when the JVM loads the CLASS. RUNTIME is preserved during compilation, the Class file is written, and the JVM loads the Class as a reflection visible annotation when it loads it. ` ` ` -@DocumentedThe purpose of annotations is to describe whether to retain the annotations when using the Javadoc tool to generate help documentation for a class@InheritedThe purpose of an annotation is to make the annotation it decorates inherited (if a class uses a@Inherited- We define our own annotations through meta-annotations - [AutoService annotation handler](HTTPS://github.com/google/auto/tree/master/service)Annotation handler is a tool in JavAC that scans and processes annotations at compile time. You can register your own annotation handler for specific annotations. At this point, I'm assuming you already know what an annotation is and how to declare an annotation type. An annotation handler for an annotation takes Java code (or compiled bytecode) as input and generates a file (usually a.java file) as output. - Virtual processor 'AbstractProcessor' - 'init(ProcessingEnvironment ENV)' : [core] Every annotation processor class must have an empty constructor. However, there is a special init() method that is called by the annotation processing tool with the 'ProcessingEnviroment' parameter. 'ProcessingEnviroment' provides many useful tool classes' Elements', 'Types', and' Filer '-' process(Set<? Extends TypeElement> Annotations, RoundEnvironment env) ': [core] This corresponds to the main function () for each processor. Here you write your code scanning, assessment and treatment annotations, and generate Java files - ` getSupportedAnnotationTypes ` here you must specify the (), The annotation processor which is registered to the annotations - ` getSupportedSourceVersion ` () is used to specify Java version you use. Usually here return ` SourceVersion. LatestSupported () ` - code generator used in the APT: * * [JavaPoet] (HTTPS:/ / github.com/square/javapoet) * * is a Java API for generating `. Java ` source files. (JavaPoet is a Java API, in order to generate the Java source file)

- 官方helloworld

```java
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);
Copy the code
  • The following Java files are generated from the above
package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!"); }}Copy the code
  • JavaPoetThe main API
- JavaFile is used to construct a Java file whose output contains a top-level class - TypeSpec generates classes, interfaces, Or enumeration-methodSpec to generate constructors or methods -fieldSpec to generate member variables or fields -parameterSpec to create parameters -AnnotationSpec to create annotationsCopy the code
  • JavaPoetPrimary placeholder
- $L(forLiterals) the character or common type that executes the structure, or TypeSpec, $S(forString, $T(forClass Types), $N (for$L>$S//1.Pass an argument value for each placeholder in the format string to `CodeBlock.add()`. In each example, we generate code to say "I ate 3 tacos"
CodeBlock.builder().add("I ate $L $L".3."tacos")
 //2.When generating the code above, we pass the hexDigit() method as an argument to the byteToHex() method using $N:
  MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex")
    .addParameter(int.class, "b")
    .returns(String.class)
    .addStatement("char[] result = new char[2]")
    .addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit)
    .addStatement("result[1] = $N(b & 0xf)", hexDigit)
    .addStatement("return new String(result)")
    .build();
/ / = = = = = = = = = = = = = = = = = = = = = = =
public String byteToHex(int b) {
  char[] result = new char[2];
  result[0] = hexDigit((b >>> 4) & 0xf);
  result[1] = hexDigit(b & 0xf);
  return new String(result);
}

//$T for Types
//We Java programmers love our types: they make our code easier to understand. And JavaPoet is on board. It has rich built-in support for types, including automatic generation of import statements. Just use $T to reference types:
.addStatement("return new $T()", Date.class)== return new Date();
Copy the code

Actual combat – Custom simple version routing -CRouter

  • Create a new name-annotation javaLib and define the CRoute annotation
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface CRoute {
    String path(a);
}
Copy the code
  • The new name – the compiler javaLib
1.
dependencies {
    implementation project(path: ':TestRouter-annotation')
    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'
}
2.@AutoService(Processor.class)
public class TestRouteProcessor extends AbstractProcessor {
  @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
       //dosomething
    }
   @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
      //dosomething}}Copy the code
  • The execution sequence of service modules is as follows
 1. annotationProcessor project(':TestRouter-compiler')
implementation project(':TestRouter-annotation')
2.Add annotations@CRoute(path = "/csetting/csetting")
3.Compile operation4.The Java file generated by the business Module APT is as follows:public final class C$csettingC$csettingHelloWorld {
  public static String holder = "/csetting/csetting:com.cnn.settingplugin.SettingsActivity";

  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!"); }}Copy the code
  • referenceARouter-initMethod, write us outCRouter-init
 /** * Init, it must be call before used router. */
    public static void init(Application application) {
        if(! hasInit) { CRouter.application=application; hasInit=true;
            try {
                getFileNameByPackageName(application, ROUTE_ROOT_PAKCAGE);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code
  • The annotation mapping is obtained by reflection and stored in HashMap by referring to ARouter

  • Simulate the jump by implicitly starting the Activity

  • Here we simulate a simple version of ARouter, full custom CRouter
/** * Created by caining on 7/29/21 16:09 * E-mail Address: [email protected] */
public class CRouter {
    private volatile static CRouter instance = null;
    private volatile static boolean hasInit = false;
    private static Application application;
    public static final String ROUTE_ROOT_PAKCAGE = "com.cnn.crouter";
    private static Map<String ,String> mapHolder = new HashMap<>();

    /** * Init, it must be call before used router. */
    public static void init(Application application) {
        if(! hasInit) { CRouter.application=application; hasInit=true;
            try {
                getFileNameByPackageName(application, ROUTE_ROOT_PAKCAGE);
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch(InterruptedException e) { e.printStackTrace(); }}}/** * Get instance of router. A * All feature U use, will be starts here. */
    public static CRouter getInstance(a) {
        if(! hasInit) {throw new InitException("ARouter::Init::Invoke init(context) first!");
        } else {
            if (instance == null) {
                synchronized (CRouter.class) {
                    if (instance == null) {
                        instance = newCRouter(); }}}returninstance; }}public void navigation(String path) {
         startActivity(path);
    }

    private void startActivity(String path) {
        String classPath
                = mapHolder.get(path);
        if(! TextUtils.isEmpty(classPath)) { Intent intent =new Intent();
            intent.setClassName(application, classPath);// Set the package path
            ActivityCompat.startActivity(application, intent, null);
        }else {
            Toast.makeText(application, "The path is empty", Toast.LENGTH_SHORT).show(); }}/** * scan all classnames ** contained under the package by specifying the package name@param context     U know
     * @paramPackageName package name *@returnSet of all classes */
    private static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
        final Set<String> classNames = new HashSet<>();

        List<String> paths = getSourcePaths(context);
        final CountDownLatch parserCtl = new CountDownLatch(paths.size());

        for (final String path : paths) {
            DefaultPoolExecutor.getInstance().execute(new Runnable() {
                @Override
                public void run(a) {
                    DexFile dexfile = null;

                    try {
                        if (path.endsWith("EXTRACTED_SUFFIX")) {
                            //NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
                            dexfile = DexFile.loadDex(path, path + ".tmp".0);
                        } else {
                            dexfile = new DexFile(path);
                        }

                        Enumeration<String> dexEntries = dexfile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (className.startsWith(packageName)) {
                                classNames.add(className);
                                try {
                                    Class clazz = Class.forName(className);
                                    Object obj = clazz.newInstance();
                                    Field field03 = clazz.getDeclaredField("holder"); // Get the field whose attribute is ID
                                    String value= (String) field03.get(obj);
                                    String[] split = value.split(":");
                                    if(split! =null&&split.length==2) {
                                        mapHolder.put(split[0],split[1]);
                                    }
                                    Log.i("test-->",mapHolder.toString());
                                } catch (ClassNotFoundException e) {
                                    e.printStackTrace();
                                } catch (IllegalAccessException e) {
                                    e.printStackTrace();
                                } catch (InstantiationException e) {
                                    e.printStackTrace();
                                } catch (SecurityException e) {
                                    e.printStackTrace();
                                } catch (NoSuchFieldException e) {
                                    e.printStackTrace();
                                } catch(IllegalArgumentException e) { e.printStackTrace(); }}}}catch (Throwable ignore) {
                        Log.e("ARouter"."Scan map file in dex files made error.", ignore);
                    } finally {
                        if (null! = dexfile) {try {
                                dexfile.close();
                            } catch(Throwable ignore) { } } parserCtl.countDown(); }}}); } parserCtl.await();return classNames;
    }
    private static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
        ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
        List<String> sourcePaths = new ArrayList<>();
        sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
        returnsourcePaths; }}Copy the code

conclusion

  • ARouter user guide
  • ARouter interceptor
  • SchemeFilte to achieve external HTML jump Native, through WEB&Native
  • Understand apt principle of JavaPoet &AutoService annotation processor
  • Write a simple version of CRouter, through the actual combat we understand the realization principle of ARouter
  • Project Demo Address

The problem

  • Other than ARouter, do you know of any frameworks that take advantage of APT?
  • Are there any disadvantages to ARouter?

reference

  • Github.com/alibaba/ARo…

  • Github.com/square/java…

  • github.com/google/auto

  • Github.com/Oslanka/sta…

  • Github.com/Oslanka/Aro…