AwesomeGithub组件化探索之旅

  • 2020 年 2 月 18 日
  • 筆記

读完需要37分钟

速读仅需 13 分钟

之前一直听说过组件化开发,而且面试也有这方面的提问,但都未曾有涉及具体的项目。所以就萌生了基于 Github 的开放 Api,并使用组件化的方式来从零搭建一个 Github 客户端,起名为AwesomeGithub。

在这里对组件化开发进行一个总结,同时也希望能够帮助别人更好的理解组件化开发。

先来看下项目的整体效果

下面是项目的结构

1

为何要使用组件化

  1. 对于传统的开发模式,一个 app 下面是包含项目的全部页面模块与逻辑。这样项目一旦迭代时间过长,业务模块逐渐增加,相应的业务逻辑复杂度也成指数增加。模块间的互相调用频繁,这样必定会导致模块间的耦合增加,业务逻辑嵌套程度加深。一旦修改其中一个模块,可能就牵一发动全身了。
  2. 传统的开发模式不利于团队的集体开发合作,因为每个开发者都是在同一个 app 模块下开发。这样导致的问题是,不能预期每个开发者所会修改到的具体代码部分,即所能够修改的代码区域。因为模块耦合在一起,涉及的区域不可预期,导致不同开发者会修改同一个文件或者同一段代码逻辑,从而导致异常冲突。
  3. 传统开发模式不利于测试,每次迭代都要将项目整体测试一遍。因为在同一个 app 下面代码是缺乏约束的,你不能保证只修改了迭代过程中所涉及的需求逻辑。

以上问题随着项目的迭代周期的增大,会表现的越来越明显。那么使用组件化又能够解决什么问题了?

2

组件化能够解决的问题

  1. 组件化开发是将各个相关功能进行分离,分别独立成一个单独可运行的 app,并且组件之间不能相互直接引用。这样就减少了代码耦合,达到业务逻辑分层效果。
  2. 组件化可以提高团队协作能力,不同的人员可以开发不同的组件,保证不同开发人员互不影响。
  3. 组件化将 app 分成多个可单独运行的子项目,可以用自己独立的版本,可以独立编译,打包、测试与部署。这样不仅可以提高单个模块的编译速度,同时也可以提高测试的效率。
  4. 组件化可以提高项目的灵活性,app 可以按需加载所要有的组件,提高 app 的灵活性,可以快速生成可定制化的产品。

现在我们已经了解了组件化的作用,但要实现组件化,达到其作用,必须解决实现组件化过程中所遇到的问题。

3

组件化需要解决的问题

  1. 组件单独运行
  2. 组件间数据传递
  3. 主项目使用组件中的 Fragment
  4. 组件间界面的跳转
  5. 组件解耦

以上是实现组件化时所遇到的问题,下面我会结合AwesomeGithub来具体说明解决方案。

3.1

组件单独运行

组件的创建,可以直接使用 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 进行单独构建、打包、调试与运行。

3.2

组件间的数据传递

由于组件与组件、项目间是不能直接使用类的相互引用来进行数据的传递,所以为了解决这个问题,这里通过一个公共库来做它们之间调用的桥梁,它们不直接拿到具体的引用对象,而是通过接口的方式来获取所需要的数据。

在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项目。

3.3

主项目使用组件中的 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 就是这么简单,基本上与之前的数据传递时一致的。

3.4

组件间界面的跳转

有了上面的基础,可能会联想到使用处理 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项目中,组件化过程中的主要难点与解决方案已经分析的差不多了。最后我们来聊聊组件间的解藕优化。

3.5

组件解耦

组件化本身就是对项目进行解藕,所以如果要进一步进行优化,主要是对组件间的依赖或者资源等方面进行解藕。而对于组件间的依赖,尝试过在依赖的时候使用 runtimeOnly。因为 runtimeOnly 可以避免依赖的组件在运行之前进行引用调用,它只会在项目运行时才能够正常的引用,这样就可以防止主项目中进行开发时直接引用依赖的组件。

但是,在实践的过程中,如果项目中使用了 DataBinding,此时使用 runtimeOnly 进行依赖组件,通过该方式依赖的组件在运行的过程中会出现错误。

这是由于 DataBinding 需要在编译时生成对应资源文件。使用 runtimeOnly 会导致其缺失,最终在程序进行运行时找不到对应资源,导致程序异常。

当然如果没有使用 DataBinding 就不会有这种问题。这是组件依赖方面,下面再来说说资源相关的。

由于不同组件模块下可以引入相同命名的资源文件,为了防止开发过程中不同组件下相同名称的资源文件引用错乱,这里可以通过在不同组件模块中的 build.gradle 中添加资源前缀。例如 login 组件中

resourcePrefix 代表 login 组件中的所有资源文件命名都必须以 login_ 为前缀命名。如果没有编译器将会标红,并提示你正确的使用方式。这种方式可以一定程度上避免资源文件的乱用与错乱。

以上是AwesomeGithub组件化过程中的整个探索经历。如果你想更深入的了解其实现过,强烈建议你直接查看项目的源码,毕竟语言上的描述是有限的,程序员就应该直接看代码才能更快更准的理解。