Android组件化开发

开发模式

为了便于区分,在这里将开发模式分为2种:一种是项目组件化开发模式,一种是单一工程开发模式。

单一工程开发模式

顾名思义,就是一个代码工程(Project)对应一个APP了,这个APP的所有业务功能都是集中在同一个工程里实现的。
在这里插入图片描述

上图是目前比较普遍使用的Android APP技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的;
捕获.PNG
上图单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。
然而随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,此时我们会面临如下几个问题:

  • 实际业务变化非常快,但是单一工程的业务模块耦合度太高,牵一发而动全身; 对工程所做的任何修改都必须要编译整个工程;
  • 功能测试和系统测试每次都要进行;
  • 团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率;
  • 不能灵活的对业务模块进行配置和组装;

组件化开发模式

捕获.PNG
集成模式—所有的业务组件被“app壳工程”依赖,组成一个完整的APP;
组件模式—可以独立开发业务组件,每一个业务组件就是一个APP;
app壳工程—负责管理各个业务组件,和打包apk,没有具体的业务功能;
业务组件—根据公司具体业务而独立形成一个的工程;
功能组件—提供开发APP的某些基础功能,例如打印日志、树状图等;
Main组件—属于业务组件,指定APP启动页面、主界面;
Common组件—属于功能组件,支撑业务组件的基础,提供多数业务组件需要的功能,例如提供网络请求功能;

APP组件化架构的目标是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为arr包集成到“app壳工程”中,组成一个完整功能的APP;

从组件化工程模型中可以看到,业务组件之间是独立的,没有关联的,这些业务组件在集成模式下是一个个library,被app壳工程所依赖,组成一个具有完整业务功能的APP应用,但是在组件开发模式下,业务组件又变成了一个个application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。
捕获.PNG
这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系,而Android中的路由实际就是对URL Scheme的封装;

如此规模大的架构整改需要付出更高的成本,还会涉及一些潜在的风险,但是整改后的架构能够带来很多好处:

  • 加快业务迭代速度,各个业务模块组件更加独立,不再出现业务耦合情况;
  • 稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量;
  • 迭代频繁的业务模块采用组件方式,各业务研发可以互不干扰、提升协作效率,并控制产品质量;
  • 为新业务随时集成提供了基础,所有业务可上可下,灵活多变;
  • 降低团队成员熟悉项目的成本,降低项目的维护难度;
  • 加快编译速度,提高开发效率;
  • 控制代码权限,将代码的权限细分到更小的粒度;

组件化实施流程

组件模式和集成模式

Android Studio中的Module主要有两种属性,分别为:

//application属性,可以独立运行的Android程序,也就是我们的APP 
apply plugin: ‘com.android.application’ 
//library属性,不可以独立运行,一般是Android程序依赖的库文件 
 apply plugin: ‘com.android.library’

Module的属性是在每个组件的 build.gradle 文件中配置的,当我们在组件模式开发时,业务组件应处于application属性,这时的业务组件就是一个 Android App,可以独立开发和调试;而当我们转换到集成模式开发时,业务组件应该处于 library 属性,这样才能被我们的“app壳工程”所依赖,组成一个具有完整功能的APP;

Gradle自动构建工具有一个重要属性,可以帮助我们完成这个事情。每当我们用AndroidStudio创建一个Android项目后,就会在项目的根目录中生成一个文件 gradle.properties,我们将使用这个文件的一个重要属性:在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来;那么我们在上面提到解决办法就有了实际行动的方法,首先我们在gradle.properties中定义一个常量值 isBuildModule(是否是组件开发模式,true为是,false为否):
每次更改“isModule”的值后,需要点击 "Sync Project"按钮

isBuildModule=false

然后我们在业务组件的build.gradle中读取 isBuildModule,但是 gradle.properties 还有一个重要属性: gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换;也就是说我们读到 isBuildModule是个String类型的值,而我们需要的是Boolean值,代码如下:

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

AndroidManifest合并

在 AndroidStudio 中每一个组件都会有对应的 AndroidManifest.xml,用于声明需要的权限、Android四大组件等,当项目处于组件模式时,业务组件的 AndroidManifest.xml 应该具有一个 Android APP 所具有的的所有属性,尤其是声明 Application 和要 launch的Activity,但是当项目处于集成模式的时候,每一个业务组件的 AndroidManifest.xml 都要合并到“app壳工程”中,要是每一个业务组件都有自己的 Application 和 launch的Activity,那么合并的时候肯定会冲突,试想一个APP怎么可能会有多个 Application 和 launch 的Activity呢?

我们可以分别为组件模式和集成模式的业务组件创建一个 AndroidManifest.xml,然后根据isBuildModule指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml。
捕获.PNG
我们在main文件夹下创建一个debug文件夹用于存放组件开发模式下业务组件的 AndroidManifest.xml,而 AndroidStudio 生成的 AndroidManifest.xml 则依然保留,并用于集成开发模式下业务组件的表单;然后我们需要在业务组件的 build.gradle 中指定表单的路径,代码如下:

