0%

Android组件化

概述

模块

模块指的是独立的业务模块,比如直播模块会员模块等。

组件

组件指的是单一的功能组件,如登录组件上报组件等,每个组件都可以以一个单独的module开发,并且可以单独抽出来作为SDK对外发布使用。

模块组件间最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。

组件化

一个可用的组件化架构图从上向下分别为APP壳,业务层、组件层和基础层:

  • 基础层:基础层包含的是一些基础库以及对基础库的封装,比如常用的图片加载,网络请求,数据上报操作等等,其他模块或者组件甚至App应用都可以引用同一套基础库,因此这些基础库最好在另一个项目/git仓库中维护。在基础库上面可以再增加一个Common组件,它位于App应用所在的项目中,用来添加一些都需要引用到的依赖以及本App应用独有的共有方法,UI控件等。
  • 组件层:基础层往上是组件层,组件层包含一些功能组件,比如分享,登录,下载,播放等等。
  • 业务层:组件层往上是业务层,一个具体的业务模块会按需引用不同的组件,最终实现业务功能。
  • APP壳:在APP模块中根据需求统筹各个业务组件,最终输出为一个完整的应用。

组件单独调试

Android Gradle提供了三种插件,在开发中可以通过配置不同的插件来配置不同的工程。

  • App插件:com.android.application
  • Library插件:com.android.libraay
  • Test插件:com.android.test

可以在gradle.properties配置文件中声明一个boolean变量:isAlone,在build.gradle中通过判断这个变量来动态加载不同的插件,ApplicationId以及AndroidManifest文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- main/manifest/AndroidManifest.xml 单独调试 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hearing.share">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ShareActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

<!-- main/AndroidManifest.xml 集成调试 -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.hearing.share">

<application android:theme="@style/AppTheme">
<activity android:name=".ShareActivity"/>
</application>
</manifest>

在组件的build.gradle中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
android {
defaultConfig {
if (isAlone.toBoolean()) {
// 单独调试时添加 applicationId ,集成调试时移除
applicationId "com.hearing.login"
}
// ...
}

sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (isAlone.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}

另外为了使组件化项目中的所有组件的compileSdkVersion、buildToolsVersion以及开源库版本等都能保持统一,也为了方便修改版本号,统一在Android工程根目录下的build.gradle中定义这些版本号,当然也可以在项目根目录添加一个单独的gradle文件定义这些常量,然后由根build.gradle引入。

组件混淆方案

组件化项目的代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。

组件Application

组件化开发中每一个组件可能都会自定义一个Application类,当所有组件要打包合并在一起的时候,由于程序只能有一个Application,组件中自己定义的Application无法使用。因此需要想办法在任何一个业务组件中都能获取到一个可用的全局Context,而且这个Context不管是在组件开发模式还是在集成开发模式都是生效的。

在基础库组件中封装了项目中用到的各种Base类(BaseActivity,BaseFragment等),这些基类中有一个BaseApplication类,它主要作用是使各个业务组件和app壳工程中都能访问到全局Context,且在其中可以存储一些全局对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
object BaseApplication {
private val mGlobalObjects = mutableMapOf<Any, Any>()
private var mApplication: Application? = null
private var mContext: Context? = null

fun init(application: Application) {
mApplication = application
mContext = application.baseContext
}

fun getContext(): Context? {
return mContext ?: mApplication
}

fun getApplication(): Application? {
return mApplication
}

@Synchronized
fun putGlobalObject(key: Any, obj: Any) {
mGlobalObjects[key] = obj
}

@Synchronized
fun getGlobalObject(key: Any): Any? {
return mGlobalObjects[key]
}

@Synchronized
fun removeGlobalObject(key: Any): Any? {
return mGlobalObjects.remove(key)
}
}

然后可以在本应用的Common组件中定义一个公用的Application基类,然后在各自组件中定义一个Application继承这个基类,这样就可以通过BaseApplication来获取全局的Context了:

1
2
3
4
5
6
open class MyBaseApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
BaseApplication.init(this)
}
}

另外需要在项目从组件模式转换到集成模式后将组件的Application剔除出项目,可以在组件的Java文件夹下创建一个debug文件夹,用于存放不会在业务组件中引用的类,比如说Application类:

1
2
3
4
5
6
7
8
9
10
11
12
13
sourceSets {
main {
if (isAlone.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
// 集成开发模式下排除debug文件夹中的所有Java文件
java {
exclude 'debug/**'
}
}
}
}

组件间数据传递与方法调用

接下来模拟一个组件间数据传递和方法调用的场景:分享组件分享时需要根据用户是否登录来判断分享的动作。

base_component

base_component组件用来提供组件间数据传递和方法调用的功能。

ILoginService

1
2
3
4
interface ILoginService {
fun isLogin(): Boolean
fun getUserId(): String
}

空实现:EmptyLoginService

1
2
3
4
5
6
7
8
9
class EmptyLoginService : ILoginService {
override fun isLogin(): Boolean {
return false
}

override fun getUserId(): String {
return ""
}
}

Service工厂:ServiceFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
object ServiceFactory {
private val mServices = CopyOnWriteArrayList<Any>()

fun register(service: Any) {
if (!mServices.contains(service)) {
mServices.add(service)
}
}

private fun <T> getService(cls: Class<T>): Any? {
mServices.forEach {
if (cls.isInstance(it)) {
return it
}
}
return null
}

fun getLoginService(): ILoginService {
val service = getService(ILoginService::class.java)
return service as? ILoginService ?: EmptyLoginService()
}
}

component_login

登录组件中提供ILoginService的具体实现:

1
2
3
4
5
6
7
8
9
class LoginService : ILoginService {
override fun isLogin(): Boolean {
return true
}

override fun getUserId(): String {
return "1024"
}
}

component_share

分享组件中进行分享的逻辑判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object ShareHelper {
fun share() {
val loginService = ServiceFactory.getLoginService()
if (loginService.isLogin()) {
Toast.makeText(
BaseApplication.getContext(),
"Share from ${loginService.getUserId()}", Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
BaseApplication.getContext(),
"Not login", Toast.LENGTH_SHORT
).show()
}
}
}

app

在App主Module中进行ILoginService服务的注册:

1
2
3
4
5
6
class MainApplication : MyBaseApplication() {
override fun onCreate() {
super.onCreate()
ServiceFactory.register(LoginService())
}
}

然后根据业务逻辑调用分享组件的功能:

1
2
3
4
5
6
7
8
9
10
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<TextView>(R.id.text_view).setOnClickListener {
ShareHelper.share()
}
}
}

组件间界面跳转

可以使用开源框架:ARouter或者CC等。

总结

以上组件化的内容是在参考了网络上一些关于Android组件化的解析后,结合了自己的理解与思考得到的一些想法,在实际场景里可能会有问题,期待在以后的开发中有更多的实践与思考!