Android 组件化实战

Android 组件化实战

为什么要掌握组件化

  作为应用技术的研发人员,我们所掌握的技术要符合实际的应用,要适合于市场的需要,这一点我们可以从招聘市场的信息中就可以知晓一二:
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述  从最近的一些公司(字节跳动、京东、滴滴、百度和好未来等)的招聘信息中,我们可以看到还是有不少公司在岗位职责要求中,明确的提出来了对组件化技术的要求。虽然这并不是一项十分难以掌握的技术,但是如果在未来面试的时候,能够在这个技能点上对答如流,那么对于面试的结果还是会有很多积极的影响的。

  另外,单纯的从技术层面上说,所谓“技多不压身”,每一项技术都不是只存在自身特定范围内的封闭性,而是或多或少的也会有一些向外的延展性,与其它的一些领域,总会有些丝丝缕缕的相通点。多掌握一项技术,从这项技术中吸收到的一点点的营养,也许会在以后涉及其它领域的拓展和提升方面,起到有意或无意的帮助。

什么是组件化

  组件化,作为一种立足于项目整体结构的架构,有别于MVC、MVP、MVVM或者其它的“模型-视图”的架构,是一种“业务模块”或者说“工程模块”的架构形式,让一个项目实现了即可以“化整为零,又可以“合而为一”。

  组件化架构对项目的业务解耦、项目的开发方式、管理方式,以及项目架构的演进等方法都起了决定性的影响。学习组件化、掌握组件化、使用组合化,不管是对开发人员自身技能提升还是对项目开发效率的提高,都是大有裨益。

什么是组件化?根据百度百科的描述:
  组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。
定义 组件化是一种高效的处理复杂应用系统,更好的明确功能模块作用的方式。
目的 为了解耦:把复杂系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。
组件化编程 采用模块式开发方式,单个组件包括模板,数据结构,程序,样式四部份。
  组件的接口表达了由该组件提供的功能和调用它时所需要的参数。
  组件是可以单独开发、测试。允许多人同时协作,编写及开发、研究不同的功能模块。

APP组件化 安卓的组件化目标实现了将各个模块之间业务完全独立、解耦,不再出现直接的依赖关系,如图:
在这里插入图片描述  业务组件层各个组件不再出现横向的、直接的依赖关系,可以分别单独编译成一个APK安装包,进行安装、测试,也可以合并编译成一个完成的APK包。

  常规工程下打包方式:
在这里插入图片描述  一个项目工程,直接编译出一个完善的安装包。

  组件化后的打包方式:
在这里插入图片描述  打包方式可以灵活控制,只需通过修改配置文件中的开关参数,就可以实现各个模块即可以单独编译,也可以将各个模块合并打包成一个完整的应用程序。

为什么要组件化

  首先我们可以先对比以前的开发模式:

  • 常规模式架构:
    在这里插入图片描述  由上图可以看出,所有的代码放在主工程目录下边,最多也就是以各个不同的业务以包的形式进行区分。各业务相互之间的依赖关系不明确,并且难以控制,代码耦合度越来越高;业务分包的形式比较随意,不同的开发习惯难以统一约束;增加了代码管理的困难主,容易冲突和混乱。

  当然还有一部分开发者习惯以功能分包,比如,把项目的所有的适配器adapter放在一个包下,将activity放在一个包下。完全没有业务划分的概念,如果应用极小,业务极简单,这种划分倒也无可厚非。但是现在一般的应用功能都不会如此简单,这样的粗糙分包,让人难以从表面上知晓业务功能的大体脉络,也复用一些功能时,也不能直观的了解依赖的相关项。

  • 模块化架构
    在这里插入图片描述  将各个基础业务模块放在一个单独的 Module中,在主项目添加依赖关系。实际上只是将单一工程中的相关业务,根据功能模块划分移动了下位置,各模块之间仍然有很强的依赖关系。

  随着项目业务功能的扩展,功能模块越来越多,项目各模块之间的依赖关系,就会难以避免的走向越来越复杂的情况:如下图所示
在这里插入图片描述  各个模块之间的依赖关系也会变得起来越来越复杂,业务之前的耦合尤其是跨模块之间的耦合就会越来越严重,代码实现过程难以阅读,业务逻辑难以理解。

  在开发人员比较多的团队中,各个开发成员之间分开协作,各自负责不同模块的开发和维护,对其它模块的功能,尤其是具体的实现逻辑并不会十分的清楚。

  所有的这些因素就会导致某些功能业务的维护扩展变得更加困难,对团队整体的开发效率的提高和代码编写的质量提高产生极大的障碍,并且修改后的代码影响范围难以估计,进而也会影响对功能的测试。

  • 组件化架构
    在这里插入图片描述  各个模块业务独立,即可以单独编译成APK包,进行安装测试,也可以将所有组件合并编译成一个完成的APK包。一般情况下,app 模块只是一个壳工程,没有功能业务代码,只负责将项目合并打包。