sourceSets {     
      main {        
             jniLibs.srcDirs = ['libs']        
             if (isBuildModule.toBoolean()) {
                      manifest.srcFile 'src/main/debug/AndroidManifest.xml'        
              } else {             
                     manifest.srcFile 'src/main/AndroidManifest.xml'     
              }     
         } 
 }

首先是集成开发模式下的 AndroidManifest.xml,前面我们说过集成模式下,业务组件的表单是绝对不能拥有自己的Application和启动Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有,下面是一份标准的集成开发模式下业务组件的 AndroidManifest.xml:

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

然后是组件开发模式下的表单文件:

<?xml version="1.0" encoding="utf-8"?> 
<manifest xmlns:android="http://schemas.android.com/apk/res/android"     
package="com.example.module_news">      
       <application
             android:name="debug.MimeApplication"  
             android:allowBackup="true"              
             android:icon="@mipmap/public_navi_news_select"         
             android:label="@string/public_image"         
             android:roundIcon="@mipmap/public_launcher_round"         
             android:supportsRtl="true"         
             android:theme="@style/public_appTheme">          
                      <activity             
                           android:name="debug.NewsMainActivity">            
                                <intent-filter>                 
                                      <action android:name="android.intent.action.MAIN" />                   
                                      <category android:name="android.intent.category.LAUNCHER" />             
                                </intent-filter>        
                      </activity>    
                      <activity android:name=".NewsDetailActivity" />
         </application>  
</manifest>

可以看到组件模式下的业务组件表单就是一个Android项目普通的AndroidManifest.xml,会包含组件自己的Application以及启动Activity,我们可以在java文件夹新建一个debug文件夹,将这两个文件都放在debug文件夹下,然后在集成模式下,这两个文件都是不需要的,就可以在build.gradle文件中将它们剔除出我们的项目,重新改写下build.gradle。

sourceSets {     
      main {        
             jniLibs.srcDirs = ['libs']        
             if (isBuildModule.toBoolean()) {
                      manifest.srcFile 'src/main/debug/AndroidManifest.xml'        
              } else {             
                     manifest.srcFile 'src/main/AndroidManifest.xml'            
                     //集成开发模式下排除debug文件夹中的所有文件            
                     java {                 
                          exclude 'debug/**'            
                     }        
              }     
         } 
 }

集成模式下全局Context的获取

当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的。

我们在组件化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类。组件模式下很简单,如果我们想获取全局的Context,一般直接获取这个application 对象就可以了。但是在集成模式下程序只有一个 Application,我们组件中自己定义的 Application 肯定是没法使用的,因此我们需要想办法再任何一个业务组件中都能获取到全局的 Context。

我们可以在Common组件中定义一个BaseApplication,主要用于各个业务组件和app壳工程中声明的 Application 类继承用的,只要各个业务组件和app壳工程中声明的Application类继承了 BaseApplication,当应用启动时 BaseApplication 就会被动实例化,这样从 BaseApplication 获取的 Context 就会生效,也就从根本上解决了我们不能直接从各个组件获取全局 Context 的问题。

public class BaseApplication extends Application {

    private static BaseApplication sInstance;
    
    public static BaseApplication getIns() {
        return sInstance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
        ...
    }
    ...
 }
Context context = BaseApplication.getIns().getApplicationContext();

library依赖问题

先说一个问题,在组件化工程模型图中,module_news组件和module_music组件都依赖了commonres组件,而module_main又同时依赖了module_news组件和module_music组件,这时候就会有人问,你这样搞岂不是commonres组件要被重复依赖了?

其实大家完全没有必要担心这个问题,如果真有重复依赖的问题,在你编译打包的时候就会报错,如果你还是不相信的话可以反编译下最后打包出来的APP,看看里面的代码你就知道了。组件只是我们在代码开发阶段中为了方便叫的一个术语,在组件被打包进APP的时候是没有这个概念的,这些组件最后都会被打包成arr包,然后被app壳工程所依赖,在构建APP的过程中Gradle会自动将重复的arr包排除,APP中也就不会存在相同的代码了;

虽然组件是不会重复了,但是我们还是要考虑另一个情况,我们在build.gradle中compile的第三方库,例如AndroidSupport库经常会被一些开源的控件所依赖,而我们自己一定也会compile AndroidSupport库 ,这就会造成第三方包和我们自己的包存在重复加载,解决办法就是找出那个多出来的库,并将多出来的库给排除掉,而且Gradle也是支持这样做的,分别有两种方式:根据组件名排除或者根据包名排除,下面以排除support-v4库为例:

compile("com.alibaba:arouter-api:1.4.1") {
        exclude module: 'support-v4'  //根据组件名排除
        exclude group: 'android.support.v4'  //根据包名排除
    }

