Lala Android Modular Routing Framework: TheRouter|Open Source Lab

Original link: https://www.kymjs.com/code/2022/09/04/01

If you have any questions about this article, you can add my personal WeChat to ask: kymjs666

TheRouter is a complete set of solution frameworks written in Kotlin for Android modular development.

Please refer to https://github.com/HuolalaTech/hll-wp-therouter-android for the Github project address and usage documentation.

The core functions of TheRouter have the following capabilities:

  • Page Navigation Jump Capability (Navigator)
  • Cross-module dependency injection capability (ServiceProvider)
  • Single-module automatic initialization capability (FlowTaskExecutor)
  • Dynamic capability (ActionManager)
  • One-click switch script for module AAR/source code dependency

1. Why use TheRouter

Routing is an indispensable function in mobile development today, especially for enterprise-level apps. It can be used to decouple the strong dependencies of Intent page jumps and reduce the interdependence problem of cross-team development.

For the development of large-scale APPs, modularization (or componentization) is basically used for development, which requires higher decoupling between modules. TheRouter is a complete set of solutions for modular development. It not only supports conventional module dependency decoupling and page jumping, but also provides solutions to common problems in the modularization process. For example, it perfectly solves the problem that the Application life cycle and business process cannot be obtained in the component after modular development, resulting in the need to modify the code across modules for each initialization and associated dependency calls.

1.1 Four Capabilities of TheRouter

Navigator:

  • Support Activity and Fragment
  • Supports many-to-one relationship or one-to-one relationship between Path and page, which can be used to solve the problem of multi-terminal path unification
  • Page Path supports regular expression declaration
  • Support json format routing table export
  • Support dynamic delivery of json routing table, downgrade any page to H5
  • Supports the transfer of any object across modules (no serialization is required, and the object type is guaranteed)
  • Support page jump interception processing
  • Support custom page parameter parsing methods (such as parsing json into objects)
  • Support using routing to jump to Activity ( Fragment ) in third-party SDK

ServiceProvider:

  • Support cross-module dependency injection
  • Support the creation rules of custom injection items, and dependency injection can customize parameters
  • Support custom service interception, single-module mock debugging
  • Support for injecting object cache, multiple injections will only new object once

FlowTaskExecutor:

  • Support single module independent initialization
  • Support lazy loading initialization
  • Independent initialization allows multitasking dependencies (see Gradle Task )
  • Support compile-time circular reference detection
  • Supports custom business initialization timing, which can be used to solve privacy compliance issues

ActionManager:

  • Support global callback configuration
  • Support priority response and interrupt response
  • Support to record the call path to solve the problem that the observer mode cannot track the Observable during the debugging period

Note: FlowTaskExecutor and ActionManager will be optional capabilities in the future, providing可插拔or stand-单独使用options (expected to be available in October).

2. Routing scheme

At present, the existing routing basically focuses on the realization of two capabilities: page jumping and cross-module calling. The core technical solution is generally as follows:

TheRouter

  1. In the development phase, add annotations to the landing page or called method to use the route.
  2. The annotations are parsed at compile time, and a series of intermediate codes are generated to be called.
  3. After the application starts, the intermediate code is called to complete the preparation of the route. Most of the routes will additionally go through Gradle Transform to do an aggregation at compile time to improve the efficiency of preparing the routing table at runtime.
  4. When a route jump is initiated, it is essentially a routing table traversal, and the corresponding landing page or method object is obtained through the uri and called.

The same is true for TheRouter ‘s page jumps and cross-module calls, but there will be some details in the design.

TheRouter

TheRouter will generate classes starting with RouteMap__ according to the annotations at compile time. These classes record all the routing information of the current module, that is, the routing table of the current module.

In the top-level app module, all the classes starting with RouteMap__ in the RouteMap__ and source code are unified into the TheRouterServiceProvideInjecter class through the Gradle plugin.

After the subsequent application is started, it is only necessary to execute the method of the TheRouterServiceProvideInjecter class when initializing the route, and it can be loaded into all routing tables without any reflection .

The loaded routing table will be saved in a Map that supports regular matching, which is TheRouter allows multiple path to correspond to the same landing page. Whenever a page jump occurs, through the path at the time of jump, go to the Map to get the corresponding landing page information, and then call startActivity() normally.

3. Use TheRouter to jump to the page

3.1 Declare routing items

