Android组件化架构解析总结

一.组件化工程架构分层

组件化架构图示

  1. 基础层->功能组件:是最基础的开发框架,包含了基础开发所需的基类、工具类、第三方库等。依赖该模块就能快速进行开发。
  2. 中间层->公共组件,包含了路由的功能,可以和业务组件进行交互。
  3. 业务层->业务组件,依赖于中间层,包含了各个业务功能的组件。每个业务组件都能运行出一个小型的 App 进行调试。
  4. 应用层->壳组件,也就是俗称的 App 壳,可以集成各种所需的业务组件,组合出不同 App。

功能组件

功能组件是为了支撑业务组件的某些功能而独立划分出来的组件,例如视频播放功能组件,地图导航功能组件,日志管理功能组件,网络请求功能组件,图片加载功能组件等。其中有些是我们对第三方功能库的二次封装简称二方库,有些是自己写的通用的功能代码,这些功能组件不包含任何业务逻辑相关功能,不同的APP可以依赖这些功能组件为业务提供支持, 功能组件实质上跟项目中引入的第三方库是一样的, 在实际开发中我们按需引入这些库, 例如:所有的业务模块都需要用到网络请求功能组件,图片加载功能组件,那么可以在common公共组件中通过api的形式依赖它们,这样所有的业务组件都能获取到这些功能组件进行使用,但有些功能组件例如:移动支付功能组件一般只是在订单相关的业务组件中才能用到,其他的业务组件不需要这个支付的功能,因此这样的功能组件最好不要在common公共组件中进行引用,而是让订单业务组件单独依赖移动支付这个功能组件即可。

网络请求功能组件,图片加载功能组件,日志管理功能组件等这些通用组件基本上每一个业务组件都会使用到其中的功能,因此笔记建议将这些通用的功能组件封装到一个lib_core组件当中,让common公共组件中通过api的形式引用lib_core组件即可。这样可以解决功能组件分散的问题。

公共组件(Common组件)

Common组件是当前App业务公共的组件库,它通过api方式依赖通用功能组件以提供对App相关业务提供功能支持,而且应用特有的相关逻辑也需要放到里面来,例如封装BaseActivity类,BaseApplication类,对网络接口统一处理,声明APP需要的uses-permission,定义全局通用的主题(Theme)及一些公用的类。Common组件有如下特点:

  1. Common组件的 AndroidManifest.xml中声明了我们 Android应用需要用到的所有使用权限 uses-permission 和 uses-feature,这样一来所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了。
  2. Common组件的 build.gradle 需要依赖业务组件中用到的 第三方依赖库和jar包的版本,例如:用到的ARouter、Okhttp等等。
  3. Common组件中封装了应用的 Base类和公用的 widget控件;业务组件中都用到的数据相关类也应放于Common组件中,例如保存到 SharedPreferences 和 DataBase 中的登陆数据;
  4. Common组件的资源文件中需要放置项目公用的 Drawable、layout、sting、dimen、color和style 等等,另外项目中的 Activity 主题必须定义在 Common中,方便和 BaseActivity 配合保持整个Android应用的界面风格统一。

业务组件

业务组件就是根据业务逻辑的不同拆分出来的组件,业务组件的特点有:

  1. 业务组件中要有两张AndroidManifest.xml,分别对应组件开发模式和集成开发模式。
  2. 业务组件有组件开发模式调试文件夹,这个文件夹在集成模式下会从业务组件的代码中排除掉,所以调试文件夹中的类不能被业务组件强引用,其他例如组件模式下给目标 Activity 传递参数的用的 launchActivity 也应该置于 调试 文件夹中。
  3. 业务组件必须在自己的 build.gradle 中根据 isModule 值的不同改变自己的属性,在组件模式下是:com.android.application,而在集成模式下com.android.library;同时还需要在build.gradle配置资源文件,如 指定不同开发模式下的AndroidManifest.xml文件路径等。
  4. 业务组件需依赖Common公共组件,以及依赖其他用到的功能组件。