组件化的优缺点

  • 组件化的优点:
  1. 将各个功能模块解耦,避免重复生产,节省开发成本,缩短开发周期;
  2. 各个组件分工开发,可以同时进行互不干扰,更适合有一定人员体量的开发团队,可以更合理的安排开发人员;
  3. 也有利于代码的管理,各个组件可以分别建立仓库,分别对不同小组的代码进行管理;
  4. 通过统一的规范制定,可以极大的把控好各个组件在开发过程中的统一;
  5. 给项目未来架构与其它技术的融合或者升级提供基础保障;
  6. 各组件实现单独编译打包,提高编译速度;
  7. 降低了系统的维护和扩展的成本;
  8. 各个组件可以单独进行测试,提高测试效率;
  • 组件化的缺点也是显而易见,比如:
  1. 增加了工程结构的复杂度;
  2. 需要额外的代码配置及路由框架引入;
  3. 需要让开发成员熟悉使用,及了解相应的开发规范,需要花费一定的时间成本;
  4. 为了做到模块之间的解耦,可能会有一些性能的消耗,和代码逻辑的增加;

组件化要点:

  实现组件化需要解决的几个问题:

1. 各个 module 能实现动态改变工程类型

  在普通项目结构中主工程类型是 com.android.application 其它的 module 作为依赖模块,其工程类型为 com.android.library,在组件化的项目中,需要 module 根据开关的配置,动态的改变 工程类型,实现在合并编译是作为依赖 module,在独立编译时为 application,打包成一个单独的 APK 文件。

2. 单独编译状态下的相关配置

  相关配置包括 AndroidManifest.xml 文件
在 library 状态下,module加载的配置文件 AndroidManifest.xml 中,不需要配置启动项 Activity, 和 application 生命周期类等,但在独立编译状态下是需要,这个时候就需要动态配置去加载不同的 xml 文件。

3. 不同编译状态下的生命周期管理

  android系统会为每一个程序运行时创建1个 Application 类的对象且仅创建1个,一般我们会在主项目的 AndroidManifest.xml 文件中注册一个 application 的生命周期实现类;

4. 组件间的跳转

  组件化后组件之间不存在依赖关系,以前经常使用的显式页面跳转方式,以及在跳转时参数的传递问题,因为对目标页面有依赖关系,已经不能使用,需要另寻方式处理;

5. 组件间的通信

  android系统本身就有应用内或者进程间的通信机制,还有第三方的事件总线的框架等等,那这些机制在组件化后还否是还可以以一种比较优雅、比较方便的方式继续使用,也是需要重新考虑的问题,否则就可能造成代码的臃肿。