library重复依赖的问题算是都解决了,但是我们在开发项目的时候会依赖很多开源库,而这些库每个组件都需要用到,要是每个组件都去依赖一遍也是很麻烦的,尤其是给这些库升级的时候,为了方便我们统一管理第三方库,我们将给整个工程提供统一的依赖第三方库的入口,前面介绍的commonsdk库的作用之一就是统一依赖第三方开源库,因为其他业务组件都依赖了commonsdk库,所以这些业务组件也就间接依赖了commonsdk所依赖的开源库。

同时,为了方便进行依赖的版本管理,我们单独编写一个config.gradle,将第三方依赖的库信息以及版本信息放在这里。在项目根目录的build.gradle中apply这个config.gradle文件,所有模块的build.gradle就都可以引用config.gradle中的配置信息了。

build.gradle

ext {
    android = [
            compileSdkVersion: 28,
            buildToolsVersion: "28.0.3",
            minSdkVersion    : 19,
            targetSdkVersion : 28,
            versionCode      : 19,
            arouterversionName      : "1.0.1"
    ]

    version = [
            androidSupportSdkVersion: "28.0.0",
            butterknifeSdkVersion   : "8.8.1",
    ]

    dependencies = [
            //support
            "appcompat-v7"             : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
            "support-v4"               : "com.android.support:support-v4:${version["androidSupportSdkVersion"]}",

            //view
            "easyNavigationBar"        : 'com.github.forvv231:EasyNavigation:1.0.2',
            "butterknife"              : "com.jakewharton:butterknife:${version["butterknifeSdkVersion"]}",
            "butterknife-compiler"     : "com.jakewharton:butterknife-compiler:${version["butterknifeSdkVersion"]}",

            //tools
            "eventbus"                 : "org.greenrobot:eventbus:3.1.1",
            "multidex"                 : "com.android.support:multidex:1.0.1",
            "arouter"                  : "com.alibaba:arouter-api:1.4.1",
            "arouter-compiler"         : "com.alibaba:arouter-compiler:1.2.2",
    ]
}

commonres的build.gradle依赖信息:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api rootProject.ext.dependencies["appcompat-v7"]
    api rootProject.ext.dependencies["support-v4"]

    //tools
    api rootProject.ext.dependencies["eventbus"]
    api rootProject.ext.dependencies["butterknife"]
    api(rootProject.ext.dependencies["arouter"]) {
        exclude module: 'support-v4'
        exclude module: 'support-annotations'
    }
    annotationProcessor rootProject.ext.dependencies["arouter-compiler"]

    api project(':commonservice')
}

组件之间资源名冲突

我们拆分出了很多业务组件和功能组件,在把这些组件合并到“app壳工程”时候就有可能会出现资源名冲突问题,例如A组件和B组件都有一张叫做“ic_back”的图标,这时候在集成模式下打包APP就会编译出错,解决这个问题最简单的办法就是在项目中约定资源文件命名规约,比如强制使每个资源文件的名称以组件名开始,这个可以根据实际情况和开发人员制定规则。当然了万能的Gradle构建工具也提供了解决方法,通过在在组件的build.gradle中添加如下的代码:

//给 Module内的资源名增加前缀, 避免资源名冲突, 建议使用 Module 名作为前缀
android {
    resourcePrefix "music_" 
}

设置了这个属性后,所有的资源名必须以指定的字符串做前缀,否则编译就会报错,而且resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。

组件化项目的工程类型

在组件化工程模型中主要有:app壳工程业务组件功能组件3种类型,而业务组件中的Main组件和功能组件中的Common组件比较特殊,下面将分别介绍。

app壳工程

app壳工程是从名称来解释就是一个空壳工程,没有任何的业务代码,也不能有Activity,但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功能:

  1. app壳工程中声明了我们Android应用的 Application,这个 Application 必须继承自 Common组件中的
    BaseApplication(如果你无需实现自己的Application可以直接在表单声明BaseApplication),因为只有这样,在打包应用后才能让BaseApplication中的Context生效,当然你还可以在这个
    Application中初始化我们工程中使用到的库文件,还可以在这里解决Android引用方法数不能超过 65535
    的限制,对崩溃事件的捕获和发送也可以在这里声明。
  2. app壳工程的 AndroidManifest.xml
    是我Android应用的根表单,应用的名称、图标以及是否支持备份等等属性都是在这份表单中配置的,其他组件中的表单最终在集成开发模式下都被合并到这份
    AndroidManifest.xml 中。
  3. app壳工程的 build.gradle
    是比较特殊的,app壳不管是在集成开发模式还是组件开发模式,它的属性始终都是:com.android.application,因为最终其他的组件都要被app壳工程所依赖,被打包进app壳工程中,这一点从组件化工程模型图中就能体现出来,所以app壳工程是不需要单独调试单独开发的。另外Android应用的打包签名,以及buildTypes和defaultConfig都需要在这里配置,而它的dependencies则需要根据isBuildModule的值分别依赖不同的组件,在组件开发模式下app壳工程只需要依赖Common组件,或者为了防止报错也可以根据实际情况依赖其他功能组件,而在集成模式下app壳工程必须依赖所有在应用Application中声明的业务组件。