**业务组件中有一个main组件比较特殊 ** Main组件除了有业务组件的普遍属性外,Main组件在集成模式下的AndroidManifest.xml是跟其他业务组件不一样的,Main组件的表单中需要声明整个Android应用的launch Activity,这就是Main组件的独特之处;所以建议SplashActivity、登陆Activity以及主界面都应属于Main组件,也就是说Android应用启动后要调用的页面应置于Main组件。

壳工程

app壳工程是就是一个空壳工程,负责管理各个业务组件,和打包apk,没有具体的业务功能;但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功能:

  1. app壳工程中声明了我们Android应用的 Application,这个 Application 必须继承自 Common组件中的 BaseApplication(如果你无需实现自己的Application可以直接在表单声明BaseApplication),因为只有这样,在打包应用后才能让BaseApplication中的Context生效。
  2. app壳工程的 AndroidManifest.xml 是Android应用的根表单,应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份 AndroidManifest.xml 中。
  3. app壳工程的 build.gradle 是比较特殊的,app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,所以app壳工程是不需要单独调试单独开发的。另外Android应用的打包签名,混淆,以及buildTypes和defaultConfig都需要在这里配置,在组件开发模式下app壳工程只需要依赖Common组件,而在集成模式下app壳工程必须依赖所有在应用Application中声明的业务组件,并且不需要再依赖任何功能组件。

二.组件化架构搭建

1.创建工程添加开关

通过Android Studio创建Android工程,并且在gradle.properties中添加isModule开关:

isModule = false

isModule为true时各个业务组件可以单独编译成App进行运行,isModule为true时为集成开发模式各个业务组件将会打包到壳工程中中合并成一个完成的App功能。

2.创建功能组件

通过Android studio的File->New->New Module->Android Library创建项目所需要的功能模块组件,前缀以"lib_"开头,例如lib_pay,lib_map等工程。建议创建一个lib_core的公共功能模块组件,将常用的与业务无关的功能封装到此模块中,例如图片加载功能,网络请求功能,日志管理功能等,这些功能可以被所有的app通用。

3.创建公共组件

通过Android studio的File->New->New Module->Android Library创建项目的公共组件lib_common工程,公共组件需要依赖lib_core公共功能模块组件,公共组件里面包含的代码逻辑有如下:

  • 封装app对应业务的公共逻辑,例如BaseActivity类,BaseApplication类等。
  • 在AndroidManifest.xml中声明 Android应用需要用到的所有使用权限。
  • 项目公用的 Drawable、layout、sting、dimen、color和style 项目通用的主题等等。
  • 使用api依赖的方式将通用的依赖提供给业务组件。

4.创建业务组件

通过Android studio的File->New->New Module->phone application创建业务组件,前缀以"module_"开头例如module_mine,module_main等工程。在其build.gradle中配置模块切换的开关:

if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
android {
    ...
    defaultConfig {
        if (isModule.toBoolean()) {
            applicationId "com.huke.module_mine"
        }
    }
}
//依赖公共项目
dependencies {
    implementation project(path: ':lib_common')
}    

在模块开发模式中可能需要一个可以启动的Activity和启动图标,应用名称,布局文件等使得app能够单独运行,但是当业务组件作为集成模式时这些东西是不需要打包到壳工程中的,因此需要在业务组件的main目录创建module目录用于存放单独作为模块时的代码,清单文件,资源文件等。
module->AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <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/Theme.AndroidComponentDemo">
        <activity
            android:name=".HomeMineActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>
</manifest>

在build.gradle中配置模块编译时将module文件夹中的内容打包进app,集成模式下则不需要调试的这些代码及资源:

...
android {
    ...
    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
                java.srcDirs = ['src/main/java', 'src/main/module/java']
                res.srcDirs = ['src/main/res', 'src/main/module/res']
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }

        }
    }
}

