
前篇回顧
鏈接:Android組件化 —— 基礎(chǔ)(一) - 組件化與集成化
上篇文章,我們了解了:
- 組件化與集成化的區(qū)別;
- 通過gradle自動(dòng)轉(zhuǎn)換組件環(huán)境和集成環(huán)境;
- 解決AndroidManifest.xml共用問題。
本篇,我們將探討組件化架構(gòu)中組件間通訊是如何完成的。
Activity跳轉(zhuǎn)
我們知道,正常的Activity跳轉(zhuǎn)代碼大致如下:
val intent = Intent(this, UserMainActivity::class.java)
startActivity(intent)
然而,在組件環(huán)境下,各模塊相互獨(dú)立,它們之間不存在任何依賴關(guān)系,導(dǎo)致模塊之間Activity互不可見的,代碼在編譯期無法識別到對方的Activty,頁面的跳轉(zhuǎn)就成了問題。

其實(shí)我們知道,最終無論是哪個(gè)模塊的Activity最終都會(huì)打包到同一個(gè)apk中,在代碼文件層面上講,這些Class文件是相互可見的,這就需要我們繞點(diǎn)彎路將這些Activity提供給對方。設(shè)想一下,能不能找一個(gè)中間人,把需要跳轉(zhuǎn)的Activity都交給它來管理?
我大致想到一個(gè)方案:
- 1、創(chuàng)建一個(gè)公共模塊lib_comm,該模塊用于存放各模塊間公共代碼或者說基礎(chǔ)功能代碼,并且其它模塊都依賴于該Library;
- 2、在lib_comm中創(chuàng)建一個(gè)“路由”容器管理類,該類向外提供路由的注冊、查詢等功能;
- 3、各模塊將需要外部跳轉(zhuǎn)的Activity,注冊到路由容器中;
- 4、跳轉(zhuǎn)時(shí),由路由容器去查詢路由,完成跳轉(zhuǎn)。
什么是路由?前面提到,如果要跨模塊跳轉(zhuǎn)Activity,我們需要將這些Activity的Class對象提供到對方模塊,為了方便管理和調(diào)用,我們用一個(gè)簡化的字符串來標(biāo)識對應(yīng)的Activity.class對象,例如:"A" -> AActivty.class,這種通過字符串查詢到指定頁面的方案,可以稱之為路由。
按照上面思路,我把相關(guān)實(shí)現(xiàn)代碼貼在了下方:
- 創(chuàng)建lib_comm公共模塊,以及路由管理類RouterManager

/**
* 路由管理類
* 提供路由注冊、查詢等功能
* */
object RouterManager {
const val TAG = "RouterManager"
// 存儲(chǔ)路由的的容器
private val mRouterMap = HashMap<String, Class<*>>()
/**
* 添加路由
* @param path 路由路徑
* @param clazz 路由目標(biāo)
* */
fun addRouter(path: String, clazz: Class<*>) {
mRouterMap[path] = clazz
}
/**
* 開啟Activity
* */
fun startActivity(context: Context, path: String) {
val clazz = mRouterMap[path]
if (clazz == null) {
val log = "not found router by path !"
Log.e(TAG, log)
showToast(context , log)
return
}
// 判斷是否是Activity的子類
if (Activity::class.java.isAssignableFrom(clazz)) {
val intent = Intent(context, clazz)
context.startActivity(intent)
} else {
val log = "router's not Activity !"
Log.e(TAG, log)
showToast(context , log)
}
}
private fun showToast(context:Context , log: String) {
Toast.makeText(context , log , Toast.LENGTH_SHORT).show()
}
}
- 各模塊依賴lib_comm,并在startup中完成路由注冊
// 各模塊build.gradle中添加依賴
implementation project(":lib_comm")
// 以user模塊為例,通過startup完成路由注冊
class UserInitializer : Initializer<UserInit> {
override fun create(context: Context): UserInit {
UserInit.init(context)
return UserInit
}
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}
}
// user模塊初始化入口
object UserInit {
fun init(context: Context) {
initRouter()
}
private fun initRouter() {
// 注冊路由
RouterManager.addRouter("user/UserMainActivity", UserMainActivity::class.java)
}
}
- 最終各模塊使用路由完成跳轉(zhuǎn)
class AppMainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btn_user).setOnClickListener {
// 通過路由跳轉(zhuǎn)
RouterManager.startActivity(this, "user/UserMainActivity")
}
}
}