示例程序说明

  本文还是结合具体的实例程序来一一说明实现过程,具体的代码示例,请在 github网站的 ComponentBasedAction (https://github.com/windfallsheng/ComponentBasedAction)项目查看。

  总体的项目结构的示意图:
在这里插入图片描述  大体描绘了示例程序中各业务分层及基本的依赖关系,和上面的组件化架构图基本保持一致。

组件化配置

  在Android中有三类工程,
一类是App应用工程,它可以生成一个可运行的APK应用;
一类是Library库工程,它可以生成AAR包给其他的App工程公用,就和我们的Jar一样,但是它包含了Android的资源等信息,是一个特殊的Jar包;
最后一类是Test测试工程,用于对App工程或者Library库工程进行单元测试。

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

  通过应用以上三种不同的插件,就可以配置我们的工程是一个Android App工程,还是一个Android Library工程,或者是一个Android Test测试工程,然后配合着Android Studio,就可以分别对他们进行编译、测试、发布等操作。

组件动态构建方式的开关配置

  在 gradle.properties 文件中添加对各个组件构建方式的开关配置:

#
# 每个组件是否单独编译:true为单独编译,false将作为被依赖的module编译;
#
#isComponentToModuleHome=true
isComponentToModuleHome=false
#isComponentToModuleMine=true
isComponentToModuleMine=false
#isComponentToModuleMessage=true
isComponentToModuleMessage=false
#isComponentToModuleLogin=true
isComponentToModuleLogin=false
#isComponentToModuleTemplate=true
isComponentToModuleTemplate=false

  建议对各个组件设置不同的变量来控制组件化编译,比如有的个组件单独编译需要,同时它可以加入对其它模块的依赖,来实现必要的功能业务,此时,可以将它所依赖的模块的组件化开关关闭,当作普通 library 库被依赖。

  同时注意我这里每个组件开关都上下挨着写了两遍,一行赋值 ture,下一行就赋值 false。这样处理是一个小技巧,如果是一次打开或着关闭所有组件的开关,可以使用编译工具的快捷键,如 Android Studio 的注释键 Ctrl + / ,长按这两个键不松开直到最后一行结束,光标自动下移,这样打开或者关闭的操作比较方便。

app 壳 Module

  app 这个 Module 里面不在承载任何的业务功能,只负责将所有组件合并编译,打包成一个完整项目的apk,可以设置一些项目基本配置,包括包名、混淆及权限注册、样式、主题等,并且将应用内所有组件的 Application 生命周期管理及初始化功能放在了这里,在合并编译时统一处理。
app的 AndroidManifest.xml 文件,其中注册了生命周期管理类 MyApplication。

AndroidManifest.xml 文件

  应用的 Application 生命周期管理
在 AndroidManifest.xml 文件 中注册了一个 MyApplication 类,负责处理应用内所有组件在合并编译时的生命周期函数执行,具体内容会在 组件化生命周期管理 部分详细说明。

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

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:name=".command.MyApplication"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ComponentBasedAction">
    </application>

</manifest>
app的 build.gradle 文件

  APP Module 除了在正常打包环境下要进行的配置,如:混淆、签名或者productFlavors等相关项外,在组件化状态下,主要不同的配置是依赖项的配置,需要根本配置的组件化开关,进行动态的依赖各个组件Module。

plugins {
    id 'com.android.application'
}

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]

    defaultConfig {
        applicationId "com.windfallsheng.componentbasedaction"
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
        product {
//            applicationIdSuffix 'componentbasedaction'
//            versionNameSuffix '1.1.0'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation project(':module_base')

    if (!isComponentToModuleMain.toBoolean()) {
        implementation project(':module_main')
    }
    if (!isComponentToModuleHome.toBoolean()) {
        implementation project(':module_home')
    }
    if (!isComponentToModuleMessage.toBoolean()) {
        implementation project(':module_message')
    }
    if (!isComponentToModuleMine.toBoolean()) {
        implementation project(':module_mine')
    }
    if (!isComponentToModuleLogin.toBoolean()) {
        implementation project(':module_login')
    }
}

业务组件的 Module

  业务组件的这个 Module 主要注意 build.gradle、AndroidManifest.xml 文件中必须要进行配置的地方,其它的一些额外添加的配置项,对组件化本身没有太大影响的部分。

业务组件内的 gradle文件 配置

  实现动态处理组件构建方式类型、动态配置组件的 ApplicationId 和 动态的加载两套不同的 AndroidManifest 文件。

if (isComponentToModuleHome.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]

    defaultConfig {
        if (isComponentToModuleHome.toBoolean()) {
            applicationId "com.windfallsheng.componentbasedaction.module_home"
        }
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"

        // Enabling multidex support.
        multiDexEnabled true

        // 这个配置只能限制xml文件里的资源,对于图片资源还是通过命名区别;
        resourcePrefix "home_"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    /**
     * 通过修改SourceSets中的属性,可以指定哪些源文件(或文件夹下的源文件)
     * 要被编译,哪些源文件要被排除。
     */
    sourceSets {
        main {
            if (isComponentToModuleHome.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                // 集成开发模式下排除 module_java 文件夹中的所有Java文件
                java {
                    exclude 'module_java/**'
                }
            }
        }
    }
}

dependencies {

    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

    implementation project(path: ':module_base')
    annotationProcessor rootProject.ext.dependencies["arouter-compiler"]

}

  通过组件 Module 的工程结构了解 AndroidManifest 等的处理方式,这些文件的路径也是可以根据自身偏好去更改的。

  另外,有一些组件 Module 中,在单独编译时需要添加一些额外的 Java 代码的,那么我们可以将这部分代码单独放在一个文件夹下,如图中的 module_java 文件目录,这样也方便管理。
在这里插入图片描述

业务组件内的清单文件配置

  业务组件正常的清单文件配置,一般没有其它功能需要的时候,可能就只是这样的:

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

</manifest>

  业务组件在单独编译时加载的清单文件的配置。可以看出这个和一个正常的 APP 的 AndroidManifest.xml 的配置逻辑相同。

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

    <application>
        <activity
            android:name=".views.LaunchActivity"
            android:theme="@style/AppLaunchActivityTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".views.AdvertisingActivity"
            android:theme="@style/AppLaunchActivityTheme" />
        <activity
            android:name=".views.MainActivity"
            android:launchMode="singleTask" />
    </application>

</manifest>

  我们可以再对比下其它组件内的配置有何不同,如 module_main:

  业务组件正常的清单文件配置:

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

    <application>
        <activity
            android:name=".views.LaunchActivity"
            android:theme="@style/LaunchActivityTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".views.AdvertisingActivity"
            android:theme="@style/LaunchActivityTheme" />
        <activity
            android:name=".views.MainActivity"
            android:launchMode="singleTask" />
    </application>

</manifest>

  业务组件在单独编译时加载的清单文件的配置:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.windfallsheng.componentbasedaction.module_main">

    <application
        android:name=".command.MainApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher_round"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ComponentBasedAction"
        tools:replace="android:label">

        <activity
            android:name=".views.LaunchActivity"
            android:theme="@style/LaunchActivityTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".views.AdvertisingActivity"
            android:theme="@style/LaunchActivityTheme" />
        <activity
            android:name=".views.MainActivity"
            android:launchMode="singleTask" />
    </application>

</manifest>

  通过对比可以发现,不同编译环境下加载的 AndroidManifest.xml 文件中的配置,只要满足当前的功能即可。

命名规范

  1. 包名命名

  每个组件的包名没有特别的要求,建议使用:应用包名 + 组件名。如果需要更改应用的包名,可以使用开发工具的快捷键对应用内所有组件包名或者各java类文件内的导入路径等统一进行修改处理。这样也方便我们将一些功能完善的组件移植到其它项目中时,减少一些修改文件的工作。

  1. 包路径命名

  与包名的命名路径相一致,对于各组件内有相同功能的文件,可以有对应一致的路径及命名。显得比较清晰、统一,也方便记忆、查找同类功能的类文件。

  1. 图片资源命名

  所有图片的命名需要加上所在组件名的前缀,如:home_xxx_xxx.png,以防止合并编译时资源冲突。

  1. value资源命名

  对于value资源下定义的 string.xml、color.xml等文件,xml文件内所有 string 或者 color 元素的“name”属性的值命名时,需要加上所在组件名的前缀 #FFBB86FC,如:

<color name="home_purple_200">#FFBB86FC</color>

,以防止合并编译时资源冲突。

  1. 布局资源文件命名

  所有布局文件的命名需要加上所在组件名的前缀,如:home_fragment_home_layout.xml,以防止合并编译时资源冲突。

组件化生命周期管理

  原来一个项目只需要实现一个 Application 的实例,完成应用中所有需要初始化的操作,并且在 AndroidManifest.xml 文件 中注册一次就可以。

  组件化以后,由于各个模块单独可实现编译,所以各个组件需要注册自己的 Application 实例,并且在自己的 实现类中完成自己需要的初始化业务,并且在很多时候某个组件可能在单独编译的时候会初始化一些业务,在合并编译的时候并不需要。这个时候需要 app 主 module能够灵活的管理到各个组件的生命周期,调用到相应的初始化业务即可。

  目前采用的一种处理方式,通过另外定义基类和各个组件自身的实现类,去剥离组件化状态下单独编译时和合并编译时的各组件生命周期的处理逻辑。这样也解耦了部分初始化的业务,也有利于此处功能的扩展。

  首先通过 UML 图例了解其中部分各相关类的关系,然后再分析具体的功能实现;
在这里插入图片描述
  不同状态下各生命周期的职责范围:
在这里插入图片描述  在合并编译的情况下,MyApplication 会通过内部业务,初始化调用各个模块自身的 ApplicationHelper实例,并且在不同的生命周期回调方法中调用各模块对应的业务;

  在组件化编译的情况下,各组件自身的 Application 会在调用自身组件化下的业务的同时,初始化调用自身的 ApplicationHelper实例,并且在不同的生命周期回调方法中调用各模块对应的业务;当然组件中有其它的依赖(依赖的组件被作为普通模块),则需要添加对其它模块对应的 ApplicationHelper实例,并且进行初始化调用。

  以下是具体的实现过程:

  首先应用内还是需要定一个 application 的基类:

package com.windfallsheng.componentbasedaction.module_base.command;

import android.app.Application;
import android.content.res.Configuration;
import android.util.Log;
import androidx.multidex.BuildConfig;
import androidx.multidex.MultiDexApplication;
import com.alibaba.android.arouter.launcher.ARouter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author: lzsheng
 */
public abstract class BaseApplication extends MultiDexApplication {

    private static Application mApplication;
    private final String TAG = "BaseApplication";
    /**
     * 各个Module中针对各个Module自身特有的初始化业务;
     */
    private List<BaseApplicationHelper> mApplicationHelperList = new ArrayList<>();

    public static Application getInstance() {
        return mApplication;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        this.mApplication = this;
        // start_01: 初始化各个module通用的功能,在分别打包时,各个module会分别进入这段初始化业务;
        initComponentSpecificService();
        initComponentCommonService();
        // end_01
        // 针对各个Module自身特有的初始化业务;
        handlelAllApplicationHelpersCreate();
    }

    /**
     * 初始化各个module通用的业务;
     */
    private void initComponentCommonService() {
        Log.d(TAG, "method:initComponentCommonService");
        // ARouter初始化相关
        if (BuildConfig.DEBUG) {
            //打印日志
            ARouter.openLog();
            //开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
            ARouter.openDebug();
        }
        ARouter.init(mApplication);
    }

    /**
     * 对于各个module在单独编译时,如果有必要的业务需要处理则重写此方法;
     */
    protected void initComponentSpecificService() {
        Log.d(TAG, "method:initComponentSpecificService");
    }

    /**
     * 处理注册初始化功能的业务实例,及调用各实例的onCreate()方法;
     */
    private void handlelAllApplicationHelpersCreate() {
        Log.d(TAG, "method:handlelAllApplicationHelpersCreate");
        // 首先注册具体实例;
        registeApplicationHelper();
        // 调用各实例的初始化方法
        mApplicationHelperList.stream()
                .forEach(applicationHelper -> {
                    Log.d(TAG, "method:handlelAllApplicationHelpersCreate#applicationHelper=" +
                            applicationHelper.getClass().getSimpleName());
                    applicationHelper.onCreate();
                });
    }

    /**
     * 注册各个Module中用于初始化功能的业务实例;
     */
    protected abstract void registeApplicationHelper();

    /**
     * 根据全路径名,通过反射注册相关实例;各组件单独编译时,其它组件会报异常
     *
     * @param className
     */
    protected void registerApplicationHelper(String className) {
        BaseApplicationHelper applicationHelper = null;
        try {
            Class<? extends BaseApplicationHelper> clazz = (Class<? extends BaseApplicationHelper>) Class.forName(className);
            Constructor<? extends BaseApplicationHelper> constructor = clazz.getConstructor(Application.class);
            applicationHelper = constructor.newInstance(BaseApplication.this);
        } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
            e.printStackTrace();
            Log.e(TAG, "method:registerApplicationHelper#e=" + e.getMessage());
        }
        if (applicationHelper != null) {
            mApplicationHelperList.add(applicationHelper);
        }
    }

    /**
     * 根据全路径名的数组,通过反射注册相关实例;各组件单独编译时,其它组件会报异常
     *
     * @param classNameArray
     */
    protected void registerApplicationHelperArray(String[] classNameArray) {
        for (String className : classNameArray) {
            BaseApplicationHelper applicationHelper = null;
            try {
                Class<? extends BaseApplicationHelper> clazz = (Class<? extends BaseApplicationHelper>) Class.forName(className);
                Constructor<? extends BaseApplicationHelper> constructor = clazz.getConstructor(Application.class);
                applicationHelper = constructor.newInstance(BaseApplication.this);
            } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException e) {
                e.printStackTrace();
                Log.e(TAG, "method:registerapplicationHelperArray#e=" + e.getMessage());
            }
            if (applicationHelper != null) {
                mApplicationHelperList.add(applicationHelper);
            }
        }
    }

    @Override
    public void onTerminate() {
        super.onTerminate();
        mApplicationHelperList.stream()
                .forEach(applicationHelper ->
                        applicationHelper.OnTerminate());
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        mApplicationHelperList.stream()
                .forEach(applicationHelper ->
                        applicationHelper.onLowMemory());
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mApplicationHelperList.stream()
                .forEach(applicationHelper ->
                        applicationHelper.configurationChanged(newConfig));
    }
}

  BaseApplicationHelper作为基类,定义的各个方法是和 application中的方法是相对应的,其它各组件会继承这个基类进行各自的实现,主要目的是将各组件在单独编译和合并编译时都会进行的初始化业务解耦出来。

