Android’s native routing scheme is to implement both explicit and implicit Activity jump schemes through intents. Explicit intents need to be directly applied to the target Activity, leading to direct coupling between different pages. Implicit intents are centrally configured in the Manifest as actions. Unmanageable problems. And in the componentalized development, each module cannot reference each other directly, and the problem of route jump management becomes a problem that must be solved.

ARouter is an open source Android routing framework of Ali, which is developed to solve the problem of route jump in componentized development.

A framework to help componentize Android Apps — support routing, communication, decoupling between modules

GitHub project address: ARouter

A brief introduction to the principle of routing framework

We take componentized development Activity jump as an example, a simple talk about the implementation principle of the routing framework. Regardless of how the upper framework encapsulates it, the low-level jump to an activity is always implemented through startActivity(), which requires obtaining an instance or path to the target activity. To achieve decoupling between modules without directly referencing the target Activity, the simplest way is to give the target Activity a simple alias and then maintain the relationship between the alias and the Activity through the mapping table Map

>. Then the implementation of the mapping table can only sink into the base module that all modules reference, such as base. The process is clear:
,>

The Activity injects the mapping into the Map in advance. When AActivity initiates a request to jump to B, the base module will look up the corresponding Activity instance in the mapping table and jump. If the corresponding Activity instance cannot be found, you can send back the jump result to avoid exceptions.

The use of ARouter

Add the dependent

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

dependencies {
    // Replace the latest version with the API
    // To be used with compiler, use the latest version to ensure compatibility
    compile 'com.alibaba:arouter-api:x.x.x'
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'. }// Old gradle plugin (< 2.2), you can use apt plugin. See 'other #4' at the end of this article.
// Kotlin configuration reference at end of article 'other #5'
Copy the code

The latest release (December 7, 2021) is version 1.5.2. The following is based on the release notes.

confusion

Add obfuscation rules (if using Proguard)

-keep public class com.alibaba.android.arouter.routes. * * {*; } -keeppublic class com.alibaba.android.arouter.facade. * * {*; } -keepclass * implements com.alibaba.android.arouter.facade.template.ISyringe{*; } # if byType is used to obtain Service, add the following rule to protect interface -keepinterface * implements com.alibaba.android.arouter.facade.template.IProviderIf single-class injection is used, no interface implementation is definedIProviderAdd the following rule to protect implementation # -keep class * implements com.alibaba.android.arouter.facade.template.IProvider
Copy the code

Initialize the

The official recommendation is to initialize it as early as possible and put it in the Application

if (isDebug()) {           
    // These two lines must be written before init, otherwise these configurations will not be valid during init
    ARouter.openLog();     // Prints logs
    ARouter.openDebug();   // Enable debug mode (if running in InstantRun mode, debug must be enabled! The online version needs to be closed, otherwise there is a security risk.
}
ARouter.init(mApplication); // As early as possible, it is recommended to initialize in Application

Copy the code

Note that you need to call the openDebug() method during development and debugging; otherwise, the Activity may not be found.

Add annotations

Add Router annotations to the target Activity/Fragment where path is the routing path

@Router(path = "/app/MainActivity")
public class MainActivity extends Activity{
  protected void onCreate(a){.../ / injection
      ARouter.getInstance().inject(this)}}Copy the code

The path must have a slash and at least two levels and needs to be injected in onCreate.

A routing

  • The Activity to jump

    ARouter.getInstance().build("/app/MainActivity").navigation();
    Copy the code
  • Fragment

     aFragment = (AFragment) ARouter.getInstance().build("/fragment/AFragment").navigation();
    Copy the code

The ginseng

  • Basic data types

    Pass parameters:

    ARouter.getInstance().build(path).withchar ()"CharKey".'a')
            .withShort("ShortKey".1)
            .withInt("IntKey".11)
            .withLong("LongKey".12l)
            .withFloat("FloatKey".1.1 f)
            .withDouble("DoubleKey".1.1 d)
            .withBoolean("BooleanKey".true)
            .withString("StringKey"."value")
            .navigation();
    Copy the code

    Parsing parameters with annotations:

    Note that the private keyword cannot be used.

    // The pseudo-code is as follows
    public class TestActivity extends Activity{
      // Annotate with Autowired
      @Autowired(name = "IntKey")
      int i;
      @Autowired(name = "StringKey")
      String str;
      / /... Omit the other
      
      onCreate(){
        ...
        Log.d("IntKey = " + i + " StringKey = "+ str); }}Copy the code

    During parameter passing, you can omit nameARouter to automatically match parameters based on type. However, you are advised to specify name to avoid exceptions.

  • Serialized object

    Delivery:

    ARouter.getInstance().build("Path")
            .withSerializable("SerializableKey".new TestObj())
            .withParcelable("ParcelableKey".new TestObj())
            .navigation();
    Copy the code

    Pass-through docking requires the Serializable and Parcelable interfaces to be implemented, respectively, invoking methods that are much the same as the basic data types, which is easy to understand.

    Resolution:

    Automatic parameter resolution is also done through annotations.

        // Support parsing custom objects, using JSON pass in URL
        @Autowired(name = "ParcelableKey")
        TestObj obj; 
    Copy the code

    It is also recommended to specify the name, although omitting the name is also supported.

  • Custom object

    Custom objects are passed only if the object does not implement the Serializable or Parcelable interfaces.

    Delivery:

    ARouter.getInstance().build("Path")
            .withObject("ObjectKey".new TestObjectBean())
            .navigation();
    Copy the code

    Resolution:

        @Autowired(name = "ObjectKey")
        TestObjectBean object;
    Copy the code

    In addition to the above two steps, passing a custom object must create a new class that implements SerializationService and annotates it with the @Route annotation.

    ARouter is designed to allow users to choose their own Json parsing methods. (Optionally specify a path that does not duplicate.)

    @Route(path = "/app/custom/json")
    public class JsonSerializationService implements SerializationService {
        Gson gson;
        @Override
        public <T> T json2Object(String input, Class<T> clazz) {
            return gson.fromJson(input,clazz);
        }
    
        @Override
        public String object2Json(Object instance) {
            return gson.toJson(instance);
        }
    
        @Override
        public <T> T parseObject(String input, Type clazz) {
            return gson.fromJson(input,clazz);
        }
    
        @Override
        public void init(Context context) {
            gson = newGson(); }}Copy the code

    In addition to the init method, the other methods handle json and object conversions.

    Because the entire custom object passing process goes through the following steps:

    • withObhect(“”, obj)
    • Call the 2JSON method of SerializationService to convert obj to a STRING JSON object.
    • Pass the JSON object of string to the target page.
    • The target page invocation calls the 2Object method of SerializationService to convert the JSON object into an OBJ object.

    You can also see the conversion steps in the withObject source code:

    
        /**
         * Set object value, the value will be convert to string by 'Fastjson'
         *
         * @param key   a String, or null
         * @param value a Object, or null
         * @return current
         */
        public Postcard withObject(@Nullable String key, @Nullable Object value) {
            serializationService = ARouter.getInstance().navigation(SerializationService.class);
            mBundle.putString(key, serializationService.object2Json(value));
            return this;
        }
    Copy the code

    Note that since the parameter resolution process is handled automatically by type matching, the List and Map objects passed with withObject() cannot be received with a List and Map implementation class that implements Serializable (ArrayList, HashMap, etc.). The object that receives withObject arguments cannot be an implementation class of Serializable or Parcelable. (Specifying an implementation class for Serializable or Parcelable affects the serialization type.)

