之前一直听说过组件化开发,而且面试也有这方面的提问,但都未曾有涉及具体的项目。所以就萌生了基于Github的开放Api,并使用组件化的方式来从零搭建一个Github客户端,起名为AwesomeGithub。
在这里对组件化开发进行一个总结,同时也希望能够帮助别人更好的理解组件化开发。
先来看下项目的整体效果
下面是项目的结构
为何要使用组件化
- 对于传统的开发模式,一个app下面是包含项目的全部页面模块与逻辑。这样项目一旦迭代时间过长,业务模块逐渐增加,相应的业务逻辑复杂度也成指数增加。模块间的互相调用频繁,这样必定会导致模块间的耦合增加,业务逻辑嵌套程度加深。一旦修改其中一个模块,可能就牵一发动全身了。
- 传统的开发模式不利于团队的集体开发合作,因为每个开发者都是在同一个app模块下开发。这样导致的问题是,不能预期每个开发者所会修改到的具体代码部分,即所能够修改的代码区域。因为模块耦合在一起,涉及的区域不可预期,导致不同开发者会修改同一个文件或者同一段代码逻辑,从而导致异常冲突。
- 传统开发模式不利于测试,每次迭代都要将项目整体测试一遍。因为在同一个app下面代码是缺乏约束的,你不能保证只修改了迭代过程中所涉及的需求逻辑。
以上问题随着项目的迭代周期的增大,会表现的越来越明显。那么使用组件化又能够解决什么问题了?
组件化能够解决的问题
- 组件化开发是将各个相关功能进行分离,分别独立成一个单独可运行的app,并且组件之间不能相互直接引用。这样就减少了代码耦合,达到业务逻辑分层效果。
- 组件化可以提高团队协作能力,不同的人员可以开发不同的组件,保证不同开发人员互不影响。
- 组件化将app分成多个可单独运行的子项目,可以用自己独立的版本,可以独立编译,打包、测试与部署。这样不仅可以提高单个模块的编译速度,同时也可以提高测试的效率。
- 组件化可以提高项目的灵活性,app可以按需加载所要有的组件,提高app的灵活性,可以快速生成可定制化的产品。
现在我们已经了解了组件化的作用,但要实现组件化,达到其作用,必须解决实现组件化过程中所遇到的问题。
组件化需要解决的问题
- 组件单独运行
- 组件间数据传递
- 主项目使用组件中的Fragment
- 组件间界面的跳转
- 组件解耦
以上是实现组件化时所遇到的问题,下面我会结合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<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
桥梁的实例构建工厂,默认提供通过反射的方式来实例化不同的类。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
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项目。
主项目使用组件中的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<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:https://github.com/idisfkj/AwesomeGithub
如果这篇文章对你有所帮助,你可以顺手点赞、关注一波,这是对我最大的鼓励!
网友评论