下面是一份 app壳工程 的 build.gradle文件:

apply plugin: 'com.android.application'

android {
    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    buildToolsVersion rootProject.ext.android["buildToolsVersion"]
    
    defaultConfig {
        applicationId "com.example.component"
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath true
            }
        }
    }
    buildTypes {
        debug {
            buildConfigField "boolean", "LOG_DEBUG", "true"
            buildConfigField "boolean", "USE_CANARY", "true"
            buildConfigField "boolean", "IS_BUILD_MODULE", "${isBuildModule}"
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        release {
            buildConfigField "boolean", "LOG_DEBUG", "false"
            buildConfigField "boolean", "USE_CANARY", "false"
            buildConfigField "boolean", "IS_BUILD_MODULE", "${isBuildModule}"
            minifyEnabled true
            shrinkResources true
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }

}

dependencies {
    implementation project(':commonres')
    annotationProcessor rootProject.ext.dependencies["butterknife-compiler"]
    annotationProcessor rootProject.ext.dependencies["arouter-compiler"]

    //需要依赖所有其他组件,其他组件中的表单最终在集成开发模式下都会被合并到壳工程的AndroidManifest.xml 中
    if (!isBuildModule.toBoolean()) {
        implementation project(":module_main")
        implementation project(":module_news")
        implementation project(":module_music")
        implementation project(":module_mime")
    }

}

功能组件和Common组件

功能组件是为了支撑业务组件的某些功能而独立划分出来的组件,功能实质上跟项目中引入的第三方库是一样的,功能组件的特征如下:

  1. 功能组件的 AndroidManifest.xml 是一张空表,这张表中只有功能组件的包名;

  2. 功能组件不管是在集成开发模式下还是组件开发模式下属性始终是: com.android.library,所以功能组件是不需要读取 gradle.properties 中的 isBuildModule 值的;另外功能组件的 build.gradle 也无需设置 buildTypes ,只需要 dependencies 这个功能组件需要的jar包和开源库。

Common组件除了有功能组件的普遍属性外,还具有其他功能:

  1. Common组件的 AndroidManifest.xml 不是一张空表,这张表中声明了我们 Android应用用到的所有使用权限 uses-permission 和 uses-feature,放到这里是因为在组件开发模式下,所有业务组件就无需在自己的 AndroidManifest.xm 声明自己要用到的权限了。

  2. Common组件的 build.gradle 需要统一依赖业务组件中用到的 第三方依赖库和jar包,例如我们用到的ARouter、Butterknife等等。

  3. Common组件中封装了Android应用的 Base类和网络请求工具、图片加载工具等等,公用的 widget控件也应该放在Common 组件中;业务组件中都用到的数据也应放于Common组件中,例如保存到 SharedPreferences 和 DataBase 中的登陆信息;

  4. Common组件的资源文件中需要放置项目公用的 Drawable、layout、sting、dimen、color和style 等等,另外项目中的 Activity 主题必须定义在 Common中,方便和 BaseActivity 配合保持整个Android应用的界面风格统一。

我的项目里Common组件命名为Commonres, 下面是Commonres组件的 build.gradle文件:

apply plugin: 'com.android.library'

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

    defaultConfig {
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath true
            }
        }
    }

//    resourcePrefix "public_" //给 Module 内的资源名增加前缀, 避免资源名冲突
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api rootProject.ext.dependencies["appcompat-v7"]
    api rootProject.ext.dependencies["support-v4"]

    //tools
    api rootProject.ext.dependencies["butterknife"]
    api rootProject.ext.dependencies["arouter"]
    api rootProject.ext.dependencies["eventbus"]

    annotationProcessor rootProject.ext.dependencies["arouter-compiler"]

    api project(':commonservice')

}