package com.windfallsheng.componentbasedaction.module_base.command;

import android.app.Application;
import android.content.res.Configuration;

/**
 * @Author: lzsheng
 */
public class BaseApplicationHelper {

    protected Application mApplication;

    private BaseApplicationHelper() {
    }

    public BaseApplicationHelper(Application application) {
        this.mApplication = application;
    }

    public void onCreate() {
    }

    public void OnTerminate() {
    }

    public void onLowMemory() {
    }

    public void configurationChanged(Configuration configuration) {
    }
}

  业务组件的实现类,如:

package com.windfallsheng.componentbasedaction.module_home.command;

import android.app.Application;
import com.windfallsheng.componentbasedaction.component_lib.util.Logger;
import com.windfallsheng.componentbasedaction.module_base.command.BaseApplicationHelper;

/**
 * @Author: lzsheng
 */
public class HomeApplicationHelper extends BaseApplicationHelper {

    private final String TAG = "HomeApplicationHelper";

    public HomeApplicationHelper(Application application) {
        super(application);
    }

    @Override
    public void onCreate() {
        Logger.dl("method:onCreate#不同的模式下当前组件必须的初始化业务");
    }
}

  业务组件层继承自 BaseApplication 的实现类,主要是为了在组件化单独编译当前组件时,动态加载的 AndroidManifest.xml 文件中注册自己的 Application 生命周期管理类,如:

