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.
- Easy to use based on compile-time annotations
- Result callback. Each jump Activity calls back the jump result
- In addition to customizing routes using annotations, manual routing is also supported
- 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:
- Annotation handling
- How to solve the dependency problem between multiple modules, and how to support multiple modules
- 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
- Depending on whether or not
@Modules
@Module
Annotations, and then generate the correspondingRouterInit
file - scanning
@Route
Notes, and according tomoudleName
Generate 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.
- Annotation handling
- How to solve the dependency problem between multiple modules, and how to support multiple modules
- 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
- Public number programmer Xu Gong repliesDark horseGet the Android Learning video
- Public number programmer Xu Gong repliesXu, male, 666, get resume template, teach you how to optimize your resume, enter big factory
- Public number programmer Xu Gong repliesThe interview, can obtain the interview common algorithm, sword refers to the offer question solution
- Public number programmer Xu Gong repliesThe horse soldiers, you can get a copy of the training video