业务组件和Main组件

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

  1. 业务组件中要有两张AndroidManifest.xml,分别对应组件开发模式和集成开发模式。

  2. 业务组件在集成模式下是不能有自己的Application的,但在组件开发模式下又必须实现自己的Application并且要继承自Common组件的BaseApplication,并且这个Application不能被业务组件中的代码引用,因为它的功能就是为了使业务组件从BaseApplication中获取的全局Context生效,还有初始化数据之用。

  3. 业务组件有debug文件夹,这个文件夹在集成模式下会从业务组件的代码中排除掉,所以debug文件夹中的类不能被业务组件强引用,例如组件模式下的 Application 就是置于这个文件夹中,还有组件模式下开发给目标 Activity 传递参数的用的 launch Activity 也应该置于 debug 文件夹中;

  4. 业务组件必须在自己的 Java文件夹中创建业务组件声明类,以使 app壳工程 中的 应用Application能够引用,实现组件跳转;

  5. 业务组件必须在自己的 build.gradle 中根据 isBuildModule 值的不同改变自己的属性,在组件模式下是:com.android.application,而在集成模式下com.android.library;同时还需要在build.gradle配置资源文件,如 指定不同开发模式下的AndroidManifest.xml文件路径,排除debug文件夹等;业务组件还必须在dependencies中依赖Common组件,并且引入Router的注解处理器annotationProcessor,以及依赖其他用到的功能组件。

下面是一份普通业务组件的 build.gradle文件:

apply from:"../common_component_build.gradle"

android {
    resourcePrefix "news_" //给 Module 内的资源名增加前缀, 避免资源名冲突, 建议使用 Module 名作为前缀
}

dependencies {
}

我们把公用的逻辑抽出来放在common_component_build.gradle,代码如下:

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

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

    defaultConfig {
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionCode rootProject.ext.android["versionCode"]
        versionName rootProject.ext.android["versionName"]
        testInstrumentationRunner rootProject.ext.dependencies["androidJUnitRunner"]
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                includeCompileClasspath true
            }
        }
    }
    buildTypes {
        debug {
            buildConfigField "boolean", "LOG_DEBUG", "true"
            buildConfigField "boolean", "USE_CANARY", "true"
            buildConfigField "boolean", "IS_BUILD_MODULE", "${isBuildModule}"
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        release {
            buildConfigField "boolean", "LOG_DEBUG", "false"
            buildConfigField "boolean", "USE_CANARY", "false"
            buildConfigField "boolean", "IS_BUILD_MODULE", "${isBuildModule}"
            minifyEnabled true
            if (isBuildModule.toBoolean()) {
                shrinkResources true
            }
            zipAlignEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        targetCompatibility JavaVersion.VERSION_1_8
        sourceCompatibility JavaVersion.VERSION_1_8
    }

    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isBuildModule.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                //集成开发模式下排除debug文件夹中的所有文件
                java {
                    exclude 'debug/**'
                }
            }
        }
    }
}

dependencies {
    implementation project(':commonres')
    annotationProcessor rootProject.ext.dependencies["butterknife-compiler"]
    annotationProcessor rootProject.ext.dependencies["arouter-compiler"]
}

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

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

    <application>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

</manifest>

最后看一下Main组件的build.gradle:

apply from:"../common_component_build.gradle"

android {
    resourcePrefix "main_" //给 Module 内的资源名增加前缀, 避免资源名冲突, 建议使用 Module 名作为前缀
}

dependencies {
    //view
    implementation rootProject.ext.dependencies["easyNavigationBar"]
}

注:Main组件中启动Activity会引用其他三个组件的Fragment,但是在这里却没有依赖其他三个组件,那么是怎么找到其他组件的Fragment呢?其实通过ARouter发现组件和跳转是不需要依赖的,ARouter就是负责不同组件之间的路由的,ARouter的使用见下面章节。而在集成模式下壳工程是需要依赖所有的组件的,因为最终各个组件的表单都会被合并到壳工程的AndroidManifest.xml 中。

组件之间调用和通信-ARouter

我们所使用的原生路由方案一般是通过显式intent和隐式intent两种方式实现的,而在显式intent的情况下,因为会存在直接的类依赖的问题,导致耦合非常严重;而在隐式intent情况下,则会出现规则集中式管理,导致协作变得非常困难。而且一般而言配置规则都是在Manifest中的,这就导致了扩展性较差。除此之外,使用原生的路由方案会出现跳转过程无法控制的问题,因为一旦使用了StartActivity()就无法插手其中任何环节了,只能交给系统管理,这就导致了在跳转失败的情况下无法降级,而是会直接抛出运营级的异常。这时候如果考虑使用自定义的路由组件就可以解决以上的一些问题,比如通过URL索引就可以解决类依赖的问题;通过分布式管理页面配置可以解决隐式intent中集中式管理Path的问题;自己实现整个路由过程也可以拥有良好的扩展性,还可以通过AOP的方式解决跳转过程无法控制的问题,与此同时也能够提供非常灵活的降级方式。

基本使用

(1)首先 ARouter 这个框架是需要初始化SDK的,所以你需要在“app壳工程”中的应用Application(也可以是BaseApplication)中加入下面的代码,注意:在 debug 模式下一定要 openDebug:

@Override
public void onCreate() {
    super.onCreate();
    if (BuildConfig.DEBUG) {
        ARouter.openLog();  // 打印日志
        ARouter.openDebug();    // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
    }
    ARouter.init(this);// 尽可能早,推荐在Application中初始化
}