业务组件作为集成模式时src/main/AndroidManifest.xml要进行修改,将启动图标应用名称都剔除掉:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
       
    </application>
</manifest>

在子module模块的build.gradle中设置资源名必须以指定的字符串做前缀,并且按照规则修改res文件夹下面的所有的文件的前缀(防止资源文件冲突导致出现的问题)

android {
    ...
    resourcePrefix "mine_"
    ...
}

5.创建壳工程组件

可以将工程初始创建的app当成壳工程组件,根据需要依赖子module:

dependencies {
    //依赖公共项目
    implementation project(path: ':lib_common')
    if (!isModule.toBoolean()) {
        implementation project(path: ':module_mine')
        implementation project(path: ':module_main')
        implementation project(path: ':module_home')
    }
}    

在壳工程中配置应用的启动的application,启动图标,应用名称等:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:name="com.huke.lib_common.BaseApplication"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/mine_app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        tools:targetApi="31" />
</manifest>

三.组件化组件Application处理

全局Application如何获取

在lib_common中创建BaseApplication,在壳app工程的manifest文件中引入BaseApplication,其他所有的module都引用了lib_common所以都可以获取到BaseApplication对象。
BaseApplication:

public class BaseApplication extends MultiDexApplication {
    private static Context mApplicationContext;
    private static Application mApplication;

    @Override
    public void onCreate() {
        super.onCreate();
        mApplicationContext = this;
        mApplication = this;
    }

    /**
     * 获取application
     */
    public static Application getApplication() {
        return mApplication;
    }

    /**
     * 获取application
     */
    public static Context getAppContext() {
        return mApplicationContext;
    }
}

Application生命周期分发

组件化开发将不同的业务逻辑进行解耦,每个业务模块拥有不同的功能,有些组件的功能需要在Application 中进行初始化,比如百度地图,百度 OCR。已知在组件化架构中壳工程中只能指定一个Application,通常是lib_common的BaseApplication,如果是所有的业务模块共有的功能,当然是可以将其放到 BaseApplication中进行初始化的,每例如bugly,统计相关功能。但是有的功能是某一个业务模块所特有的,,例如商品详情业务模块需要第三方分享, 城市定位业务模块需要百度地位等。如果这些功能都在lib_common进行依赖并且在BaseApplication中去初始化,那么在模块化模式中,不需要用到这个功能的业务也会强行初始化这个功能, 前文中一直强调组件化的目的是为了业务解耦,这显然是不合适的。因此我们需要将 BaseApplication的生命周期分发到各个模块中去,由这些业务模块自行初始化相关功能。 目前有如下几种常见的方式:

方式一:通过读取Manifest反射进行调用(推荐)
  1. 在lib_common创建ApplicationDelegate类。
    ApplicationDelegate:
interface ApplicationDelegate {
    fun attachBaseContext(application: Application?, context: Context?)
    fun onCreate(application: Application?)
}
  1. 在lib_common中的BaseApplication中添加初始化代码。
