以前一直據說過組件化開發,並且面試也有這方面的提問,但都不曾有涉及具體的項目。因此就萌生了基於Github的開放Api,並使用組件化的方式來從零搭建一個Github客戶端,起名爲AwesomeGithub。html
在這裏對組件化開發進行一個總結,同時也但願可以幫助別人更好的理解組件化開發。java
先來看下項目的總體效果android
下面是項目的結構git
以上問題隨着項目的迭代週期的增大,會表現的愈來愈明顯。那麼使用組件化又可以解決什麼問題了?程序員
如今咱們已經瞭解了組件化的做用,但要實現組件化,達到其做用,必須解決實現組件化過程當中所遇到的問題。github
以上是實現組件化時所遇到的問題,下面我會結合AwesomeGithub來具體說明解決方案。web
組件的建立,能夠直接使用library的方式進行建立。只不過在建立完以後,要讓組件達到能夠單獨運行調試的地步,還須要進行相關配置。面試
首先,當建立完library時,在build.gradle中能夠找到這麼一行代碼緩存
apply plugin: 'com.android.library'
複製代碼
這是gradle插件所支持的一種構建類型,表明能夠將其依賴到主項目中,構建後輸出aar包。這種方式對於咱們將組件依賴到主項目中徹底吻合的。bash
而gradle插件的另外一種構建方式,能夠在主項目的build.gradle中看到這麼一行代碼
apply plugin: 'com.android.application'
複製代碼
這表明在項目構建後會輸出apk安裝包,是一個獨立可運行的項目。
明白了gradle的這兩種構建方式以後,咱們接下須要作的事也很是明瞭:須要將這兩種方式進行動態配置,保證組件在主項目中以library方式存在,而本身單獨的時候,則以application的方式存在。
下面我以AwesomeGithub中的login組件爲例。
首先咱們在根項目的gradle.properties中添加addLogin變量
addLogin = true
複製代碼
而後在login中的build.gradle經過addLogin變量來控制構建方式
if (addLogin.toBoolean()) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
複製代碼
這樣就實現了對login的構建控制,可單獨運行,也可依賴於app項目。
除了修改gradle的構建方式,還須要動態配置ApplicationId與AndroidManifest文件。
有了上面的基礎,實現方式也很簡單。
能夠在defaultConfig中增長對applicationId的動態配置
defaultConfig {
if (!addLogin.toBoolean()) {
applicationId "com.idisfkj.awesome.login"
}
minSdkVersion Versions.min_sdk
targetSdkVersion Versions.target_sdk
versionCode Versions.version_code
versionName Versions.version_name
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
複製代碼
而AndroidManifest文件能夠經過sourceSets來配置
sourceSets {
main {
if (addLogin.toBoolean()) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
}
}
}
複製代碼
同時addLogin也能夠做用於app,讓login組件可配置依賴
這樣login組件就能夠獨立於app進行單獨構建、打包、調試與運行。
因爲組件與組件、項目間是不能直接使用類的相互引用來進行數據的傳遞,因此爲了解決這個問題,這裏經過一個公共庫來作它們之間調用的橋樑,它們不直接拿到具體的引用對象,而是經過接口的方式來獲取所須要的數據。
在AwesomeGithub中我將其命名爲componentbridge,各個組件都依賴於該公共橋樑,經過該公共橋樑各個組件間能夠輕鬆的實現數據傳遞。
上圖圈起來的部分都是componentbridge的重點,也是公共橋樑實現的基礎。下面來分別詳細說明。
這是公共橋樑的底層接口,每個組件要向外實現本身的橋樑都要實現這個接口。
interface BridgeInterface {
fun onClear() {}
}
複製代碼
內部很簡單,只有一個方式onClear(), 用來進行數據的釋放。
用來作數據存儲,對橋樑針對不一樣的key進行緩存。避免橋樑內部的實例屢次建立。具體實現方式以下:
class BridgeStore {
private val mMap = HashMap<String, BridgeInterface>()
fun put(key: String, bridge: BridgeInterface) {
mMap.put(key, bridge)?.onClear()
}
fun get(key: String): BridgeInterface? = mMap[key]
fun clear() {
for (item in mMap.values) {
item.onClear()
}
mMap.clear()
}
}
複製代碼
橋樑的實例構建工廠,默認提供經過反射的方式來實例化不一樣的類。Factory接口只提供一個create方法,實現方式由子類自行解決
interface Factory {
fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T
}
複製代碼
在AwesomeGithub中提供了經過反射方式來實例化不一樣類的具體實現NewInstanceFactory
class NewInstanceFactory : Factory {
companion object {
val instance: NewInstanceFactory by lazy { NewInstanceFactory() }
}
override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T = try {
bridgeClazz.newInstance()
} catch (e: InstantiationException) {
throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
} catch (e: IllegalAccessException) {
throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
}
}
複製代碼
Factory的做用是經過抽象的方式來獲取所須要類的實例,至於該類如何實例化,將經過create方法自行實現。
Provider是提供橋樑的註冊與獲取各個組件暴露的接口實現。經過register來統一各個組件向外暴露的橋樑類,最後再經過getBridge來獲取具體的橋樑類,而後調用所需的相關方法,最終達到組件間的數據傳遞。
來看下BridgeProviders的具體實現
class BridgeProviders {
private val mProvidersMap = HashMap<Class<*>, BridgeProvider>()
private val mBridgeMap = HashMap<Class<*>, Class<*>>()
private val mDefaultBridgeProvider = BridgeProvider(NewInstanceFactory.instance)
companion object {
val instance: BridgeProviders by lazy { BridgeProviders() }
}
fun <T : BridgeInterface> register(
clazz: Class<T>,
factory: Factory? = null,
replace: Boolean = false
) = apply {
if (clazz.interfaces.isEmpty() || !clazz.interfaces[0].interfaces.contains(BridgeInterface::class.java)) {
throw RuntimeException("$clazz must implement BridgeInterface")
}
// 1. get contract interface as key, and save implement class to map value.
// 2. get contract interface as key, and save bridgeProvider of implement class instance
// to map value.
clazz.interfaces[0].let {
if (mProvidersMap[it] == null || replace) {
mBridgeMap[it] = clazz
mProvidersMap[it] = if (factory == null) {
mDefaultBridgeProvider
} else {
BridgeProvider(factory)
}
}
}
}
fun <T : BridgeInterface> getBridge(clazz: Class<T>): T {
mProvidersMap[clazz]?.let {
@Suppress("UNCHECKED_CAST")
return it.get(mBridgeMap[clazz] as Class<T>)
}
throw RuntimeException("$clazz subClass is not register")
}
fun clear() {
mProvidersMap.clear()
mBridgeMap.clear()
mDefaultBridgeProvider.bridgeStore.clear()
}
}
複製代碼
每次register以後都會保存一個BridgeProvider實例,若是沒有實現自定義的Factory,將會使用默認是mDefaultBridgeProvider,它內部使用的就是默認的NewInstanceFactory
class BridgeProvider(private val factory: Factory) {
val bridgeStore = BridgeStore()
companion object {
private const val DEFAULT_KEY = "com.idisfkj.awesome.componentbridge"
}
fun <T : BridgeInterface> get(key: String, bridgeClass: Class<T>): T {
var componentBridge = bridgeStore.get(key)
if (bridgeClass.isInstance(componentBridge)) {
@Suppress("UNCHECKED_CAST")
return componentBridge as T
}
componentBridge = factory.create(bridgeClass)
bridgeStore.put(key, componentBridge)
return componentBridge
}
fun <T : BridgeInterface> get(bridgeClass: Class<T>): T =
get(DEFAULT_KEY + "@" + bridgeClass.canonicalName, bridgeClass)
}
複製代碼
註冊完以後就能夠在任意的組件中經過調用橋樑的getBridge來獲取組件向外暴露的方法,從而達到數據的傳遞。
咱們來看下具體的使用示例。
AwesomeGithub項目使用的是Github Open Api,用到的接口基本都要AuthorizationBasic或者是AccessToken,而爲了讓每個組件在調用接口時都可以正常獲取到AuthorizationBasic或者AccessToken,因此提供了一個AppBridge與AppBridgeInterface來向外暴露這些數據,實現以下:
interface AppBridgeInterface: BridgeInterface {
/**
* 獲取用戶的Authorization Basic
*/
fun getAuthorizationBasic(): String?
fun setAuthorizationBasic(authorization: String?)
/**
* 獲取用戶的AccessToken
*/
fun getAccessToken(): String?
fun setAccessToken(accessToken: String?)
}
複製代碼
class AppBridge : AppBridgeInterface {
override fun getAuthorizationBasic(): String? = App.AUTHORIZATION_BASIC
override fun setAuthorizationBasic(authorization: String?) {
App.AUTHORIZATION_BASIC = authorization
}
override fun getAccessToken(): String? = App.ACCESS_TOKEN
override fun setAccessToken(accessToken: String?) {
App.ACCESS_TOKEN = accessToken
}
}
複製代碼
有了上面的橋樑接口,接下來須要作的是先在App主項目中進行註冊
private fun registerBridge() {
BridgeProviders.instance.register(AppBridge::class.java, object : Factory {
override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return AppBridge() as T
}
})
.register(HomeBridge::class.java)
.register(UserBridge::class.java)
.register(ReposBridge::class.java)
.register(FollowersBridge::class.java)
.register(FollowingBridge::class.java)
.register(NotificationBridge::class.java)
.register(SearchBridge::class.java)
.register(WebViewBridge::class.java)
}
複製代碼
在註冊AppBridge時使用的是自定義的Factory,這裏只是爲了簡單展現自定義的Factory的使用,其實沒有特殊需求能夠與後面的bridge同樣直接調用regiser進行註冊。
註冊完了以後就能夠直接在須要的地方進行調用。首先在登陸組件中將獲取到的AuthorizationBasic或者AccessToken進行保存,以便被以後的組件進行調用。
以AccessToken爲例,在login組件中的核心調用代碼以下:
fun getAccessTokenFromCode(code: String) {
showLoading.value = true
repository.getAccessToken(code, object : RequestCallback<Response<ResponseBody>> {
override fun onSuccess(result: ResponseSuccess<Response<ResponseBody>>) {
try {
appBridge.setAccessToken(
result.data?.body()?.string()?.split("=")?.get(1)?.split("&")?.get(
0
)
)
getUser()
} catch (e: IOException) {
e.printStackTrace()
}
}
override fun onError(error: ResponseError) {
showLoading.value = false
}
})
}
複製代碼
如上所示,只需調用appBridge.setAccessToken將數據進行保存;而appBridge能夠經過以下獲取
appBridge = BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)
複製代碼
如今已經有了AccessToken數據,爲了不每次調用接口都手動加入AccessToken,可使用okhttp的Interceptor,即在network組件中進行統一加入。
class GithubApiInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val appBridge =
BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)
Timber.d("intercept url %s %s %s", request.url(), appBridge.getAuthorizationBasic(), appBridge.getAccessToken())
val builder = request.newBuilder()
val authorization =
if (!TextUtils.isEmpty(appBridge.getAuthorizationBasic())) "Basic " + appBridge.getAuthorizationBasic()
else "token " + appBridge.getAccessToken()
builder.addHeader("Authorization", authorization)
val response = chain.proceed(builder.build())
Timber.d("intercept url %s, response %s ,code %d", request.url(), response.body().toString(), response.code())
return response
}
}
複製代碼
這樣就完成了將AccessToken從login組件到network組件間的傳遞。
以上是主項目中集成了login組件,login組件會提供AuthorizationBasic或者AccessToken。那麼對於單個組件(組件能夠單獨運行),爲了讓組件單獨運行時也能調通相關的接口,在調用的時候加入正確的AuthorizationBasic或者AccessToken。須要提供默認的AppBridgeInterface實現類。我這裏命名爲DefaultAppBridge
class DefaultAppBridge : AppBridgeInterface {
override fun getAuthorizationBasic(): String? = BuildConfig.AUTHORIZATION_BASIC
override fun setAuthorizationBasic(authorization: String?) {
}
override fun getAccessToken(): String? = BuildConfig.ACCESS_TOKEN
override fun setAccessToken(accessToken: String?) {
}
}
複製代碼
裏面具體的AuthorizationBasic與AccessToken值能夠經過BuildConfig獲取,而值的定義能夠在local.properities中進行設置
AuthorizationBasic="xxxx"
AccessToken="xxx"
複製代碼
由於每一個組件都會依賴與橋樑componentbridge,因此將值配置到componentbridge的build中,具體以下:
android {
compileSdkVersion Versions.target_sdk
buildToolsVersion Versions.build_tools
defaultConfig {
minSdkVersion Versions.min_sdk
targetSdkVersion Versions.target_sdk
versionCode Versions.version_code
versionName Versions.version_name
buildConfigField "String", "AUTHORIZATION_BASIC", getProperties("AuthorizationBasic") + ""
buildConfigField "String", "ACCESS_TOKEN", getProperties("AccessToken") + ""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
複製代碼
有了默認的組件橋樑實現,如今只需在對應的組件Application中進行註冊便可。
例如項目中的followers組件,單獨運行時使用DefaultAppBridge來達到接口的正常調用。
class FollowersApp : Application() {
override fun onCreate() {
super.onCreate()
SPUtils.init(this)
initTimber()
initRouter()
// register bridges
BridgeProviders.instance.register(DefaultAppBridge::class.java)
.register(DefaultWebViewBridge::class.java)
}
private fun initTimber() {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
private fun initRouter() {
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(this)
}
}
複製代碼
在組件單獨運行時的Application中註冊以後,單獨運行時調用的就是local.properities中設置的值。即保證了組件正常單獨運行。
以上是組件間數據傳遞的所有內容,即解決了組件間的數據傳遞也解決了組件單獨運行時的默認數據調用問題。如需瞭解所有代碼能夠查看AwesomeGithub項目。
AwesomeGithub主頁有三個tab,分別是三個組件。這個三個組件是主頁viewpager中的三個fragment。前面已經說了,在主項目中不能直接調用各個組件,那麼組件中的fragment又該如何加入到主項目中呢?
其實也很簡單,能夠將獲取fragment的實例看成爲組件間的數據傳遞的一種特殊形式。那麼有了上面的組件間數據傳遞的基礎,實如今主項目中調用組件的fragment也瞬間簡單了許多。藉助的仍是橋樑componentbridge。
下面以主頁的search爲例
首先在componentbridge中建立SearchBridgeInterface接口,而且實現默認的橋樑的BridgeInterface接口。
interface SearchBridgeInterface : BridgeInterface {
fun getSearchFragment(): Fragment
}
複製代碼
其中就一個方法,用來向外提供SearchFragment的獲取
接下來在search組件中實現SearchBridgeInterface的具體實現類
class SearchBridge : SearchBridgeInterface {
override fun getSearchFragment(): Fragment = SearchFragment.getInstance()
}
複製代碼
而後回到主項目的Application中進行註冊
BridgeProviders.instance.register(SearchBridge::class.java)
複製代碼
註冊完以後,就能夠在主項目的ViewPagerAdapter中進行獲取SearchFragment實例
class MainViewPagerAdapter(fm: FragmentManager?) : FragmentPagerAdapter(fm) {
override fun getItem(position: Int): Fragment = when (position) {
0 -> BridgeProviders.instance.getBridge(SearchBridgeInterface::class.java).getSearchFragment()
1 -> BridgeProviders.instance.getBridge(NotificationBridgeInterface::class.java)
.getNotificationFragment()
else -> BridgeProviders.instance.getBridge(UserBridgeInterface::class.java).getUserFragment()
}
override fun getCount(): Int = 3
}
複製代碼
主項目中調用組件中的Fragment就是這麼簡單,基本上與以前的數據傳遞時一致的。
有了上面的基礎,可能會聯想到使用處理Fragment方式來進行組件間頁面的跳轉。的確這也是一種解決方式,不過接下來要介紹的是另外一種更加方便與高效的跳轉方式。
項目中使用的是ARouter,它是一個幫助App進行組件化改造的框架,支持模塊間的路由、通訊與解藕。下面簡單的介紹下它的使用方式。
首先須要去官網找到版本依賴,並進行導入。這裏很少說,而後須要在你全部用到的模塊中的build.gradle中添加如下配置
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
複製代碼
記住只要該模塊須要調用ARouter,就須要添加以上代碼。配置完以後就能夠開始使用。
下面我以項目中的webview組件爲例,跳轉到組件中的WebViewActivity
上面已經將相關依賴配置好了,首先須要在Application中進行ARouter初始化
private fun initRouter() {
if (BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(this)
}
複製代碼
再爲WebViewActivity進行path定義
object ARouterPaths {
const val PATH_WEBVIEW_WEBVIEW = "/webview/webview"
}
複製代碼
由於每個ARouter進行路由的時候,都須要配置一個包含兩級的路徑,而後將定義的路徑配置到WebViewActivity中
@Route(path = ARouterPaths.PATH_WEBVIEW_WEBVIEW)
class WebViewActivity : BaseActivity<WebviewActivityWebviewBinding, WebViewVM>() {
@Autowired
lateinit var url: String
@Autowired
lateinit var requestUrl: String
override fun getVariableId(): Int = BR.vm
override fun getLayoutId(): Int = R.layout.webview_activity_webview
override fun getViewModelInstance(): WebViewVM = WebViewVM()
override fun getViewModelClass(): Class<WebViewVM> = WebViewVM::class.java
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ARouter.getInstance().inject(this)
viewModel.url.value = url
viewModel.request(requestUrl)
}
override fun addObserver() {
super.addObserver()
viewModel.backClick.observe(this, Observer {
finish()
})
}
override fun onBackPressed() {
if (viewDataBinding.webView.canGoBack()) {
viewDataBinding.webView.goBack()
return
}
super.onBackPressed()
}
}
複製代碼
如上所示,在進行配置時,只需在類上添加@Route註解,而後再將定義的路徑配置到path上。其中的@Autowired註解表明WebViewActivity在使用ARouter進行跳轉時,接收兩個參數,分別爲url與requestUrl。
ARouter本質是解析註解,而後定位到參數,再經過原始的Intent中獲取到傳遞過來的參數值。
有了上面的準備過程,最後剩下的就是調用ARouter進行頁面跳轉。這裏爲了統一調用方式,將其調加到橋樑中。
class WebViewBridge : WebViewBridgeInterface {
override fun toWebViewActivity(context: Context, url: String, requestUrl: String) {
ARouter.getInstance().build(ARouterPaths.PATH_WEBVIEW_WEBVIEW).with(
bundleOf("url" to url, "requestUrl" to requestUrl)
).navigation(context)
}
}
複製代碼
前面是定義的跳轉路徑,後面緊接的是頁面傳遞的參數值。剩下的就是在別的組件中調用該橋樑,例如followers組件中的contentClick點擊:
class FollowersVHVM(private val context: Context) : BaseRecyclerVM<FollowersModel>() {
var data: FollowersModel? = null
override fun onBind(model: FollowersModel?) {
data = model
}
fun contentClick() {
BridgeProviders.instance.getBridge(WebViewBridgeInterface::class.java)
.toWebViewActivity(context, data?.html_url ?: "", "")
}
}
複製代碼
更多ARouter的使用方式,讀者能夠自行查閱官方文檔
在AwesomeGithub項目中,組件化過程當中的主要難點與解決方案已經分析的差很少了。最後咱們來聊聊組件間的解藕優化。
組件化自己就是對項目進行解藕,因此若是要進一步進行優化,主要是對組件間的依賴或者資源等方面進行解藕。而對於組件間的依賴,嘗試過在依賴的時候使用runtimeOnly。由於runtimeOnly能夠避免依賴的組件在運行以前進行引用調用,它只會在項目運行時纔可以正常的引用,這樣就能夠防止主項目中進行開發時直接引用依賴的組件。
可是,在實踐的過程當中,若是項目中使用了DataBinding,此時使用runtimeOnly進行依賴組件,經過該方式依賴的組件在運行的過程當中會出現錯誤。
這是因爲DataBinding須要在編譯時生成對應資源文件。使用runtimeOnly會致使其缺失,最終在程序進行運行時找不到對應資源,致使程序異常。
固然若是沒有使用DataBinding就不會有這種問題。這是組件依賴方面,下面再來講說資源相關的。
因爲不一樣組件模塊下能夠引入相同命名的資源文件,爲了防止開發過程當中不一樣組件下相同名稱的資源文件引用錯亂,這裏能夠經過在不一樣組件模塊中的build.gradle中添加資源前綴。例如login組件中
resourcePrefix表明login組件中的全部資源文件命名都必須以login_爲前綴命名。若是沒有編譯器將會標紅,並提示你正確的使用方式。這種方式能夠必定程度上避免資源文件的亂用與錯亂。
以上是AwesomeGithub組件化過程當中的整個探索經歷。若是你想更深刻的瞭解其實現過,強烈建議你直接查看項目的源碼,畢竟語言上的描述是有限的,程序員就應該直接看代碼才能更快更準的理解。
AwesomeGithub: github.com/idisfkj/Awe…
若是這篇文章對你有所幫助,你能夠順手點贊、關注一波,這是對我最大的鼓勵!