(諾GIF圖加載失敗,可點(diǎn)擊此處查看)
Fragment獲取
實(shí)現(xiàn)方案與Activity類似,將Fragment對應(yīng)的路由存儲(chǔ)到路由容器中,再暴露一個(gè)獲取Fragment API即可。
// 存儲(chǔ)到路由容器中
RouterManager.addRouter("user/UserFragment", UserFragment::class.java)
// 暴露獲取Fragment的API
object RouterManager {
...
/**
* 獲取Fragment
* */
fun getFragment(context: Context, path: String): Fragment? {
val clazz = mRouterMap[path]
if (clazz == null) {
val log = "not found router by path !"
Log.e(TAG, log)
showToast(context, log)
return null
}
// 判斷是否是Fragment的子類
if (Fragment::class.java.isAssignableFrom(clazz)) {
return clazz.newInstance() as Fragment
} else {
val log = "router's not Fragment !"
Log.e(TAG, log)
showToast(context, log)
}
return null
}
...
}
// 獲取Fragment ,并使用
RouterManager.getFragment(this, "user/UserFragment")?.apply {
val beginTransaction = supportFragmentManager.beginTransaction()
beginTransaction.replace(R.id.fl_fragment, this)
beginTransaction.commit()
}

(諾GIF圖加載失敗,可點(diǎn)擊此處查看)
跳轉(zhuǎn)攜帶參數(shù)
頁面跳轉(zhuǎn)過程中需要攜帶的參數(shù),可以通過Bundle對象進(jìn)行傳遞,代碼實(shí)現(xiàn)如下,此處就不做過多闡述:
object RouterManager {
/**
* 開啟Activity
* */
fun startActivity(context: Context, path: String, bundle: Bundle? = null) {
val clazz = mRouterMap[path]
if (clazz == null) {
val log = "not found router by path !"
Log.e(TAG, log)
showToast(context, log)
return
}
// 判斷是否是Activity的子類
if (Activity::class.java.isAssignableFrom(clazz)) {
val intent = Intent(context, clazz)
// 添加參數(shù)
if (bundle != null) {
intent.putExtras(bundle)
}
context.startActivity(intent)
} else {
val log = "router's not Activity !"
Log.e(TAG, log)
showToast(context, log)
}
}
/**
* 獲取Fragment
* */
fun getFragment(context: Context, path: String, bundle: Bundle? = null): Fragment? {
val clazz = mRouterMap[path]
if (clazz == null) {
val log = "not found router by path !"
Log.e(TAG, log)
showToast(context, log)
return null
}
// 判斷是否是Fragment的子類
if (Fragment::class.java.isAssignableFrom(clazz)) {
val fragment = clazz.newInstance() as Fragment
//添加參數(shù)
if (bundle != null) {
fragment.arguments = bundle
}
return fragment
} else {
val log = "router's not Fragment !"
Log.e(TAG, log)
showToast(context, log)
}
return null
}
}
跨模塊功能調(diào)用
實(shí)際開發(fā)中,部分模塊的功能可能需要提供給別的模塊使用。
例如:user模塊在用戶登錄后會(huì)保存用戶登錄狀態(tài),其他模塊部分業(yè)務(wù)可能需要校驗(yàn)該狀態(tài)才可繼續(xù)操作。
顯然,是否登錄功能需要暴露給其它模塊使用,如果直接將用戶是否登錄的業(yè)務(wù)代碼(校驗(yàn)token等)丟到lib_comm中,雖然可以解決問題,但我們知道這樣的代碼實(shí)際是屬于用戶業(yè)務(wù)線的,我們定義lib_comm的初衷是希望它存放公共代碼,并且業(yè)務(wù)線盡量少甚至不去修改這部分代碼,一旦用戶業(yè)務(wù)線的校驗(yàn)規(guī)則發(fā)生改變,那么業(yè)務(wù)線去修改lib_comm就不可避免。
仔細(xì)想想,我們只需將業(yè)務(wù)線需要暴露的功能,以接口的形式提供給lib_comm,別的模塊再通過這些接口來訪問對應(yīng)的功能即可,具體功能的實(shí)現(xiàn)還是保留在各自業(yè)務(wù)線模塊里,這樣就可避免業(yè)務(wù)線代碼入侵問題,詳細(xì)實(shí)現(xiàn)可以參考下方代碼:
- user模塊的用戶是否登錄業(yè)務(wù)代碼
object UserUtils {
/**
* 用戶模塊是否登錄
* 實(shí)際業(yè)務(wù)代碼
* */
fun isLogin(): Boolean {
// 校驗(yàn)Token之類的業(yè)務(wù)邏輯
// ...
// ...
return true
}
}
- lib_comm模塊
- 定義表示功能標(biāo)記接口
- 以及user模塊提供的功能接口
- 并在RouterManager中提供對外獲取接口實(shí)例的方法
/**
* 功能性路由標(biāo)記接口
* */
interface IService
/**
* User模塊對外提供的功能接口
* */
interface IUserService : IService {
/**
* 是否登錄
*/
fun isLogin(): Boolean
}
object RouterManager {
...
/**
* 獲取用戶模塊提供的服務(wù)
* */
fun getUserService(context: Context): IUserService? {
val clazz = mRouterMap["user/UserService"]
if (clazz == null) {
val log = "not found service router by path !"
Log.e(TAG, log)
showToast(context, log)
return null
}
// 判斷是否是Service & IUserService
if (IService::class.java.isAssignableFrom(clazz)
&& IUserService::class.java.isAssignableFrom(clazz)
) {
return clazz.newInstance() as IUserService
} else {
val log = "router's not IUserService !"
Log.e(TAG, log)
showToast(context, log)
}
return null
}
...
}
- user模塊實(shí)現(xiàn)IUserService接口,并注冊路由
/**
* user模塊實(shí)現(xiàn)對外暴露的功能
* */
class UserServiceImpl : IUserService {
override fun isLogin(): Boolean {
return UserUtils.isLogin()
}
}
// 注冊功能到路由中
RouterManager.addRouter("user/UserService", UserServiceImpl::class.java)
- app模塊調(diào)用user模塊暴露的isLogin()功能
RouterManager.getUserService(this)?.apply {
Toast.makeText(
this@AppMainActivity,
"登錄狀態(tài):${this.isLogin()}",
Toast.LENGTH_SHORT
).show()
}