open class BaseApplication : Application() {
    var delegates: List<ApplicationDelegate>? = null
    private val MODULE_META_KEY = "ApplicationDelegate"

    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        base?.let {
            delegates = findApplicationDelegate(it)
        }
        delegates?.forEach { it.attachBaseContext(this, base); }
    }

    override fun onCreate() {
        super.onCreate()
        mApplicationContext = this
        mApplication = this

        Log.d("Application", "BaseApplication->onCreate()")
        ARouter.init(this)

        delegates?.forEach { it.onCreate(this); }
    }

    private fun findApplicationDelegate(context: Context): List<ApplicationDelegate> {
        val delegates: MutableList<ApplicationDelegate> = ArrayList()
        try {
            val pm = context.packageManager
            val packageName = context.packageName
            val info = pm.getApplicationInfo(packageName, GET_META_DATA)
            if (info.metaData != null) {
                for (key in info.metaData.keySet()) {
                    val value = info.metaData[key]
                    if (MODULE_META_KEY == value) {
                        val delegate: ApplicationDelegate = initApplicationDelegate(key)
                        delegates.add(delegate)
                    }
                }
            }
        } catch (e: PackageManager.NameNotFoundException) {
            e.printStackTrace()
        }
        return delegates
    }

    private fun initApplicationDelegate(className: String): ApplicationDelegate {
        var clazz: Class<*>? = null
        var instance: Any? = null
        try {
            clazz = Class.forName(className)
        } catch (e: ClassNotFoundException) {
            e.printStackTrace()
        }
        try {
            instance = clazz?.newInstance()
        } catch (e: Exception) {
            e.printStackTrace()
        }
        if (instance !is ApplicationDelegate) {
            throw RuntimeException("不能获取 " + ApplicationDelegate::class.java.name + " 的实例 " + instance)
        }
        return instance
    }
}
  1. 在业务模块中创建ApplicationDelegate的实现类。
class MineApplicationDelegate : ApplicationDelegate {
    override fun attachBaseContext(application: Application?, context: Context?) {

    }
    override fun onCreate(application: Application?) {
        Log.d("Application", "MineApplicationDelegate->onCreate()")
    }
}
  1. 在业务模块的AndroidManifest中添加mate信息参数。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <!--value参数是在BaseApplication中固定的值,value是实现类的路径-->
        <meta-data
            android:name="com.huke.module_mine.MineApplicationDelegate"
            android:value="ApplicationDelegate" />
    </application>
</manifest>
  1. 在业务模块的AndroidManifest中添加android:name参数。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:name="com.huke.lib_common.BaseApplication" >
        <!--value参数是在BaseApplication中固定的值,value是实现类的路径-->
        <meta-data
            android:name="com.huke.module_mine.MineApplicationDelegate"
            android:value="ApplicationDelegate" />
    </application>
</manifest>
  1. 在壳工程中添加android:name指向lib_common中的BaseApplication代码。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:name="com.huke.lib_common.BaseApplication">
    </application>
</manifest>
  1. 这样一来业务模块在模块工程中和App工程中都可以调用到ApplicationDelegate->onCreate(application: Application?)进行初始化了。
方式二:通过ContentProvider实现
  1. 在业务模块中创建ContentProvider继承自ContentProvider,初始化相关的逻辑在onCreate中进行编写。
class MineContentProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        Log.d("Application", "MineContentProvider->onCreate()")
        return false
    }

    override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        return null
    }

    override fun getType(uri: Uri): String? {
        return null
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }
}
  1. 在业务模块的AndroidManifest和应用AndroidManifest中都配置ContentProvider
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        ...
         <provider
            android:name=".MineContentProvider"
            android:authorities="com.huke.mine.authority" />
    </application>

</manifest>

需要注意都各个业务模块配置的ContentProvider的authorities不能设置为一样的,否则就只能在同一设备安装其中一个应用。

方式三:通过Android Startup实现

和ContentProvider方式大同小异,略过。

方式四:通过spi技术实现
  1. 在lib_common组件的build.gradle中添加如下依赖:
 //common组件 build.gradle
 api 'com.google.auto.service:auto-service:1.0-rc7'
 kapt 'com.google.auto.service:auto-service:1.0-rc7'
  1. 在lib_common组件中创建一个接口,这个接口和Application中的接口方法一致:
interface IApplication {
    fun attachBaseContext(base: Context)
 
    fun onCreate()

    fun onLowMemory()

    fun onTerminate()

    fun onTrimMemory(level: Int)

    fun onConfigurationChanged(newConfig: Configuration)
}
  1. 在业务模块等中实现IApplication接口:
@AutoService(IApplication::class)
class MineApplicationImpl : IApplication {