package com.windfallsheng.componentbasedaction.module_home.command;

import android.util.Log;
import com.windfallsheng.componentbasedaction.component_lib.util.Logger;
import com.windfallsheng.componentbasedaction.module_base.command.BaseApplication;

/**
 * @Author: lzsheng
 */
public class HomeApplication extends BaseApplication {

    private final String TAG = "HomeApplication";

    @Override
    protected void initComponentSpecificService() {
        Logger.dl(TAG, "method:initComponetSpecificService#单独编译时,初始化当前组件特有的业务");
    }

    @Override
    protected void registeApplicationHelper() {
        Log.d(TAG, "method:registeApplicationHelper#单独编译时,注册相关初始化业务类");
        registerTargetApplicationHelper(HomeApplicationHelper.class.getName());
    }
}

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.windfallsheng.componentbasedaction.module_home">

    <application
        android:name=".command.HomeApplication"
        tools:replace="android:label"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher_round"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ComponentBasedAction">

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

</manifest>

  app 模块的实现类:MyApplication 在合并编译的情况下,app壳工程中 AndroidManifest.xml 中注册的 MyApplication 会创建各个组件内的初始化 *ApplicationHelper 类,并跟据生命周期的回调状态进行调用,保证各组件业务同步进行。

package com.windfallsheng.componentbasedaction.command;