Routing management

The jump process above inevitably need to specify a lot of routing path (such as Activity path, etc.), in order to facilitate the management and processing, usually define a constant to maintain and manage all routing path, and for each module can be normal reference needs to be constant class down to the base module (such as BaseModule), Although this breaks the independence of each module to some extent (you must rely on constant-class modules for routing hops) and increases the coupling between the business module and the base module, it is still necessary for code maintenance.

/** * Route management */
public class ARouterPath {
    /** push module */
    public static final String SERVICE_PUSH_RECEIVER_URL = "/push/PushRegisterActivity";

    /** Display Module */
    public static final String ACTIVITY_DISPLAY_DISPLAY_URL = "/display/DisplayActivity";
    public static final String SERVICE_DISPLAY_UPLOAD_URL = "/display/PushReceiver";
}
Copy the code

The interceptor

As mentioned earlier in the introduction to OkHttp, the OkHttp interceptor design is a particularly classic and exciting part of the framework’s implementation. Also added in ARouter is the implementation of an interceptor that intercepts and handles routing processes.

@interceptor (priority = 8, name = "test Interceptor ")
public class TestInterceptor implements IInterceptor {
    private static final String TAG = "Interceptor";

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        String threadName = Thread.currentThread().getName();
        Log.i(TAG, "Interceptor starts execution, thread name:" + threadName +
                "\n postcard = " + postcard.toString());

        // Intercept the route operation
        callback.onInterrupt(new RuntimeException("There is an exception. No route."));
        // Continue routing operations
        callback.onContinue(postcard);
    }

    @Override
    public void init(Context context) {
        Log.i(TAG, "TestInterceptor interceptor initialization"); }}Copy the code

Interceptors can be added to multiple interceptors. The @interceptor () annotation is used to register interceptors. Priority is used to define the priority of interceptors.

  • Multiple interceptors cannot have the same priority.
  • withnameProperty specifies a name for the interceptor (which can be omitted).
  • init()Method is called automatically when the interceptor is initialized.
  • process()When a routing operation is initiated, the route can pass as requiredonInterrupt()Intercept the route, or pass itonContinue()To continue the routing operation, note that either method must be invoked; otherwise, the route will be lost and the operation will not continue.

