Android development, componentization, modularization is a platitude problem. As project complexity increases, modularity is an inevitable trend. Unless you can bear to change the code, it takes six or seven minutes.

Another problem that comes with modularization is the problem of page hops, which sometimes make code inaccessible to each other due to code isolation. Thus, the Router framework was born.

At present, there are more commonly used ali ARouter, Meituan WMRouter, ActivityRouter and so on.

Today, let’s take a look at how to implement a routing framework. The functions realized are.

  1. Easy to use based on compile-time annotations
  2. Result callback. Each jump Activity calls back the jump result
  3. In addition to customizing routes using annotations, manual routing is also supported
  4. Support multi-module use, support componentized use

Directions for use

The basic use

First, specify path above the activity to jump to,

@Route(path = "activity/main")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
Copy the code

In the place to jump

Router.getInstance().build("activity/main").navigation(this);
Copy the code

If you want to use it in Dommoule

The first step is to specify how many moudles there are using @Modules({“app”, “SDK “}) and specify the name of the current moudle in each moudle, annotating it with @Module(“”). Note that @modules ({“app”, “SDK “}) corresponds to @module (“”).

In the main moudle,

@Modules({"app", "moudle1"}) @Module("app") public class RouterApplication extends Application { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); Router.getInstance().init(); }}Copy the code

In moudle1,

@Route(path = "my/activity/main") @Module("moudle1") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main_2); }}Copy the code

This allows multiple modules to be used.

User-defined injection router

Router.getInstance().add("activity/three", ThreeActivity.class);
Copy the code

Called when a jump occurs

Router.getInstance().build("activity/three").navigation(this);
Copy the code

Results the callback

Description Route jump result callback.

Router.getInstance().build("my/activity/main", New RouterCallback() {@override public Boolean beforeOpen(Context Context, Uri Uri) {// Before opening the route log. I (TAG, "beforeOpen: uri=" + uri); return false; } @override public void afterOpen(Context Context, Uri Uri) {log. I (TAG, "afterOpen: uri=" + uri); } @override public void notFind(Context Context, uri uri) {log. I (TAG, "notFind: uri=" + uri); } @override public void error(Context Context, Uri Uri, Throwable e) {log. I (TAG, "error: Uri =" + Uri + "; e=" + e); } }).navigation(this);Copy the code

StartActivityForResult Indicates the jump result callback

Router.getInstance().build("activity/two").navigation(this, new Callback() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(TAG, "onActivityResult: requestCode=" + requestCode + "; resultCode=" + resultCode + "; data=" + data); }});Copy the code

Principle that

Implementing a Router framework involves the following key points:

  1. Annotation handling
  2. How to solve the dependency problem between multiple modules, and how to support multiple modules
  3. Router jump and Activty startActivityForResult processing

Let’s take these three questions and explore them.

It is divided into four parts: Router-annotion, router-Compiler, router-API, and stub

Router-annotion mainly defines annotations and is used to store annotation files

Router-compiler is primarily used for annotations and automatically generates code for us

Router-api is an external API for handling jumps.

Stub this is to store some empty Java files, preempted pit. Does not package into jars.

router-annotion

Three main annotations are defined

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    String path();
}
Copy the code
@Retention(RetentionPolicy.CLASS)
public @interface Modules {
    String[] value();
}

Copy the code
@Retention(RetentionPolicy.CLASS)
public @interface Module {
    String value();
}
Copy the code

The Route annotation is mainly used to indicate the path to jump to.

Modules annotation, indicating how many moudles there are.

Module annotation with the name of the current moudle.

Modules, Module annotations are designed to support multiple Modules.


router-compiler

Router-compiler only has one class, RouterProcessor. The principle of router-Compiler is also relatively simple: it scans those classes with annotations and stores the information for corresponding processing. Here the corresponding Java files are generated.

It consists of the following two steps

  1. Depending on whether or not@Modules @ModuleAnnotations, and then generate the correspondingRouterInitfile
  2. scanning@RouteNotes, and according tomoudleNameGenerate the appropriate Java file

Basic introduction to annotations

Before getting into The RouterProcessor, let’s take a look at the basics of annotations.

If you’re not familiar with custom annotations, you can start with the two previous articles I wrote. Android custom compile-time annotations 1 – A simple example, Android compile-time annotations – syntax details

public class RouterProcessor extends AbstractProcessor { private static final boolean DEBUG = true; private Messager messager; private Filer mFiler; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); messager = processingEnv.getMessager(); mFiler = processingEnv.getFiler(); UtilManager.getMgr().init(processingEnv); } / * * * define your annotation processor which registered to comment on * / @ Override public Set < String > getSupportedAnnotationTypes () {Set < String > annotations = new LinkedHashSet<>(); annotations.add(Route.class.getCanonicalName()); annotations.add(Module.class.getCanonicalName()); annotations.add(Modules.class.getCanonicalName()); return annotations; } / Java version * * * * / @ Override public SourceVersion getSupportedSourceVersion () {return SourceVersion. LatestSupported (); }}Copy the code

First of all, let’s take a look at getSupportedAnnotationTypes method, this method returns the annotations we support scan.