    override fun attachBaseContext(base: Context) {
        app = base as Application
    }

    override fun onCreate() {
        Log.d("Application", "ApplicationImpl->onCreate()")
    }
    override fun onLowMemory() {

    }
    override fun onTerminate() {

    }
    override fun onTrimMemory(level: Int) {

    }
    override fun onConfigurationChanged(newConfig: Configuration) {

    }
    companion object {
        lateinit var app: Application
    }
}
  1. 最后就在lib_common中的BaseApplication中使用ServiceLoader加载出每个模块实现的ApplicationImpl,Application执行每个生命周期的时候分发给每个组件:
class BaseApplication : Application() {
    private var mApplicationList: List<IApplication> = ServiceLoader.load(IApplication::class.java, javaClass.classLoader).toList()
    override fun attachBaseContext(base: Context?) {
        super.attachBaseContext(base)
        mApplicationList.forEach {
            it.attachBaseContext(this)
        }
    }
    override fun onCreate() {
        super.onCreate()
        mApplicationList.forEach {
            it.onCreate()
        }
    }

    override fun onLowMemory() {
        super.onLowMemory()
        mApplicationList.forEach {
            it.onLowMemory()
        }
    }

    override fun onTerminate() {
        super.onTerminate()
        mApplicationList.forEach {
            it.onTerminate()
        }
    }

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        mApplicationList.forEach {
            it.onTrimMemory(level)
        }
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)
        mApplicationList.forEach {
            it.onConfigurationChanged(newConfig)
        }
    }
}

四.组件之间的通信

前面说到,组件化的核心就是解耦,所以组件和组件之间是不能有依赖的,但业务模块不同,业务模块之间却存在互相通信的情况,核心情况有三种:

  • 页面跳转: A模块跳转B模块的页面,B模块跳转A模块的页面。
  • 数据调用: A模块获取B模块的数据,比如调用B模块的网络请求。
  • 事件通知: A模块通知B模块做相关更改。

组件之间页面跳转

前面说到,组件化的核心就是解耦,所以组件和组件之间是不能有依赖的,例如在首页模块点击购物车按钮需要跳转到购物车模块的购物车页面,两个模块之间没有依赖,所以不能在使用Activity的显示跳转来跳转页面了 ,虽然隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。可以借助路由框架来实现界面跳转,比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。

1. 引入Arouter

ARouterARouter 是一个用于帮助 Android App 进行组件化改造的框架,支持模块间的路由、通信、解耦。

  1. 首先需要在工程的 gradle.properties添加支持Androidx适配
android.enableJetifier=true 解决arouter适配Androidx的问题
  1. 在lib_common的build.gradle文件中添加依赖支持:
....
dependencies {
    api 'com.alibaba:arouter-api:1.5.2'
}
  1. 在业务组件及壳工程的build.gradle文件中添加使得在编译期间生成路径映射:
apply plugin: 'kotlin-kapt'
...
kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}
...
dependencies {
    ...
    kapt 'com.alibaba:arouter-compiler:1.5.2'
}
2. 适度使用路由框架

有些人引入路由框架后会把所有的 startActivity(intent) 都改成使用路由跳转。 笔者建议是, 路由框架只在业务组件间必要的交互上使用,其它非必要的情况能不用就不用 。因为阅读路由跳转的代码还需要知道 path 对应哪个 Activity,如果是 startActivity(intent) 就能直接看到跳转的 Activity,可读性更好。但是有路由跳转拦截需求的话还是得用路由跳转,如果能直接访问到所需的类且没有路由拦截的需求,用了路由和直接实现的效果是一样的,而使用路由会增加代码的复杂度,降低代码的阅读性,并没有必要使用路由。。

组件间数据数据调用