@Override
public void onTerminate() {
    super.onTerminate();
    ARouter.getInstance().destroy();
}

(2)首先我们需要在 Common 组件中的 build.gradle 将ARouter 依赖进来,方便我们在业务组件中调用:

android {
	...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
    }

dependencies {
  compile 'com.alibaba:arouter-api:1.2.2'
}

然后在每一个业务组件的 build.gradle 都引入ARouter 的 Annotation处理器

android {
	...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
    }

dependencies {
  annotationProcessor 'com.alibaba:arouter-compiler:1.1.3'
}

(3)添加注解,对于你需要路由到的Activity,需要使用Route注解,对于Route注解,必须初始化path路径,而且path必须至少存在两级以上,即像这样 /xx/xx …:

@Route(path = /news/NewsDetailActivity)
public class NewsDetailActivity extends BaseActivity {
...
}

(4)路由到该Activity时,使用以下方法(跳转如果不对应路径,框架会Toast说路径不匹配):

ARouter.getInstance().build("/news/NewsDetailActivity").navigation();

最基本的路由方案已经好了,现在编译就可以使用了。没错,就是这么简单。好了,看到这里我们就会发现,路径的标签如果多了就不是很好管理,所以更好的选择是写一个类,在这个类里面统一管理和维护路径标签,不仅利于维护也方便后期拓展,看到路径就一目了然,哇~这个路径对应的是登录界面,这个路径对应的是详情界面;

路由传参

ARouter.getInstance().build(RouterHub.MUSIC_DETAIL_ACTIVITY)
        .withString("source", "news")
        .withInt("index", 1)
        .withParcelable("item", new NewsItem(888, "news name"))
        .navigation();

那么目标Activity呢?

@Route(path = RouterHub.MUSIC_DETAIL_ACTIVITY)
public class MusicDetailActivity extends BaseActivity {

    @Autowired(name = "source")
    String source;
    @Autowired(name = "index")
    int index;
    @Autowired(name = "item")
    NewsItem newsItem;

    @Override
    protected int initView(@Nullable Bundle savedInstanceState) {
        return R.layout.music_activity_detail;
    }

    @Override
    protected void initData(@Nullable Bundle savedInstanceState) {
        if (newsItem != null) {
            Toast.makeText(this, "source:"+source+",index:"+index+",\nnewsItem:"+newsItem.toString(), Toast.LENGTH_LONG).show();
        }
    }
}

我们需要为参数声明字段,并用@Autowired注解表示,@Autowired可以填写name标识,依次来映射URL中的不同参数。最后使用ARouter.getInstance().inject(this);方法来注入初始化@Autowired注解的字段。一般这句放在BaseActivity或BaseFragment中即可,达到公用。

注:ARouter传递对象的时候,首先该对象需要Parcelable或者Serializable序列化,可能Parcelable这个序列化大家觉得手写起来比较麻烦,但是Android Studio已经有一些插件帮我们自动生成Parcelable序列化了(因为Android用Parcelable序列化优势会更加明显一些)

界面跳转动画

ARouter.getInstance().build(RouterHub.NEWS_DETAIL_ACTIVITY)
        .withString("source", "music")
        .withInt("index", 2)
        .withParcelable("item", new MusicItem(777, "music name"))
        .withTransition(R.anim.public_fade_in, R.anim.public_fade_out)
        .navigation(getContext());

这里经测试,我发现只有navigation带有context动画才生效。

startActivityForResult

很多时候我们路由到目标Activity,然后需要返回Result,即我们通常重写的startActivityForResult&onActivityResult方法。使用ARouter也很简单,如下:

ARouter.getInstance().build(RouterHub.NEWS_MAIN_ACTIVITY)
        .withString("source", "music")
        .withInt("index", 2)
        .withParcelable("item", new MusicItem(777, "music name"))
        .withTransition(R.anim.public_fade_in, R.anim.public_fade_out)
        .navigation(getActivity(), 77);

只需要在navigation()方法中添加参数了,第一个参数必须是Activity,第二个参数就是我们的requestCode,requestCode必须大于零。

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    switch (requestCode) {
        case 77:
            if (resultCode == RESULT_OK && data != null) {
                String result = data.getStringExtra("result");
                Toast.makeText(getContext(), result, Toast.LENGTH_LONG).show();
            }
            break;
    }
}

而如果从Fragment中用这种方式调用,发现不起作用了,我们可以使用另外一种请求方式,接受方式不变:

Postcard postcard = ARouter.getInstance().build(RouterHub.NEWS_DETAIL_ACTIVITY);
LogisticsCenter.completion(postcard);
Class<?> destination = postcard.getDestination();
Intent intent = new Intent(getContext(), destination);
intent.putExtra("source", "music");
intent.putExtra("index", 2);
intent.putExtra("item", new MusicItem(777, "music name"));
startActivityForResult(intent, 77);
getActivity().overridePendingTransition(R.anim.public_fade_in, R.anim.public_fade_out);