Annotation handling

Let’s look at the process method again

@Override public boolean process(Set<? Annotations extends TypeElement> Annotations, RoundEnvironment roundEnv) { Direct return if (annotations = = null | | annotations. The size () = = 0) {return false. } UtilManager.getMgr().getMessager().printMessage(Diagnostic.Kind.NOTE, "process"); boolean hasModule = false; boolean hasModules = false; // module String moduleName = "RouterMapping"; Set<? extends Element> moduleList = roundEnv.getElementsAnnotatedWith(Module.class); if (moduleList ! = null && moduleList.size() > 0) { Module annotation = moduleList.iterator().next().getAnnotation(Module.class); moduleName = moduleName + "_" + annotation.value(); hasModule = true; } // modules String[] moduleNames = null; Set<? extends Element> modulesList = roundEnv.getElementsAnnotatedWith(Modules.class); if (modulesList ! = null && modulesList.size() > 0) { Element modules = modulesList.iterator().next(); moduleNames = modules.getAnnotation(Modules.class).value(); hasModules = true; } debug("generate modules RouterInit annotations=" + annotations + " roundEnv=" + roundEnv); debug("generate modules RouterInit hasModules=" + hasModules + " hasModule=" + hasModule); // RouterInit if (hasModules) {// Generate a moudle debug("generate Modules RouterInit") with @modules; generateModulesRouterInit(moduleNames); } else if (! HasModule) {// Generate a moudle debug with a single moudle debug(" Generate default RouterInit") without the @Modules annotation; generateDefaultRouterInit(); } // scan the Route annotation Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class); List<TargetInfo> targetInfos = new ArrayList<>(); for (Element element : elements) { System.out.println("elements =" + elements); // Check the type if (! Utils.checkTypeValid(element)) continue; TypeElement typeElement = (TypeElement) element; Route route = typeElement.getAnnotation(Route.class); targetInfos.add(new TargetInfo(typeElement, route.path())); } // Generate the corresponding Java file according to the module name if (! targetInfos.isEmpty()) { generateCode(targetInfos, moduleName); } return false; }Copy the code

, first of all, to determine whether there are annotations need to deal with, if not directly return annotations = = null | | annotations. The size () = = 0.

We then determine if there is an @modules annotation (in this case, multiple moudles), Have words will call generateModulesRouterInit (String [] moduleNames) method to generate RouterInit Java file, when there is no @ Modules annotations, And without the @Module (in this case, a single moudle), the default RouterInit file is generated.

private void generateModulesRouterInit(String[] moduleNames) { MethodSpec.Builder initMethod = MethodSpec.methodBuilder("init") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); for (String module : moduleNames) { initMethod.addStatement("RouterMapping_" + module + ".map()"); } TypeSpec routerInit = TypeSpec.classBuilder("RouterInit") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(initMethod.build()) .build(); try { JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, routerInit) .build() .writeTo(mFiler); } catch (Exception e) { e.printStackTrace(); }}Copy the code

Let’s say we have two moudles: “app” and “moudle1”, so the code we end up generating looks like this.

public final class RouterInit { public static final void init() { RouterMapping_app.map(); RouterMapping_moudle1.map(); }}Copy the code

If we didn’t use @moudles and @Module annotations, the generated RouterInit file would look something like this.

public final class RouterInit { public static final void init() { RouterMapping.map(); }}Copy the code

That’s why stub Modules exist. By default, we need to initialize the map with RouterInit. Without these two files, the IDE editor will report an error when compile.

compileOnly project(path: ':stub')
Copy the code

The way we introduced this is to use compileOnly, which will not include these files when you regenerate them into jars, but will run in the IDE editor. This is also a little trick.

Route annotation processing

Let’s go back to the process method’s handling of the Route annotation.

// Scan Route for its own annotation Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Route.class); List<TargetInfo> targetInfos = new ArrayList<>(); for (Element element : elements) { System.out.println("elements =" + elements); // Check the type if (! Utils.checkTypeValid(element)) continue; TypeElement typeElement = (TypeElement) element; Route route = typeElement.getAnnotation(Route.class); targetInfos.add(new TargetInfo(typeElement, route.path())); } // Generate the corresponding Java file according to the module name if (! targetInfos.isEmpty()) { generateCode(targetInfos, moduleName); }Copy the code

All Route annotations are scanned and added to the targetInfos list, and the generateCode method is called to generate the corresponding file.

private void generateCode(List<TargetInfo> targetInfos, String moduleName) {
      
        MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("map")
//                .addAnnotation(Override.class)
                .addModifiers(Modifier.STATIC)
                .addModifiers(Modifier.PUBLIC);

//                .addParameter(parameterSpec);
        for (TargetInfo info : targetInfos) {
            methodSpecBuilder.addStatement("com.xj.router.api.Router.getInstance().add($S, $T.class)", info.getRoute(), info.getTypeElement());
        }


        TypeSpec typeSpec = TypeSpec.classBuilder(moduleName)
//                .addSuperinterface(ClassName.get(interfaceType))
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .addJavadoc("Generated by Router. Do not edit it!\n")
                .build();
        try {
            JavaFile.builder(Constants.ROUTE_CLASS_PACKAGE, typeSpec)
                    .build()
                    .writeTo(UtilManager.getMgr().getFiler());
            System.out.println("generateCode: =" + Constants.ROUTE_CLASS_PACKAGE + "." + Constants.ROUTE_CLASS_NAME);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("generateCode:e  =" + e);
        }

    }