上文说到组件之间是没有依赖关系的,因此无法组件之间无法互相调用类的方法,但是很多时候又需要进行调用, 例如,module_home中有一个提现的按钮,点击的时候要获取到用户是否已经实名认证。而实名认证功能是module_mine中的能力。第一种方式是我们将实名认证功能下沉到lib_common中去那这样module_home和module_mine都可以获取到这个能力,但是别的业务模块也依赖了lib_common也会获取到这个能力,所以这种方式不好。 另外一种方式是使用接口进行通信, 接口通信概念并不是什么新颖的概念,我们可以理解为将一个模块当作SDK,提供接口+数据结构给其他需要的模块调用,从而实现通信。

  1. lib_component中创建IAuthService接口
interface IAuthService {
    fun isAuth(): Boolean
    fun getAuthTime(): String
}
  1. lib_component中创建IAuthService的默认实现类(为了防止module_home单独编译时没有IAuthService接口实现造成的空指针)
class EmptyAuthService : IAuthService {
    override fun isAuth(): Boolean {
        return false
    }

    override fun getAuthTime(): String {
        return ""
    }
}
  1. 创建接口注册获取的工厂类ServiceFactory
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 getAuthService(): IAuthService {
        val service = getService(IAuthService::class.java)
        return service as? IAuthService ?: EmptyAuthService()
    }
}
  1. 在module_mine中实现IAuthService定义的相关逻辑。
class AuthService : IAuthService {
    override fun isAuth(): Boolean {
        return true
    }

    override fun getAuthTime(): String {
        return "2020.9.1"
    }
}
  1. 在module_mine中的Application初始化代理类MineApplicationDelegate中进行注册。
class MineApplicationDelegate : ApplicationDelegate {
    override fun attachBaseContext(application: Application?, context: Context?) {

    }

    override fun onCreate(application: Application?) {
        Log.d("Application", "MineApplicationDelegate->onCreate()")
        ServiceFactory.register(AuthService())
    }
}
  1. 在module_home通过ServiceFactory获取到AuthService执行相关逻辑。
class HomeFragment : BaseFragment() {
    override fun getLayoutId(): Int = R.layout.home_fragment_page

    override fun initPage(savedInstanceState: Bundle?) {
        findViewById<View>(R.id.btn_collect).setOnClickListener {
            //到我的收藏页面需要登录
            val authService = ServiceFactory.getAuthService()
            if (loginService.isAuth()) {
                Toast.makeText(
                   context,
                    "Share from ${authService.getAuthTime()}", Toast.LENGTH_SHORT
                ).show()
            } else {
                Toast.makeText(
                    context,
                    "Not Auth", Toast.LENGTH_SHORT
                ).show()
            }
        }
    }
}

对于这种接口化的通信实现,ARouter的IProvider也已经实现,完全可以满足我们的要求。

  1. lib_component中创建IAuthService接口继承自ARouter的IProvider接口
interface IAuthService  : IProvider {
    fun isAuth(): Boolean
    fun getUserId(): String
}
  1. 在module_mine中实现IAuthService定义的相关逻辑。
@Route(path = "/mine/AuthServiceImpl")
class AuthService : IAuthService {
    override fun isAuth(): Boolean {
        return true
    }

    override fun getAuthTime(): String {
        return "2020.9.1"
    }
}
  1. 在module_home通过ARouter获取到AuthService执行相关逻辑。
class HomeFragment : BaseFragment() {
    override fun getLayoutId(): Int = R.layout.home_fragment_page
    override fun initPage(savedInstanceState: Bundle?) {
        findViewById<View>(R.id.btn_collect).setOnClickListener {
            //到我的收藏页面需要登录
            val authService = ARouter.getInstance().build("/mine/AuthServiceImpl").navigation() as? IAuthService
            if (loginService.isAuth()) {
                Toast.makeText(
                   context,
                    "Share from ${authService.getAuthTime()}", Toast.LENGTH_SHORT
                ).show()
            } else {
                Toast.makeText(
                    context,
                    "Not Auth", Toast.LENGTH_SHORT
                ).show()
            }
        }
    }
}