服务提供和调用

这里的服务不是Android四大组件的Service,更贴切的说,应该是一种接口,通过ARouter依赖注入找到其实现类,然后使用接口中的方法。

服务接口需要继承IProvider:

public interface INewsModuleService extends IProvider {

    String getNewsData();
}

实现类实现接口方法,并用@Route绑定服务路径:

@Route(path = RouterHub.NEWS_SERVICE)
public class NewsModuleService implements INewsModuleService {

    @Override
    public void init(Context context) {
    }

    @Override
    public String getNewsData() {
        return "This is news data";
    }
}

服务接口一般我们放在单独的组件中,大家都可以调用,这里我使用的commonservice,而服务提供方(服务的实现)则是放在各自的组件中。

其他组件获取服务时可以有两种方法,一种是通过Name获取,一种是通过Type获取:

//     INewsModuleService newsModuleService = (INewsModuleService)ARouter.getInstance().build(RouterHub.NEWS_SERVICE).navigation();  //通过Name获取
        INewsModuleService newsModuleService = ARouter.getInstance().navigation(INewsModuleService.class);  //通过Type获取
        String data = newsModuleService != null ? newsModuleService.getNewsData() : "";
        Toast.makeText(getContext(), data, Toast.LENGTH_LONG).show();

拦截器Interceptor

拦截器是ARouter这一款框架的亮点。说起拦截器这个概念,可能印象更加深刻的是OkHttp的拦截器,OkHttp的拦截器主要是用来拦截请求体(比如添加请求Cookie)和拦截响应体(打印返回信息),在真正的请求和响应前做一些判断和修改然后在去进行操作,大抵这就是拦截器的简单概念。

拦截器有很多用处,比如路由到目标页面时,检查用户是否登录,检查用户权限是否满足,如果不满足,则路由到相应的登录界面或者相应的路由界面。ARouter框架的拦截器是怎么实现的?只需要实现IInterceptor接口,并使用@Interceptor注解即可,并不需要注册就能使用。当然这也有了它的坏处,就是每一次路由之后,都会经过拦截器进行拦截,显然这样程序的运行效率就会降低。Interceptor可以定义多个,比如定义登录检查拦截器,权限检查拦截器等等,拦截器的优先级使用priority定义,priority数值越小,越先执行,优先级越高(优先级改成一样,项目编译就会直接报错!)。拦截器内部使用callback.onContiune()/callback.onInterrupt(),前者表示拦截器任务完成,继续路由;后者表示终止路由。

下面我们定义两个拦截器:

@Interceptor(name = "login", priority = 1)
public class LoginInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        String path = postcard.getPath();
        Log.i("hx","LoginInterceptor:process() path:"+path);
        IMimeModuleService mimeModuleService = ARouter.getInstance().navigation(IMimeModuleService.class);
        boolean isLogin = mimeModuleService.isLogined();
        if (isLogin) { // 如果已经登录不拦截
            callback.onContinue(postcard);
        } else {      // 如果没有登录
            switch (path) {
                // 不需要登录的直接进入这个页面
                case RouterHub.MIME_MAIN_ACTIVITY:
                    callback.onContinue(postcard);
                    break;
                // 需要登录的直接拦截下来
                default:
                    callback.onInterrupt(null);
                    ARouter.getInstance().build(RouterHub.MIME_MAIN_ACTIVITY).navigation();
                    break;
            }
        }
    }

    @Override
    public void init(Context context) {
        Log.i("hx","LoginInterceptor:init()");
    }
}
@Interceptor(name = "check", priority = 7)
public class CheckInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        String path = postcard.getPath();
        Log.i("hx","CheckInterceptor:process() path:"+path);
        callback.onContinue(postcard);
    }

    @Override
    public void init(Context context) {
        Log.i("hx","CheckInterceptor:init()");
    }
}

我们可以在navigation时传一个NavigationCallback回调,监控路由的情况:

ARouter.getInstance().build(RouterHub.NEWS_DETAIL_ACTIVITY)
        .withString("source", "music")
        .withInt("index", 2)
        .withParcelable("item", new MusicItem(777, "music name"))
        .withTransition(R.anim.public_fade_in, R.anim.public_fade_out)
        .navigation(getContext(), new NavigationCallback() {

            @Override
            public void onFound(Postcard postcard) {
                //路由目标被发现
                Log.i("hx", "onFound");
            }

            @Override
            public void onLost(Postcard postcard) {
                //路由丢失
                Log.i("hx", "onLost");
                //单独降级
                ARouter.getInstance().build(RouterHub.MIME_MAIN_ACTIVITY).navigation();
            }

            @Override
            public void onArrival(Postcard postcard) {
                //路由到达
                Log.i("hx", "onArrival");
            }

            @Override
            public void onInterrupt(Postcard postcard) {
                //路由被拦截
                Log.i("hx", "onInterrupt");
            }
        });