import android.util.Log;
import com.windfallsheng.componentbasedaction.module_base.command.BaseApplication;

/**
 * @Author: lzsheng
 */
public class MyApplication extends BaseApplication {

    private static final String TAG = "MyApplication";

    /**
     * 增加新的组件时,需要在此数组中添加相应的数据,保证模块上下文及其相关初始化成功;
     */
    private static final String[] APPLICATION_HELPER_NAME_LIST =
            {
//                    MyApplicationHelper.class.getName(),
                    "com.windfallsheng.componentbasedaction.module_main.command.MainApplicationHelper",
                    "com.windfallsheng.componentbasedaction.module_home.command.HomeApplicationHelper",
                    "com.windfallsheng.componentbasedaction.module_login.command.LoginApplicationHelper",
                    "com.windfallsheng.componentbasedaction.module_mine.command.MineApplicationHelper",
                    "com.windfallsheng.componentbasedaction.module_message.command.MessageApplicationHelper"
            };

    @Override
    protected void registeApplicationHelper() {
        Log.d(TAG, "method:registeApplicationHelper#项目合并编译时,注册所有相关初始化业务类");
        registerApplicationHelperArray(APPLICATION_HELPER_NAME_LIST);
    }
}

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

    <application
        android:name=".command.MyApplication"
        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.ComponentBasedAction"></application>

</manifest>

  综上所述,BaseApplication 的实现类用于各个组件单独编译时注册的生命周期管理类,同时处理单独编译的情况下,各组件自己的业务,BaseApplicationHelper 的实现类,主要是为了分离各组件内生命周期 application 类的不同编译状态下的初始化业务,在合并编译的时候,保证 app 主工程的生命周期初始化化时,能够调用到各组件内部的初始化业务。而在单独编译时,各组件内部注册的有自己的 aplication 实现类,也能够完整的完成自己的初始化业务。

  MyApplication 是一个关键,它需要能获取到,各个组件的 BaseApplicationHelper 的实现类,但是在组件化情况下,各个 module 单独存在,没有强依赖关系,由于这里也没有太复杂的逻辑,可以采用反射的方式获取各个组件的实现类实例,

  使用这样的方式,既不容易产生业务上或者编译上的冲突,也能够更加灵活的处理各组件的初始化业务。