If a page (supporting Activity, Fragment) is allowed to be opened by routing, you need to use the annotation @Route declare routing items. Each page is allowed to declare multiple routing items, that is, one-to-many capability, which greatly reduces the unification of multi-terminal routing. business impact.

Parameter interpretation

  • path : routing path [required].
    The suggestion is a url. The use of regular expressions in the path is supported (for matching efficiency, the regular expression must contain back double slashes \), allowing multiple paths to correspond to the same Activity (Fragment).
  • action : custom event [optional].
    It is generally used to perform an execution action after opening the target page, such as a pop-up advertisement pop-up window on a custom page.
  • description : page description [optional].
    It will be recorded in the routing table, so that it is convenient to know what business each path or activity is during later investigation.
  • params : page parameters [optional].

    It is automatically written into the intent , allowing the writing to dynamically deliver and modify the default value in the routing table, or pass in the code when the routing jumps.

 @Route(path = "http://therouter.com/home", action = "action://scheme.com", description = "第二个页面", params = {"hello", "world"}) public class HomeActivity extends AppCompatActivity { }

3.2 Initiate page jump

The incoming parameters can be String and 8 basic data types, or Bundle , Serializable ,
Parcelable object, consistent with the Intent pass-by-value rule.

It also supports adding special business parameters such as Flag/Uri/ClipData/identifier to the Intent of this jump.

 // 传入参数可以通过注解@Autowired 解析成任意类型,如果是对象建议传json // context 参数如果不传或传null,会自动使用application 替换TheRouter.build("http://therouter.com/home") .withInt("key1", 12345678) .withString("key2", "参数") .withBoolean("key3", false) .withSerializable("key4", object) .withObject("object", any) // 这个方法可以传递任意对象,但是接收的地方对象类型需自行保证一致,否则会强转异常.navigation(context); // 如果传入requestCode,默认使用startActivityForResult启动Activity .navigation(context, 123); // 如果要打开的是fragment,需要使用.createFragment();

3.3 Routing table generation rules

If the path and target className of the two routes are exactly the same, they are considered to be the same route, regardless of whether the parameters are the same .

Routing table generation rules: The union is taken in the following order at compile time.

Override rules :

According to the following order, if the same, the latter can override the routing table rules of the former.

  1. Compile-time parsing annotations to generate routing tables
  2. First take the routing table in the业务模块aar
  3. Then take the routing table in the main app module code
  4. Finally, take the routing table declared in the assets/RouteMap.json file.
    • If there is no such file during compilation, a default routing table will be generated and placed in this directory; if there is, the routing table will be merged.
    • When the routing table is generated, you can configure whether to enable checking the validity of the route to determine whether the target page exists, and the (warning/error) level.
  5. Routing table dynamically delivered online at runtime
    • The routing table allows online dynamic distribution, which will cover the local routing table. For details, see [3.4 Design and Use of Dynamic Routing Tables]

If there is no such file during compilation, a default routing table will be generated and placed in this directory; if there is, the routing table will be merged. Therefore, for the third-party SDK that cannot modify the code, if you want to open it through routing, you only need to Manually declare it in the RouteMap.json file, and it can be opened by routing.

3.4 Design and use of dynamic routing table

The routing table of TheRouter is dynamically added. After each compilation of the project, a full routing table of the current APP will be generated in the apk. The default path is: /assets/therouter/routeMap.json . This routing table can also be used by remote delivery. For example, the remote terminal can deliver different routes for different APP versions for configuration purposes. In this way, if a crash occurs on some online pages in the future, you can temporarily solve such problems by replacing the landing page of this page with H5.

There are two recommended remote delivery methods for users to choose from:

  1. Connect the packaging system with the configuration system, and automatically upload the configuration files in the assets/ directory to the configuration system after each new version of the APP is packaged, and deliver it to the corresponding version of the APP. The advantage is that it is fully automatic without error.
  2. If the configuration system cannot be connected, the routing items that need to be modified are manually delivered online, because TheRouter will automatically overwrite the routing items in the package with the newly delivered routing items. The advantage is that it is accurate and occupies less traffic resources.