组件之间通知

组件之间的通信,例如A业务组件中有消息列表,而用户在B组件中操作某个事件后会产生一条新消息,需要通知A组件刷新消息列表,这样业务场景需求可以使用Android广播来解决,也可以使用第三方的事件总线来实现,比如EventBus。

  • BroadcastReceiver:系统提供,比较笨重,使用不够优雅。
  • EventBus:使用简单优雅,将发送这与接收者解耦,2.x使用反射方式比较耗性能,3.x使用注解方式比反射快得多。

而EventBus目前最被诟病的一点就是无法追溯事件,所以为了能更好的控制EvenBus,我们可以自建一个事件索引。

  1. 首先,我们可以创建一个Event类,作为传递数据的统一实体类。
public class Event<T> {
    private Object index;
    private T data;

    public Event() {

    }

    public Event(Object index, T data) {
        this.index = index;
        this.data = data;
    }

    public Object getIndex() {
        return index;
    }

    public void setIndex(Object index) {
        this.index = index;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}
  1. 在我们每次传递Event<T>时,必须为事件添加index索引标识。
  2. 而我们可以在公共模块类中创建一个索引类,用于记录所有的索引标识,这样我们就可以根据这些标识,简单的做到事件追溯。
class EventIndex {
    companion object {
        // 事件从修改昵称页面发送至用户信息页面
        const val EVENT_FROM_MODIFYNAME_TO_USERINFO = "From_ModifyName_To_UserInfo"
        // 事件从修改邮箱页面发送至用户信息页面
        const val EVENT_FROM_MODIFYEMAIL_TO_USERINFO = "From_ModifyEmail_To_UserInfo"
    }
}

还有值得注意的一点,在公共模块中,仅存放模块之间的通信索引标识;模块内使用EventBus通信,建立的索引放在对应模块内。

五.单工程及多工程组件化方案

单工程组件化方案

优点:

  1. 架构直观,可以让加入开发的新成员比较快速的理解项目的构建。
  2. 团队协作灵活,在项目开发阶段(特别是起始不稳定的阶段),有更多的module依赖选择,例如直接依赖project,或者通过aar/jar依赖,或者是maven依赖,可以更加快速的进行开发调试。

缺点:

  1. 团队协作的时候,大家都是在同一个app模块中做测试自己开发的模块,比较容易产生git冲突。
  2. 单工程方案没法做到代码权限管控,也不能做到开发人员职责划分明确,因为所有的module都在一个project中,每个人都可以修改他人负责的module,每个开发人员都可以对任意的组件进行修改,不是很安全。

多工程组件化方案

在 单工程Android组件化方案 中,由于所有组件都在同一个项目中,并且使用 implementation project(‘:组件名’) 方式依赖其他组件,这样就会导致很多问题。

