ARouter Design Philosophy and Practical Guide
As an Android application grows, teams inevitably face a critical architectural decision: Componentization (Modularization). Breaking down a monolithic app into independent business modules is the only way to maintain development efficiency and code health in large projects. However, componentization introduces an immediate problem: How do isolated modules communicate with each other?
Traditional Android navigation relies on Intent, which requires the caller to directly reference the target Activity's Class object. If ModuleA wants to navigate to a page in ModuleB, it must declare a Gradle dependency on ModuleB. As modules multiply, these dependencies degrade into an entangled web, forcing the architecture back into "pseudo-componentization".
ARouter, open-sourced by Alibaba, was created specifically to solve this issue. Its core mission is: To replace "Class References" with "URL-like Paths", entirely severing compile-time dependencies between modules.
This article details ARouter's design philosophy, its core APIs, and its practical application patterns in production projects.
I. Why Do We Need a Routing Framework?
1.1 The Componentization Dilemma
In an ideal componentized architecture, business modules are isolated at compile-time—ModuleA's code cannot see any classes inside ModuleB. This means the following standard navigation code will fail to compile:
// Code inside ModuleA - Compile Error! ModuleA cannot see OrderDetailActivity
Intent intent = new Intent(this, OrderDetailActivity.class);
startActivity(intent);
| Traditional Solution | Implementation | Fatal Flaw |
|---|---|---|
| Implicit Intent | Matching via action strings |
No compile-time safety. Typos crash at runtime. Cannot pass complex custom objects easily. |
| Reflection | Class.forName("com.xxx.OrderDetailActivity") |
Poor performance. Highly fragile during refactoring (obfuscation breaks strings). |
| Global Constant Module | Registering all targets in a shared module | The shared module bloats into a "God Module", requiring modification for every new page. |
The fundamental issue is: They either introduce runtime fragility or destroy component boundaries.
1.2 The Intermediary Pattern
ARouter's design philosophy can be understood through an analogy:
Imagine a large enterprise as an office building. Traditional intents are like "every department memorizing everyone else's room number and knocking directly." As the company grows, this fails. ARouter acts as a Central Front Desk (Routing Table) in the lobby. When a department moves in, it registers its path and room number. If someone wants to visit a department, they just tell the desk the path (e.g., "/order/detail"), and the desk routes them. The caller doesn't need to know the target's physical location.
┌───────────┐ ┌──────────────────┐ ┌───────────┐
│ ModuleA │ "/order/detail" │ ARouter Table │ Lookup Target │ ModuleB │
│ (Caller) │ ──────────────────→ │ │ ──────────────────→ │ (Target) │
│ │ No B references │ path -> Class │ Builds Intent │ │
└───────────┘ └──────────────────┘ └───────────┘
▲
│ Auto-registered at compile-time
@Route(path="/order/detail")
This model provides three fundamental advantages:
- Compile-Time Decoupling: Callers only depend on a string path.
- Centralized Management: All routing maps are queryable and maintainable.
- Extensibility: Interceptors (for login checks, A/B testing) can be easily inserted into the routing flow.
II. Configuration and Initialization
2.1 Gradle Dependencies
ARouter consists of three artifacts, each with a specific role:
| Artifact | Role | Dependency |
|---|---|---|
arouter-api |
Runtime API (build(), navigation()) |
implementation |
arouter-compiler |
Annotation Processor (scans @Route and generates maps) |
kapt / ksp / annotationProcessor |
arouter-register |
Gradle Plugin (Auto-registers via bytecode weaving) | classpath |
Standard Configuration (Kotlin + KAPT):
// Project root build.gradle
buildscript {
dependencies {
// Optional but highly recommended: Auto-register plugin to avoid runtime Dex scanning
classpath 'com.alibaba:arouter-register:1.0.2'
}
}
// Inside every module's build.gradle that uses ARouter
plugins {
id 'kotlin-kapt'
}
// If using the arouter-register plugin
apply plugin: 'com.alibaba.arouter'
android {
defaultConfig {
kapt {
arguments {
// Crucial: Defines a unique namespace for the APT generated classes
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
}
dependencies {
implementation 'com.alibaba:arouter-api:1.5.2'
kapt 'com.alibaba:arouter-compiler:1.5.2'
}
2.2 SDK Initialization
Initialize early in Application.onCreate():
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug() // Required for InstantRun/HotSwap
}
// Initialize ARouter
// Loads compile-time generated maps into memory
ARouter.init(this)
}
}
III. Core API: Page Routing
3.1 Marking Targets (@Route)
Add the @Route annotation to target Activities:
/**
* Path must contain at least two segments (/group/name).
* The first segment is automatically used as the 'group'.
*/
@Route(path = "/order/detail")
class OrderDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_order_detail)
}
}
3.2 Initiating Navigation
Trigger navigation from anywhere using the path string:
// Basic navigation
ARouter.getInstance()
.build("/order/detail") // Builds a 'Postcard'
.navigation() // Executes the route
// Passing parameters
ARouter.getInstance()
.build("/order/detail")
.withString("orderId", "20240101001")
.withInt("source", 1)
.withParcelable("orderInfo", orderParcelable)
.navigation()
// URI Navigation (Deep Links)
val uri = Uri.parse("arouter://m.example.com/order/detail?orderId=123")
ARouter.getInstance()
.build(uri)
.navigation()
Why Builder Pattern? build() returns a Postcard object. This allows chaining parameters, custom animations, and Intent flags before the final navigation() call "fires" the request.
3.3 Fetching Fragments
ARouter can also route to Fragments, which is incredibly useful when a Host Activity needs to embed a Fragment from an isolated Module.
// In target module
@Route(path = "/user/profile")
class UserProfileFragment : Fragment() { ... }
// In Host module
val fragment = ARouter.getInstance()
.build("/user/profile")
.withString("userId", "12345")
.navigation() as Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commit()
IV. Auto-Injection (@Autowired)
4.1 Goodbye Manual Intents
Traditionally, you must manually extract data from Intent.extras. ARouter automates this:
@Route(path = "/order/detail")
class OrderDetailActivity : AppCompatActivity() {
// Auto-maps the "orderId" string from the Intent
@Autowired
@JvmField
var orderId: String = ""
// Maps custom parameter name
@Autowired(name = "source")
@JvmField
var fromSource: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Crucial: Triggers the injection logic
ARouter.getInstance().inject(this)
Log.d("OrderDetail", "Order: $orderId, Source: $fromSource")
}
}
4.2 Passing Custom Objects (JSON Serialization)
To pass non-Parcelable objects via URL or standard routing, provide a SerializationService:
@Route(path = "/service/json")
class JsonSerializationService : SerializationService {
private lateinit var gson: Gson
override fun init(context: Context) { gson = Gson() }
override fun <T> json2Object(input: String, clazz: Type): T = gson.fromJson(input, clazz)
override fun object2Json(instance: Any): String = gson.toJson(instance)
override fun <T> parseObject(input: String, clazz: Type): T = gson.fromJson(input, clazz)
}
V. Interceptors: Routing AOP
5.1 The Need for Interceptors
Before navigating to an order page, you often need to check if the user is logged in. Doing this in every Activity leads to boilerplate. ARouter's Interceptors provide an AOP (Aspect-Oriented Programming) solution.
5.2 Defining an Interceptor
/**
* Lower priority executes first.
*/
@Interceptor(priority = 8, name = "Login Interceptor")
class LoginInterceptor : IInterceptor {
override fun init(context: Context) {
// Called once during ARouter.init()
}
override fun process(postcard: Postcard, callback: InterceptorCallback) {
// Read custom extra flags defined in @Route
if (postcard.extra and 0x01 != 0) {
if (UserManager.isLoggedIn()) {
callback.onContinue(postcard) // Allow
} else {
// Reroute to login
ARouter.getInstance()
.build("/user/login")
.withString("redirect", postcard.path)
.navigation()
callback.onInterrupt(RuntimeException("Not Logged In")) // Block
}
} else {
callback.onContinue(postcard)
}
}
}
5.3 Green Channel (Bypassing Interceptors)
Sometimes (like routing to the Login page itself), you must skip interceptors:
ARouter.getInstance()
.build("/user/login")
.greenChannel()
.navigation()
VI. Cross-Module Service Discovery (IProvider)
6.1 The Problem
Modules don't just need to launch Activities; they need to call methods on each other. E.g., ModuleOrder needs getUserInfo() from ModuleUser.
6.2 The Solution
1. Public Interface (in Base Module)
interface IUserService : IProvider {
fun isLoggedIn(): Boolean
}
2. Implementation (in User Module)
@Route(path = "/user/service")
class UserServiceImpl : IUserService {
override fun init(context: Context) {}
override fun isLoggedIn(): Boolean = TokenManager.hasValidToken()
}
3. Usage (in Order Module)
// Option 1: Injection
@Autowired
@JvmField
var userService: IUserService? = null
// Option 2: Direct lookup by Type
val service = ARouter.getInstance().navigation(IUserService::class.java)
// Option 3: Lookup by Name
val service = ARouter.getInstance().build("/user/service").navigation() as IUserService
VII. Degradation Strategy
If a route target is deleted or misspelled, ARouter handles the failure gracefully.
7.1 Global Degradation (DegradeService)
Catch all failed routes (e.g., to show a 404 page):
@Route(path = "/service/degrade")
class GlobalDegradeService : DegradeService {
override fun init(context: Context) {}
override fun onLost(context: Context, postcard: Postcard) {
ARouter.getInstance()
.build("/common/error")
.withString("lost_path", postcard.path)
.greenChannel()
.navigation(context)
}
}
VIII. ARouter vs. Jetpack Navigation
| Feature | ARouter | Jetpack Navigation |
|---|---|---|
| Primary Goal | Cross-module decoupling in componentized architectures | Single-module UI flow and back-stack management |
| Routing Protocol | String Paths | XML Graphs + Safe Args |
| Compile-Time Safety | ❌ No strict validation of string paths | ✅ Generates Safe Args classes |
| Global Interceptors | ✅ First-class citizen | ⚠️ Requires manual NavController listeners |
| Service Discovery | ✅ IProvider support | ❌ Not supported |
Conclusion: They are not mutually exclusive. Large Android projects frequently use ARouter for cross-module jumping and Jetpack Navigation for intra-module page flows.
In the next article, we will dive into ARouter's source code, exploring how APT builds the routing tables, the inner workings of LogisticsCenter, and the magic of ASM bytecode weaving in the arouter-register plugin.