com 组件调用不起来_AwesomeGithub组件化探索之旅
閱讀前請點擊右上角“關注”,每天免費獲取Android知識解析及面試解答。Android架構解析,只做職場干貨,完全免費分享!
之前一直聽說過組件化開發,而且面試也有這方面的提問,但都未曾有涉及具體的項目。所以就萌生了基于Github的開放Api,并使用組件化的方式來從零搭建一個Github客戶端,起名為AwesomeGithub。
在這里對組件化開發進行一個總結,同時也希望能夠幫助別人更好的理解組件化開發。
先來看下項目的整體效果
下面是項目的結構
為何要使用組件化
以上問題隨著項目的迭代周期的增大,會表現的越來越明顯。那么使用組件化又能夠解決什么問題了?
組件化能夠解決的問題
現在我們已經了解了組件化的作用,但要實現組件化,達到其作用,必須解決實現組件化過程中所遇到的問題。
組件化需要解決的問題
以上是實現組件化時所遇到的問題,下面我會結合AwesomeGithub來具體說明解決方案。
組件單獨運行
組件的創建,可以直接使用library的方式進行創建。只不過在創建完之后,要讓組件達到可以單獨運行調試的地步,還需要進行相關配置。
運行方式動態配置
首先,當創建完library時,在build.gradle中可以找到這么一行代碼
apply?plugin:?'com.android.library'這是gradle插件所支持的一種構建類型,代表可以將其依賴到主項目中,構建后輸出aar包。這種方式對于我們將組件依賴到主項目中完全吻合的。
而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項目。
ApplicationId與AndroidManifest
除了修改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的重點,也是公共橋梁實現的基礎。下面來分別詳細說明。
BridgeInterface
這是公共橋梁的底層接口,每一個組件要向外實現自己的橋梁都要實現這個接口。
interface BridgeInterface { fun onClear() {}}內部很簡單,只有一個方式onClear(), 用來進行數據的釋放。
BridgeStore
用來做數據存儲,對橋梁針對不同的key進行緩存。避免橋梁內部的實例多次創建。具體實現方式如下:
class BridgeStore { private val mMap = HashMap() 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
橋梁的實例構建工廠,默認提供通過反射的方式來實例化不同的類。Factory接口只提供一個create方法,實現方式由子類自行解決
interface Factory { fun create(bridgeClazz: Class): T}在AwesomeGithub中提供了通過反射方式來實例化不同類的具體實現NewInstanceFactory
class NewInstanceFactory : Factory { companion object { val instance: NewInstanceFactory by lazy { NewInstanceFactory() } } override fun create(bridgeClazz: Class): 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
Provider是提供橋梁的注冊與獲取各個組件暴露的接口實現。通過register來統一各個組件向外暴露的橋梁類,最后再通過getBridge來獲取具體的橋梁類,然后調用所需的相關方法,最終達到組件間的數據傳遞。
來看下BridgeProviders的具體實現
class BridgeProviders { private val mProvidersMap = HashMap, BridgeProvider>() private val mBridgeMap = HashMap, Class>() private val mDefaultBridgeProvider = BridgeProvider(NewInstanceFactory.instance) companion object { val instance: BridgeProviders by lazy { BridgeProviders() } } fun register( clazz: Class, 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 getBridge(clazz: Class): T { mProvidersMap[clazz]?.let { @Suppress("UNCHECKED_CAST") return it.get(mBridgeMap[clazz] as Class) } 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 get(key: String, bridgeClass: Class): 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 get(bridgeClass: Class): 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 create(bridgeClazz: Class): 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> { override fun onSuccess(result: ResponseSuccess>) { 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項目。
主項目使用組件中的Fragment
AwesomeGithub主頁有三個tab,分別是三個組件。這個三個組件是主頁viewpager中的三個fragment。前面已經說了,在主項目中不能直接調用各個組件,那么組件中的fragment又該如何加入到主項目中呢?
其實也很簡單,可以將獲取fragment的實例當作為組件間的數據傳遞的一種特殊形式。那么有了上面的組件間數據傳遞的基礎,實現在主項目中調用組件的fragment也瞬間簡單了許多。借助的還是橋梁componentbridge。
下面以主頁的search為例
SearchBridgeInterface
首先在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() { @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::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() { 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組件化過程中的整個探索經歷。如果你想更深入的了解其實現過程,強烈建議你直接查看項目的源碼,畢竟語言上的描述是有限的,程序員就應該直接看代碼才能更快更準的理解。
原文作者:午后一小憩
推薦閱讀:阿里P7進階學習之路,你還沒掌握的幾個關鍵技術,年薪60W不是夢
總結
以上是生活随笔為你收集整理的com 组件调用不起来_AwesomeGithub组件化探索之旅的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java提高篇四_(转)java提高篇(
- 下一篇: BSP模型