组件间的跳转

  Intent 是一个消息传递对象,您可以用来从其他应用组件请求操作。尽管 Intent 可以通过多种方式促进组件之间的通信,但其基本用例主要包括以下三个:

  1. 启动 Activity
  2. 启动服务
  3. 传递广播

  Android 通过 Intent 是一个将要执行的动作的抽象的描述,一般来说是作为参数来使用,由 Intent 来协助完成 Android各个组件之间的通讯。

  Intent 用于描述要启动的 Activity,并携带任何必要的数据。一般情况下我们通过Intent机制实现跳转,隐式的或者显式的;
在这里插入图片描述Intent 分为两种类型:

  1. 显式 Intent:通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理 Intent 的应用。通常,您会在自己的应用中使用显式 Intent 来启动组件,这是因为您知道要启动的 Activity 或服务的类名。例如,您可能会启动您应用内的新 Activity 以响应用户操作,或者启动服务以在后台下载文件。

  2. 隐式 Intent :不会指定特定的组件,而是声明要执行的常规操作,从而允许其他应用中的组件来处理。例如,如需在地图上向用户显示位置,则可以使用隐式 Intent,请求另一具有此功能的应用在地图上显示指定的位置。

  在组件化的项目中,各组件之间没有依赖关系,所以无法通过显式 Intent 的方式,实现组件之间的页面跳转,所以只能通过 隐式 Intent 的方式实现。

  Android官网的注意说明:用户可能没有任何应用处理您发送到 startActivity() 的隐式 Intent。或者,由于配置文件限制或管理员执行的设置,可能无法访问应用。如果发生这样的情况,调用失败,应用也会崩溃。要验证 Activity 是否会接收 Intent,请对 Intent 对象调用 resolveActivity()。如果结果为非空,则至少有一个应用能够处理该 Intent,并且可以安全调用 startActivity()。如果结果为空,不要使用该 Intent。如有可能,您应停用发出该 Intent 的功能。以下示例说明如何验证 Intent 是否解析为 Activity。此示例没有使用 URI,但已声明 Intent 的数据类型,用于指定 extra 携带的内容。

// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");

// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}

  隐式启动虽然可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式 — 路由,支持模块间的路由、通信、解耦”,具体内容在
路由框架应用 部分讲述。

组件间的通信

  1. 广播

  广播是 Android 的四大组件之一,Android 应用与 Android 系统和其他 Android 应用之间可以相互收发广播消息,这与发布-订阅设计模式相似。
可以采用的有全局广播(BroadcastReceiver)和本地广播(LocalBroadcastReceiver)。
  本地广播只在应用内传播,效率更高、安全性更好,但目前已被废弃,不再过多介绍。全局广播,可以应用在组件间接的通信,但是使用不方便、性能不高、也有安全性风险。如果组件间通信多的话,造成代码的臃肿,也容易出现安全风险,所以还是需要寻找其它更合适的通信方式。

  1. 事件总线

  以EventBus为例,它本身就是一个使用比较简单,低耦合的一个框架。通过EventBus作为组件间通信的一种方式,因为组件之间没有不能有依赖关系,所以事件对象只能放在公共的依赖层,我们可以选择放在 base 层。

  1. 路由服务

  通过路由在组件间实现数据的交互,具体内容在
路由框架应用 部分讲述,

路由框架应用

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

功能介绍

  • 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  • 支持多模块工程使用
  • 支持添加多个拦截器,自定义拦截顺序
  • 支持依赖注入,可单独作为依赖注入框架使用
  • 支持InstantRun
  • 支持MultiDex(Google方案)
  • 映射关系按组分类、多级管理,按需初始化
  • 支持用户指定全局降级与局部降级策略
  • 页面、拦截器、服务等组件均自动注册到框架
  • 支持多种方式配置转场动画
  • 支持获取Fragment
  • 完全支持Kotlin以及混编
  • 支持第三方 App 加固(使用 arouter-register 实现自动注册)
  • 支持生成路由文档
  • 提供 IDE 插件便捷的关联路径和目标类
  • 支持增量编译(开启文档生成后无法增量编译)

使用过程

  1. 添加依赖和配置
  2. 添加注解
  3. 初始化SDK

组件通信 IProvider

  在公共的依赖层,可以选择 base 层创建一个接口,继承自 ARouter 提供的 IProvider 接口,定义一个抽象方法:

