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
andFragment
- 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:
- In the development phase, add annotations to the landing page or called method to use the route.
- The annotations are parsed at compile time, and a series of intermediate codes are generated to be called.
- 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. - 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
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.
- Compile-time parsing annotations to generate routing tables
- First take the routing table in the
业务模块aar
- Then take the routing table in the main
app module
code - 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.
- 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:
- 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. - 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.
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 bepublic 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.
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.
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:
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:
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.