The interceptor is used to judge login events in classic applications. Some pages in the app can only be redirected after the user logs in. In this way, the interceptor can be used for login check to avoid repeated checks on the target page.

Jump result monitor processing:

ARouter.getInstance().build("Path")
    .navigation(this.new NavigationCallback() {
        @Override
        public void onFound(Postcard postcard) {
            // Route discovery
        }

        @Override
        public void onLost(Postcard postcard) {
						// Route lost
        }

        @Override
        public void onArrival(Postcard postcard) {
        		/ / to
        }

        @Override
        public void onInterrupt(Postcard postcard) {
						/ / intercept}});Copy the code

The NavigationCallback is specified to listen for jumps. You can also simplify this by specifying the abstract class NavCallback if you only want to listen for incoming events.

Note that only activities trigger interceptors, fragments and iProviders do not support interception.

Looking at the source code, you can see that interception is handled in the navigation() method of _ARouter.

if(! postcard.isGreenChannel()) {// It must be run in async thread, maybe interceptor cost too mush time made ANR.
  // Handle interceptors
	interceptorService.doInterceptions(postcard, new InterceptorCallback() {
		/**
		* Continue process
		*
		* @param postcard route meta
		*/
		@Override
		public void onContinue(Postcard postcard) {
			_navigation(postcard, requestCode, callback);
		}
		/ / to omit
	}
Copy the code

Whether it is intercepted depends on the postcars.isgreenChannel () value, which is assigned in the Completion () method of LogisticsCenter:

            switch (routeMeta.getType()) {
                case PROVIDER:  // if the route is provider, should find its instance
                    // Its provider, so it must implement IProvider
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            logger.error(TAG, "Init provider failed!", e);
                            throw new HandlerException("Init provider failed!");
                        }
                    }
                    postcard.setProvider(instance);
                		// The Provider does not need interception
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                		//Fragment does not need to be intercepted
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
Copy the code

Routemeta.gettype () is clearly the route type, although ARouter defines a number of types:

/**
 * Type of route enum.
 *
 * @author Alex <a href="mailto:[email protected]">Contact me.</a>
 * @version 1.0
 * @since16/8/23 22:33 * /
public enum RouteType {
    ACTIVITY(0."android.app.Activity"),
    SERVICE(1."android.app.Service"),
    PROVIDER(2."com.alibaba.android.arouter.facade.template.IProvider"),
    CONTENT_PROVIDER(-1."android.app.ContentProvider"),
    BOARDCAST(-1.""),
    METHOD(-1.""),
    FRAGMENT(-1."android.app.Fragment"),
    UNKNOWN(-1."Unknown route type");
  / / to omit
}
Copy the code

RouteType is defined as Service, ContentProvider, and Boardcast, but ARouter only supports route jumps: RouteType is defined as Service, ContentProvider, and Boardcast. There are three types of Activity, Fragment, and IProvider. The other types are not supported (based on version 1.5.2). IProvider is ARouter’s service component and will be mentioned later.

grouping

Above mentioned can log in by interceptors, and intercepting page, but we need if we do in the heart of the interceptor judging all need to jump after the login page, this problem can be simplified and grouping function, will all need to jump after the login page to specify the same group, you just need to judge grouping in the interceptor.

// Specify a group using the group keyword
@Route(path = "/app/MainActivity", group = "app")
public class MainActivity extends AppCompatActivity {
		/ / to omit
}

// Intercept judgment
@interceptor (priority = 8, name = "test Interceptor ")
public class TestInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
      	// Get the grouping
        String group = postcard.getGroup();
      	/ / to omit
    }
  / / to omit
}
Copy the code

Note that if you want to jump to the target page of the group specified by the group keyword, you need to display the specified group; otherwise, the corresponding route cannot be found.

ARouter.getInstance().build("/app/TextActivity"."test").navigation();
Copy the code

Of course, if the grouping is not specified by group, the first/after the path is used as the group by default, which is why the path must have at least two levels of //.

Declare more information for the target page

// We often need to configure some properties in the target page, such as "need to log in" or not"
// This can be extended by the Extras attribute in the Route annotation, which is an int value. In other words, a single int has 4 bytes, or 32 bits, and can be configured with 32 switches
// The rest can be left to your own devices. 32 switches can be identified by byte manipulation, and some attributes of the target page can be identified by the switch, which can be retrieved in the interceptor for business logic judgment
@Route(path = "/test/activity", extras = Consts.XXXX)
Copy the code

In simple terms, can be specified as the target page simple attributes, such as the official said the login management functions, but it is not all pages need to log in, of course, we can through the way of grouping above, will need to log in to determine the login page is divided into unified group, and then judge grouping in the interceptor, according to the current state of login to intercept.

You can also set the extrAS to 1 for pages that require login verification, and 2 or no for pages that do not require login verification.

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        String threadName = Thread.currentThread().getName();
        Log.i(TAG, "Interceptor starts execution, thread name:" + threadName +
                "\n postcard = " + postcard.toString());

        // Simulate the login state
        boolean isLogin = false;
        int extras = postcard.getExtra();
      	// Login verification is required, and unlogged pages are blocked
        if(extras == 1 && !isLogin){
            callback.onInterrupt(new RuntimeException("Please log in first."));
        } else{ callback.onContinue(postcard); }}Copy the code

An int extras has 4 bytes (32 bits) and can be configured with 32 switches.

startActivityForResult

ARouter also supports startActivityForResult() to get the return value of the target page.

// Specify a RequestCode,123, to redirect a route
ARouter.getInstance().build("/AModule/AModuleActivity").navigation(this.123);
Copy the code

The target page needs to return ResultCode and data using the setResult method upon exit

// Target page implementation, omit other code
    @Override
    public void onBackPressed(a) {
        Intent data = new Intent();
        data.putExtra("name"."I am AModuleActivity");
        setResult(321, data);
        super.onBackPressed();
    }
Copy the code

The onActivityResult () method needs to be overridden on the jump page. The whole process is basically the same as startActivityForResult().

    // jump page implementation, other code omitted
		@Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == 123 && resultCode == 321){
            String name = data.hasExtra("name")? data.getStringExtra("name") : "";
            Log.d(TAG, "onActivityResult: name = "+ name); }}Copy the code

Dynamically Adding routes

Routing for all the pages mentioned above is added automatically by the ARouter framework when the code is compiled via the annotation keyword @route and injection method Inject (). ARouter also supports the addition of dynamic routes.

    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,  // Target Class
                    "/dynamic/activity".// Path
                    "dynamic".// Group, try to keep the same as the first paragraph of path
                    0.// Priority, not currently used
                    0                           // Extra, for page marking)); }});Copy the code

This method applies to some plug-in development scenarios. The jump process after adding routes is the same as before.

service

The IProvider service component is defined by implementing the IProvider interface to meet the invocation requirements of other components. In plain English, AModule exposes functionality through services that other Modules can call.

  • Implement the IProvider interface

    Define the ARouter service through the IProvider interface.

    // Declare the interface through which other components invoke the service
    public interface HelloService extends IProvider {
        String sayHello(String name);
    }
    
    // Implement the interface
    @ the Route (path = "/ yourservicegroupname/hello," name = "test services")
    public class HelloServiceImpl implements HelloService {
    
        @Override
        public String sayHello(String name) {
        return "hello, " + name;
        }
    
        @Override
        public void init(Context context) {}}Copy the code

    In componentized development, it is common to define an interface class HelloService in the underlying module (for example, in the Base module) and implement the interface functions in the concrete module.

    Note: the init method is triggered only when the service is in use.

  • The discovery service

    • Dependency injection

      public class Test {
          @Autowired
          HelloService helloService;
      
          @Autowired(name = "/yourservicegroupname/hello")
          HelloService helloService2;
        
        	public void fun(a){
            // You can use the service directly
            // 1. (recommended) Use dependency injection to discover services. Annotate fields and use them
          // The Autowired annotation will use byName to inject the corresponding field, without setting the name attribute. By default, it will use byType to discover the service (when multiple implementations of the same interface must use byName).
            helloService.sayHello("1");
            helloService2.sayHello("2"); }}Copy the code
    • Rely on to find

      public class Test {
          @Autowired
          HelloService helloService;
      
          @Autowired(name = "/yourservicegroupname/hello")
          HelloService helloService2;
        
        	public void fun(a){
          // 2. Use the dependency lookup method to discover and use the service. The following two methods are byName and byType respectively
          helloService3 = ARouter.getInstance().navigation(HelloService.class);
          helloService4 = (HelloService) ARouter.getInstance().build("/yourservicegroupname/hello").navigation();
          helloService3.sayHello("Vergil");
          helloService4.sayHello("Vergil"); }}Copy the code
    • Pre-processing service

      // Implement the PretreatmentService interface with an annotation of the Path content
      @Route(path = "/xxx/xxx")
      public class PretreatmentServiceImpl implements PretreatmentService {
          @Override
          public boolean onPretreatment(Context context, Postcard postcard) {
              // Preprocess the jump. If you need to process the jump yourself, return false
          }
      
          @Override
          public void init(Context context) {}}Copy the code

summary

We’ve basically covered all the features and usage of ARouter. Similar routing frameworks exist

  • Meituan: WMRouter WMRouter
  • DRouter DRouter

If you’re interested, check it out, but at the moment ARouter is used more often.