package com.windfallsheng.componentbasedaction.module_base.provider;

import com.alibaba.android.arouter.facade.template.IProvider;

public interface LoginProvider extends IProvider {

    String getLoginUserInfo();

}

  在提供数据的业务组件内新建一个该接口的实现类,实现提供给其它组件调用的方法,返回必要的数据:

package com.windfallsheng.componentbasedaction.module_login.provider;

import android.content.Context;
import com.alibaba.android.arouter.facade.annotation.Route;

@Route(path = "/login/LoginProviderImpl")
public class LoginProviderImpl implements LoginProvider{
    @Override
    public String getLoginUserInfo() {
        // 根据业务逻辑返回相应的数据;
        return null;
    }

    @Override
    public void init(Context context) {

    }
}

在目标调用业务组件,获取该接口,并调用方法:

        // 模块间服务调用
        LoginProvider loginProvider = (LoginProvider) ARouter.getInstance()
                .build("/login/LoginProviderImpl")
                .navigation();
        if (loginProvider != null) {
            String userInfoJson = loginProvider.getLoginUserInfo();
        }

  ARouter其它使用功能这里不再作过多的介绍,具体的使用以官方的文档为主。

组件化解耦分析

  在生命周期管理部分,我们谈了application的解耦处理方案,组件通信部分讲了

通用组件模板

  在一个成熟的组件化项目中,各个组件的基础配置及业务几乎是一样的,有几乎一样的配置文件,和几乎一样的业务类,只是有一些包名、路径名或者类名的不同而已。

  当我们在新建一个组件,也就是刚开始创建 Module 的时候,如果能自动生成相应的内容就更好了,可以节省很多我们手动的一些配置工作,但是很遗憾,Android Studio 开发工具不支持我们去自定义 Module 模板。

  不过我们仍然可以通过自己的方式,去化整为零,尽量减轻一些工作量,比如创建一个相对通用的 Module 模板,根据自身项目约定的一些规范,创建出相对完善的模板内容,放在一个独立的文件夹下。

  创建一个模板 Module,命名为:module_template,并完成通用的配置内容,以下贴出部分内容。

模板 Module 工程结构:
在这里插入图片描述
build.gradle的部分配置,如:

if (isComponentToModuleTemplate.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]

    defaultConfig {
        if (isComponentToModuleTemplate.toBoolean()) {
            applicationId "com.windfallsheng.componentbasedexample.module_template"
        }
        
        // ... 省略部分配置项

        // 这个配置只能限制xml文件里的资源,对于图片资源还是通过命名区别;
        resourcePrefix "template_"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }

    // ... 省略部分配置项
}

TemplateApplicationHelper 类:

package com.windfallsheng.componentbasedaction.module_template.command;

import android.app.Application;
import com.windfallsheng.componentbasedaction.lib_common.util.Logger;
import com.windfallsheng.componentbasedaction.module_base.command.BaseApplicationHelper;

/**
 * @Author: lzsheng
 */
public class TemplateApplicationHelper extends BaseApplicationHelper {

    private final String TAG = "TemplateAppHelper";

    public TemplateApplicationHelper(Application application) {
        super(application);
    }

    @Override
    public void onCreate() {
        Logger.dl(TAG, "method:onCreate#不同的模式下当前组件必须的初始化业务");
    }
}

  从上面的部分示例内容中可以看出,通常情况下,组件内容之间的不同主要是在命名上,而命名的格式规范我们又是可以相对统一的,我们只需要定义好模板内容中相关的命名,如:
组件名 module_template,
包名:com.windfallsheng.componentbasedaction.module_template,
内部的一些类名,如:com.windfallsheng.componentbasedaction.module_template.command.TemplateApplication。

  在这个模板 module 中 我们用 “template” 作为占位关键词,以后在导入这个 Module 后,我们可以直接利用开发工具搜索 “template” 这个关键词,基本上是只需要找到所有相关的文件,替换这个名字即可,有个别不能用快捷操作搜索到的文件或者路径名等,可以通过人工查找修改,这样其实已经节省了很多时间。

这样,创建新组件的流程就简化为:

  1. 导入通用的模板 Module,完成依赖配置;
  2. module内搜索占位关键词,并且替换或者重命名;
  3. 添加新组件构建方式的开关配置;

总结

  在组件化实施的过程当中汲取了大量业内先进开发者的经验,有了他们在社区的代码贡献、博客总结,疑难解答,等等无私的奉献行为,才有了后来者提升的机会,让同行及后来者走的更快、更远。

致敬不懈努力的开发者们。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

windfallsheng

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值