  1. 编译很慢。由于所有的组件工程都在同一个项目中,并且组件之间或app壳工程会依赖其他组件,导致每次打包APP都需要把各个组件编译一次,如果项目中的组件达到十几个后,随着组件数量的增长,编译时间几乎呈指数性增加。
  2. 组件不方便引用。因为我们的组件是以源代码的形式置于项目中,如果另外一个项目也需要某个组件,这个时候就只能再复制一份代码到新项目中。这就导致一个组件存在于多个项目中,那么最终肯定无法保证这个组件的代码会不会被修改,也就是说组件已经无法保证唯一性了。
  3. 无法控制权限,也不方便混淆。因为项目中包含所有的组件源代码,这时候肯定没有办法控制代码权限了,假如某个组件是另外一个部门或公司提供给你用的,那么他们当然不希望给你源代码。

作为android开发人员我们知道在使用第三库的时候,这些开源库一般都是上传到maven或jcenter仓库上供我们工程引用。那么我们自己开发的业务组件能不能也传到maven或jcenter中央仓库让后再让壳工程像是以第三方库一样引用这些业务组件呢?答案是肯定的,不过我们的业务代码并不是真正上传到开源仓库上去,而是可以通过nexus公司内部搭建一个私有的maven仓库,将我们开发好的组件上传到这个私有的maven仓库上,然后内部开发人员就可以像引用三方库那样轻而易举的将组件引入到项目中了。这样每个业务组件都是一个工程,在这个工程里可以创建app 作为壳组件,它依赖 业务组件 运行调试功能,因此不需要 isModule 来控制独立调试,等业务组件调试完毕以后就可以将其上传到搭建好的远程仓库中供项目壳工程使用。例如,购物车组件 就是 新建的工程Cart 的 module_cart模块,业务代码就写在module_cart中即可。app模块是依赖module_cart。app模块只是一个组件的入口,或者是一些demo测试代码。 组件存在于独立工程 那么当所有业务组件都拆分成独立组件时,原本的工程就变成一个只有app模块的壳工程了,壳工程就是用来集成所有业务组件的。

  1. 发布组件的aar包 到公司的maven仓库。
  2. 然后在壳工程中就使用implementation依赖就可以了,和使用第三方库一样。另外aar包 分为 快照版本(snapshot) 和 正式(release)版本,快照版本是开发阶段调试使用,正式版本是正式发版使用。
多工程方案的优势

优点

  1. 多工程把每个组件都分割成单独的工程,代码权限可以明确管控。
  2. 集成测试时,通过maven引用来集成即可。并且业务组件和业务基础组件也可以 和 基础组件一样,可以给公司其他项目复用。
  3. 每个工程有自己单独的项目,减少项目的复杂度,缩短编译时间。
  4. 不同业务逻辑之间的代码是物理隔离开的,因此开发成员只需要负责自己的业务工程,不需要关注其他的功能模块,无法修改到其他人负责的功能模块,更加安全。

缺点

  1. 对于新加入的开发成员不是很友好,不能直观的了解项目的构建。
  2. 修改后需要上传到maven仓库,其他工程再次编译后才能感知到变化,多了上传和编译的时间。
  3. 因为每个功能都是单独的project,所以开发调式时,只能使用aar/jar或者maven来依赖需要的module,不如单工程模式灵活,在项目初期不稳定时的开发成本要高于单工程模式方案。

单工程和多工程组件化选择建议

  • 在个人开发或者小团队开发时,没有必要使用多工程方案模式,成本太高。
  • 开发小项目时,单工程模式足以,如果项目后期变的越来越大,可以在转多工程模式。
  • 在项目前期不稳定时,如果要使用多工程模式架构,对于基础组件与公共组件部分尽可能的要考虑完善,否则每次更改都需要重新发布aar/jar或者上传到maven仓库中,成本较高。

组件下沉

比如有首页,订单,视频,个人中心,设备等不同业务组件,一般利用到一些公共布局或资源,会往公共common组件下沉。但是也需要避免过渡下沉造成公共common库臃肿! 比如一些很基础的组件,就可以下沉到一个单独的项目中,作为二方库使用,通常而言,二方库是对三方库的再一次封装,当然也有完全自己实现功能的。比如日志库、图片库、网络库等十分基础的常用的组件和功能,就可以下沉为二方库。就公司层面而言,组件下沉有几方面的好处:

  • 代码隔离:降低因代码改动带来的风险
  • 功能复用:例如日志、网络等基础功能,封装好组件库后,一个公司内的所有 APK 都可以导入使用,避免了重复造轮子带来的浪费

对于组件,就公司层面而言,一般我们会把下沉的组件放到服务器上,方便公司的其他项目也一起使用。这就涉及到了组件项目的编译和上传过程。Google 提供的 library 插件可以把项目打包成一个 aar 包 。我们只需要关注如何将编译好的 aar 包上传到服务器即可。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值