Copy the code

This method mainly uses Javapoet to generate Java files. See the documentation on the official website for the use of Javaposet. The generated Java files look like this.

package com.xj.router.impl; import com.xj.arounterdemo.MainActivity; import com.xj.arounterdemo.OneActivity; import com.xj.arounterdemo.TwoActivity; /** * Generated by Router. Do not edit it! */ public class RouterMapping_app { public static void map() { com.xj.router.api.Router.getInstance().add("activity/main", MainActivity.class); com.xj.router.api.Router.getInstance().add("activity/one", OneActivity.class); com.xj.router.api.Router.getInstance().add("activity/two", TwoActivity.class); }}Copy the code

As you can see, the annotation information we define is eventually stored by calling router.getInstance ().add().


router-api

This module is mainly a multi-exposed API, the most important file is Router.

public class Router { private static final String TAG = "ARouter"; private static final Router instance = new Router(); private Map<String, Class<? extends Activity>> routeMap = new HashMap<>(); private boolean loaded; private Router() { } public static Router getInstance() { return instance; } public void init() { if (loaded) { return; } RouterInit.init(); loaded = true; }}Copy the code

When we want to initialize the Router, use init instead. Init checks if it is initialized, and calls RouterInit#init to initialize it if it is not.

In RouterInit#init, RouterMap_{@modulename}#map is called and router.getInstance ().add() is called

The router jumped back. Procedure

Public interface RouterCallback {/** * before jump router * @param context * @param uri * @return */ Boolean beforeOpen(Context context, Uri uri); / @param context * @param uri */ void afterOpen(context context, uri uri); /** * router * @param context * @param uri */ void notFind(context context, uri uri); /** * router error * @param context * @param uri * @param e */ void error(context context, uri uri, Throwable e); }Copy the code
public void navigation(Activity context, int requestCode, Callback callback) {
    beforeOpen(context);
    boolean isFind = false;
    try {
        Activity activity = (Activity) context;
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(context.getPackageName(), mActivityName));
        intent.putExtras(mBundle);
        getFragment(activity)
                .setCallback(callback)
                .startActivityForResult(intent, requestCode);
        isFind = true;
    } catch (Exception e) {
        errorOpen(context, e);
        tryToCallNotFind(e, context);
    }

    if (isFind) {
        afterOpen(context);
    }

}

private void tryToCallNotFind(Exception e, Context context) {
    if (e instanceof ClassNotFoundException && mRouterCallback != null) {
        mRouterCallback.notFind(context, mUri);
    }
}



Copy the code

Focus on the navigation method. When jumping to an activity, the beforeOpen method is first called to call RouterCallback#beforeOpen. Then when a catch exception occurs, the errorOpen method is called to call back the RouterCallback#errorOpen method if an error occurs. Call the tryToCallNotFind method to determine if it is a ClassNotFoundException and call back RouterCallback#notFind.

If no eception occurs, RouterCallback#afterOpen is called back.

The Activity’s startActivityForResult callback

As you can see, our Router also supports startActivityForResult

Router.getInstance().build("activity/two").navigation(this, new Callback() { @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.i(TAG, "onActivityResult: requestCode=" + requestCode + "; resultCode=" + resultCode + "; data=" + data); }});Copy the code

Its implementation principle is actually very simple, with the help of a blank fragment to achieve, the principle can be seen in my previous article.

Android Fragment nines – Gracefully apply for permissions and handle onActivityResult


summary

If you feel the effect is good, please go to Star on Github, thank you. Router

For our Router framework, the process looks something like this.


digression

Read the above article, do you understand the three questions mentioned at the beginning of the article? Feel free to leave a comment in the comments section.

  1. Annotation handling
  2. How to solve the dependency problem between multiple modules, and how to support multiple modules
  3. Router jump and Activty startActivityForResult processing

In fact, many router frameworks are implemented with gradle plug-ins. One advantage of this is that when using the moudle, we only need the Apply plugin, and some details are shielded. But, in fact, his principle is pretty much the same as ours.

Next, I will write about the Gradle Plugin and implement the Router framework with Gradle. If you are interested, you can follow my wechat public number, Xu Gong Code word, thank you.

Related articles

Java Type,

Java reflection mechanism in detail

Introduction to Annotations (PART 1)

If you feel good, you can pay attention to my wechat public number programmer Xu Gong

  1. Public number programmer Xu Gong repliesDark horseGet the Android Learning video
  2. Public number programmer Xu Gong repliesXu, male, 666, get resume template, teach you how to optimize your resume, enter big factory
  3. Public number programmer Xu Gong repliesThe interview, can obtain the interview common algorithm, sword refers to the offer question solution
  4. Public number programmer Xu Gong repliesThe horse soldiers, you can get a copy of the training video