在这里插入图片描述
首先是两个拦截器的初始化,然后是跳转MusicDetailActivity,回调了NavigationCallback的onFound(),执行了LoginInterceptor里面的process()方法进行拦截,回调NavigationCallback的onInterrupt(),由于跳转MusicDetailActivity被优先级更高的LoginInterceptor拦截了,所以CheckInterceptor就拦截不到了。拦截后跳转MimeMainActivity,两个拦截器均没有进行拦截,成功跳转。

当然了,如果有些路由希望不经过任何的拦截器,ARouter很贴心的给出了一个绿色通道函数供我们使用,使用greenChannel()时所有的Interceptor将失效:

ARouter.getInstance().build(RouterHub.NEWS_MAIN_ACTIVITY).greenChannel().navigation();

那么,这个回调里面的 Postcard 又是什么意思?点进去源码看看,类注释写的一目了然:
在这里插入图片描述
翻译过来的类注释就是:一个包含路线图的容器。既然是路线图的容器,那肯定有些API会获取到相应的信息,改写下onFound,

@Override
public void onFound(Postcard postcard) {
    String group = postcard.getGroup();
    String path = postcard.getPath();
    Log.i("hx", "onFound group="+group+",path="+path);
}

在这里插入图片描述
通过Postcard可以获取到路径的组以及全路径,那么,路径的组(Group)又是什么?是这样,一般来说,ARouter在编译期框架扫描了所有的注册页面/字段/拦截器等,很明显运行期不可能一股脑全部加载进来,而是使用分组来管理,根据日志,打印了分组的信息,可以发现Group的值默认就是第一个 / /(两个分隔符) 之间的内容。

其实我们也可以自定义分组,来进行界面跳转。

自定义分组实现跳转界面

(1)类注解新增 group,赋值我们自定义的组名,(依旧统一写在一个类里面这样便于管理)

String GROUP_1 = "group1";//自定义的分组
@Route(path = RouterHub.MUSIC_DETAIL_ACTIVITY, group = RouterHub.GROUP_1)
public class MusicDetailActivity extends BaseActivity {
...
}

(2)在build方法里面(这是一个方法重载),添加我们的与之对应的组名

ARouter.getInstance().build(RouterHub.MUSIC_DETAIL_ACTIVITY, RouterHub.GROUP_1)...

在这里插入图片描述
通过日志显示,这里的组名已经被我们更改成自定义分组且成功完成了跳转。

自定义全局降级策略

ARouter提供的降级策略主要有两种方式,一种是通过回调的方式(单独降级);一种是提供服务接口的方式(全局降级)。我们分别来看看两种方式的使用方法。

单独降级:
还是通过上面讲到的NavigationCallBack:

ARouter.getInstance().build(RouterHub.NEWS_DETAIL_ACTIVITY)
        .withString("source", "music")
        .withInt("index", 2)
        .withParcelable("item", new MusicItem(777, "music name"))
        .withTransition(R.anim.public_fade_in, R.anim.public_fade_out)
        .navigation(getContext(), new NavigationCallback() {

            @Override
            public void onFound(Postcard postcard) {
                //路由目标被发现
                Log.i("hx", "onFound");
            }

            @Override
            public void onLost(Postcard postcard) {
                //路由丢失
                Log.i("hx", "onLost");
                //单独降级
                ARouter.getInstance().build(RouterHub.MIME_MAIN_ACTIVITY).navigation();
            }

            @Override
            public void onArrival(Postcard postcard) {
                //路由到达
                Log.i("hx", "onArrival");
            }

            @Override
            public void onInterrupt(Postcard postcard) {
                //路由被拦截
                Log.i("hx", "onInterrupt");
            }
        });

全局降级:

@Route(path = "/user/*")
public class DegradeServiceImpl implements DegradeService {
    @Override
    public void onLost(Context context, Postcard postcard) {
        Log.i("hx", "全局降级");
        //全局降级
        ARouter.getInstance().build(RouterHub.MIME_MAIN_ACTIVITY).navigation();
    }

    @Override
    public void init(Context context) {

    }
}
ARouter.getInstance().build("/user/info").navigation();

这个是定义登录时,如果使用了错误的路由方式,将路由到登录界面。注意到path="/user/**",表明只要是一次级的user的错误路由,都会传递到这里。因为项目会存在很多模块,这里定义的"/user/*" 只是识别用户模块的,而不会影响其他模块。

注:级策略主要是这两种实现方式,比较简单,各有千秋,可以结合自己的需求进行使用,不过需要注意一点,不能两种同时使用,单独降级的方式优先于全局降级,也就是如果同时使用两种方式,调用完单独降级策略后就不会再调用全局降级。

DEMO下载地址

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值