Note: Once you set up a custom InitTask , the routing table initialization task in the original framework will no longer be executed. You need to deal with the bottom line logic when the routing table cannot be found by yourself. For a suggested processing method, see the following code.

 // 此代码必须在Application.super.onCreate() 之前调用RouteMap.setInitTask(new RouterMapInitTask() { /** * 此方法执行在异步*/ @Override public void asyncInitRouteMap() { // 此处为纯业务逻辑,每家公司远端配置方案可能都不一样// 不建议每次都请求网络,否则请求网络的过程中,路由表是空的,可能造成APP无法跳转页面// 最好是优先加载本地,然后开异步线程加载远端配置String json = Connfig.doHttp("routeMap"); // 建议加一个判断,如果远端配置拉取失败,使用包内配置做兜底方案,否则可能造成路由表异常if (!TextUtils.isEmpty(json)) { List<RouteItem> list = new Gson().fromJson(json, new TypeToken<List<RouteItem>>() { }.getType()); // 建议远端下发路由表差异部分,用远端包覆盖本地更合理RouteMap.addRouteMap(list); } else { // 在异步执行TheRouter内部兜底路由表initRouteMap() } } });

3.5 Advanced usage

TheRouter also supports more page jumping capabilities. For details, please refer to the project documentation [ https://github.com/HuolalaTech/hll-wp-therouter-android/wiki/Navigator.md ]:

  • Add a routing table for the pages in the third-party library to achieve the purpose of downgrading and replacing some pages;
  • Delay routing jump (starting from Android 8, cannot start pages in the background);
  • Jump process interceptor (four layers in total, which can be used according to actual needs);
  • Jump result callback;

Fourth, the design of cross-module dependency injection ServiceProvider

For cross-module calls in modular development, we recommend the SOA (Service Oriented Architecture) design method. The service caller is completely isolated from the user, and the ability to call outside the module does not need to pay attention to who the provider of the ability is.

The core design idea of ServiceProvider is also the same. At present, the calling protocol between services adopts the method of interface. Of course, it is also compatible with direct calls instead of sinking through the interface.

TheRouter

Specifically on the Android side, it is a similar design to AIDL, but it is much simpler than AIDL development:

  • The service provider is responsible for providing the service and does not need to care who the caller will call itself and when.
  • The user of the service only cares about the service itself, not who provides the service, but only what capabilities the service can provide.

For example, in the picture above: Lala needs to use the recording service, and Xiaohuo provides a recording service, which is matched by TheRouter ‘s ServiceProvider .

4.1 Service user: Lala

She doesn’t need to care who provides the interface service IRecordService , he only needs to know that he needs to use such a service.

Note: TheRouter.get() may return null if there is no provider providing the service

 TheRouter.get(IRecordService::class.java)?.doRecord()

4.2 Service Provider: Small Goods

The service provider needs to declare a method to provide the service, marked with the @ServiceProvider annotation.

  • If it is java , it must be public static modification
  • If it is kotlin , it is recommended to write a top level function
  • Unlimited method name
 /** * 方法名不限定,任意名字都行* 返回值必须是服务接口名,如果是实现了服务的子类,需要加上returnType限定(例如下面代码) * 方法必须加上public static 修饰,否则编译期就会报错*/ @ServiceProvider public static IRecordService test() { return new IRecordService() { @Override public void doRecord() { String str = "执行录制逻辑"; } }; } // 也可以直接返回对象,然后标注这个方法的服名是什么@ServiceProvider(returnType = IRecordService.class) public static RecordServiceImpl test() { // xxx }

5. Design of FlowTaskExecutor, a single-module automatic initialization capability

As mentioned earlier, TheRouter is a set of solutions that are completely oriented towards modular development. In modular development, each module may have its own code that needs to be initialized. The previous practice was to declare these codes in the Application , but this may require modification of the module where the Application is located every time with business changes. The single-module automatic initialization capability of TheRouter is designed to solve such a situation. After the initialization method is declared in the current module, it will be automatically called in the business scenario.

Each method that wants to be automatically initialized must be modified with public static , the main reason is that it can be called directly by the class name. In addition, a lot of initialization code needs to obtain the Context object, so we use the Context as the default parameter of the initialization method, which will be automatically passed to the Application . There are no restrictions on other class names and method names. Anyway, as long as the @FlowTask annotation is added, it can be obtained through APT at compile time.

5.1 Introduction to FlowTaskExecutor

You can declare a method with any method name in any class in the current module, and add the @FlowTask annotation to the method.

@FlowTask annotation parameter description:

  • taskName : The task name of the current initialization task, which must be globally unique. The recommended format is: moduleName_taskName
  • dependsOn : Referring to Gradle Task, there may be dependencies between tasks. If the current task needs to depend on other tasks to be initialized first, declare the dependent task name here. You can depend on multiple tasks at the same time, separated by commas, optional spaces, and will be filtered: dependsOn = “mmkv, config, login”, the default is empty, and the application will be called when it starts.
  • async : Whether to execute this task asynchronously, the default is false.
 /** * 将会在异步执行*/ @FlowTask(taskName = "mmkv_init", dependsOn = TheRouterFlowTask.APP_ONCREATE, async = true) public static void test2(Context context) { System.out.println("异步=========Application onCreate后执行"); } @FlowTask(taskName = "app1") public static void test3(Context context) { System.out.println("main线程=========应用启动就会执行"); } /** * 将会在主线程初始化*/ @FlowTask(taskName = "test", dependsOn = "mmkv,app1") public static void test3(Context context) { System.out.println("main线程=========在app1和mmkv两个任务都执行以后才会被执行"); }

5.2 Built-in initialization node

Using this capability, two lifecycle tasks are supported by default inside the route, which can be directly referenced when using

  • TheRouterFlowTask.APP_ONCREATE : Initialized when Application’s onCreate() is executed
  • TheRouterFlowTask.APP_ONSPLASH : Initialized when the application’s first Activity.onCreate() is executed

At the same time, using TheRouter ‘s automatic initialization dependencies, there is no need to worry about the problems caused by circular dependencies. The framework will build a directed acyclic graph at compile time and monitor the circular dependencies. If found, it will report an error directly at compile time, and a loop will occur. Referenced tasks are displayed for troubleshooting.

5.3 Implementation principle

Each method annotated with @FlowTask will be parsed at compile time, and a corresponding Task object will be generated. This object contains relevant information about the initialization method, such as whether to execute asynchronously, task name, and whether to rely on other tasks to execute first.

When all Task are compiled and all tasks are generated, they will be aggregated in the main app through the Gradle plugin. At this time, all Task will be checked once, and有向无环图will be constructed to prevent Task references to tasks. Case.

After each application starts, all Task in the directed graph will be loaded in order according to their dependencies when the route is initialized.

TheRouter

6. Design of Dynamic Capability ActionManager

Action is essentially a global system callback, which is mainly used for a series of embedded operations, such as pop-up windows, uploading logs, and clearing caches.

Similar to the broadcast notification that comes with the Android system, you can declare actions and processing methods anywhere. And all Action can be tracked, as long as you want, you can output all the action call stacks in the log to facilitate debugging, which can solve the common problem brought by the observer mode to a certain extent: unable to track the Observable problem .

6.1 Action usage

Declare an Action:

 // action建议遵循一定的格式const val ACTION = "therouter://action/xxx" @FlowTask(taskName="action_demo") fun init(context: Context) = TheRouter.addActionInterceptor(ACTION, object: ActionInterceptor() { override fun handle(context: Context, args: Bundle): Boolean { // do something return false } })

Execute an Action:

 // action建议遵循一定的格式const val ACTION = "therouter://action/xxx" // 如果执行了一个没有被声明的Action,则不会有任何动作TheRouter.build(ACTION).action()

6.2 Advanced usage

Each Action is allowed to associate with multiple ActionInterceptor for processing, and the interceptor priority can be customized between multiple ActionInterceptor , and the execution of the next low-priority interceptor can be terminated at the same time.

The most typical application scenario: There may be multiple pop-up windows on the home page, and the pop-up windows between different businesses have priorities. In order to optimize the experience, we will definitely not pop up all the ActionInterceptor -up windows on the home page at one time. The priority relationship is declared for each pop-up window. Assuming that the requirement is that only 3 pop-up windows can pop up on the home page, then the current event can be closed after the third pop-up window is processed, and the next interceptor will not be responded.

 abstract class ActionInterceptor { abstract fun handle(context: Context, args: Bundle): Boolean fun onFinish() {} /** * 数字越大,优先级越高*/ open val priority: Int get() = 5 }

6.3 Client Dynamic Response Usage Scenario

If it is only used by the client , the common scenario may be: when the user performs some operations (opening a page, clicking a button in H5, click event of dynamic page configuration), it will be automatically triggered and the embedded Action logic will be executed. .

If the link with the server is opened , this capability actually requires the cooperation of the entire company. For example, there is a set of solutions similar to smart brains, which can intelligently infer what the user is going to do next based on some buried data of the client in the past. Directly send instructions to the client to do certain things through a persistent connection. Then, through operations such as page jumps, pop-up windows, clearing caches, logging out, etc. embedded in the client, you can operate through server-side instructions, which is a complete set of dynamic solutions.

TheRouter-ActionManager

7. One-click switch between source code and AAR

7.1 Gradle Scripts for Modular Support

In the process of modular development, if you do not use sub-warehouses, or use sub-warehouses but still use git-submodule to develop, you should encounter a problem. If the integrated package is compiled from source code, the construction time is too long, which greatly reduces the efficiency of development and debugging; if aar-dependent compilation is used, the aar must be rebuilt every time the code of the underlying module is modified, and the version number of the upper-level module can be modified before continuing Building and compiling the whole package also greatly affects the development efficiency.

A Gradle script is provided in TheRouter . You only need to declare the modules to be compiled in the local.properties file of the development locality. Other undeclared modules are compiled with module by default, so that you can flexibly switch between source code and aar without affecting others. , the following excerpt code is available for reference:

 /** * 如果工程中有源码,则依赖源码,否则依赖aar */ def moduleApi(String compileStr, Closure configureClosure) { String[] temp = compileStr.split(":") String group = temp[0] String artifactid = temp[1] String version = temp[2] Set<String> includeModule = new HashSet<>() rootProject.getAllprojects().each { if (it != rootProject) includeModule.add(it.name) } if (includeModule.contains(artifactid)) { println(project.name + "源码依赖:===project(\":$artifactid\")") projects.project.dependencies.add("api", project(':' + artifactid), configureClosure) // projects.project.configurations { compile.exclude group: group, module: artifactid } } else { println(project.name + "依赖:=======$group:$artifactid:$version") projects.project.dependencies.add("api", "$group:$artifactid:$version", configureClosure) } }

In actual use, you can completely replace the original api with moduleApi . Of course, implementation can also have a corresponding moduleImplementation , so that only need to comment or uncomment the include statement in the setting.gradle file to achieve the purpose of switching source code and aar .

8. Migrating from other routes to TheRouter

8.1 One-click Migration with Migration Tool

TheRouter provides a migration tool with a graphical interface, which can migrate from other routes to TheRouter with one click. Currently, only ARouter is supported. The migration of other routing frameworks is also under development:

If the IProvider.init() method of ARouter is used in the project, the initialization logic may need to be handled manually.

As shown below:

TheRouter

8.2 Comparison with other routes

Function TheRouter ARouter WMRouter
Fragment routing ✔️ ✔️ ✔️
Support for dependency injection ✔️ ✔️ ✔️
Load routing table No Runtime Scan No Reflections Scan dex reflection instance class at runtime

Large performance loss

Read file reflection instance class at runtime

performance loss

Annotated Regular Expressions ✔️ ✖️ ✔️
Activity specified interceptor ✔️ (Four interceptors can be customized according to business) ✖️ ✔️
Export routing document ✔️ (Route documentation supports adding comment description) ✔️ ✖️
Dynamically register routing information ✔️ ✔️ ✖️
APT supports incremental compilation ✔️ ✔️ (Incremental compilation is not possible when document generation is turned on) ✖️
plugin supports incremental compilation ✔️ ✖️ ✖️
Multiple Paths correspond to the same page (low cost to achieve unification of double-ended paths) ✔️ ✖️ ✖️
Remote routing table delivery ✔️ ✖️ ✖️
Support single module independent initialization ✔️ ✖️ ✖️
Support for opening third-party library pages using routing ✔️ ✖️ ✖️
Support for opening third-party library pages using routing ✔️ ✖️ ✖️
Support for hotfixes (eg tinker) ✔️ (unchanged code builds multiple times without changes) ✖️(Multiple builds of apt products will change and generate meaningless patches) ✖️(Multiple builds of apt products will change and generate meaningless patches)

9. Summary

TheRouter is not just a small and flexible routing library, but a complete set of Android modular solutions that can solve almost all problems encountered in the modular process.

For the existing routing framework, we also support smooth migration to the greatest extent. At present, the one-click migration tool of ARouter has been completed, and the migration of other frameworks is still under development. You can also submit a request in a Github issue , we will support it as soon as possible after evaluation, and anyone is welcome to provide Pull Requests .

Join WeChat group to communicate:

TheRouter official WeChat group

For more in-depth technical articles, and the cognition of the future direction of mobile terminal and big front-end, go to subscribe to the small column of Open Source Lab.

This article is reproduced from: https://www.kymjs.com/code/2022/09/04/01
This site is for inclusion only, and the copyright belongs to the original author.

Leave a Comment