(諾GIF圖加載失敗,可點(diǎn)擊此處查看)
關(guān)于lib_comm的修改問題
雖然通過上面的方案,我們將業(yè)務(wù)模塊對外提供的功能解耦到了自身模塊里,但不得不在lib_comm中對外提供對應(yīng)的IService子接口,一旦子業(yè)務(wù)線需要對外提供新的功能,或者刪除舊的功能,那么在lib_comm修改IService子接口就在所難免。
顯然IService子接口會(huì)隨著業(yè)務(wù)線的變動(dòng)發(fā)生修改,我們只是做到了盡量少的修改lib_comm代碼;在后續(xù)篇章中,我會(huì)提供一種方案來解決該問題,該問題先暫時(shí)保留下來。
小結(jié)
本篇就先到這里,我們主要了解了組件化架構(gòu)中Activity的跳轉(zhuǎn)、Fragment的獲取、以及跨模塊功能調(diào)用等開發(fā)中常會(huì)遇到的場景案例,并嘗試手寫代碼來解決這些問題。如果你一步步完成了這些功能,恭喜你,你已經(jīng)對“路由”的具體實(shí)現(xiàn)有了基本的認(rèn)識。
我們編寫的路由處理框架還存在很多問題,例如:
- 我們在startup中注冊路由,這就會(huì)導(dǎo)致在App啟動(dòng)時(shí)會(huì)將所有模塊的路由全部注冊到內(nèi)存中,然而部分路由在用戶的實(shí)際使用中可能未被使用,這就導(dǎo)致額外內(nèi)存開銷;
- 對于功能性的Service,每次都重新創(chuàng)建新的對象給調(diào)用者,這塊也可以進(jìn)行緩存優(yōu)化;
- ...
路由中可能涉及到的其它功能,我們也沒做具體的實(shí)現(xiàn),但只要通過本篇對路由核心實(shí)現(xiàn)有了清晰認(rèn)識也收獲足以。市面上路由已經(jīng)有了很多成熟框架,例如美團(tuán)的WMRouter,阿里的ARouter等,如果項(xiàng)目對代碼的自研要求不高,使用這些框架來實(shí)現(xiàn)路由無論是在性能上,還是效率上都再好不過。
下篇,以ARouter為例繼續(xù)探討路由那些事。