Android 组件化项目详细实施方案

3,643 阅读15分钟
原文链接: mp.weixin.qq.com

导读: 组件化,插件化开发,将是未来Android中不可或缺的一环。今天来自张华洋的分享,张华洋的blog地址:http://blog.csdn.net/guiying712,点击阅读原文,可看对应原文,实施方案Demo: https://github.com/guiying712/AndroidModulePattern


1、Android组件化项目

在Android项目组件化之前,我们的项目都是像下图那样,一个单一工程下,根据不同的业务分几个文件夹,把需要的第三方库依赖下就开始开发了,这样的代码耦合严重,牵一发而动全身,删除某处代码就会到处报错,如果不解决掉报错的地方,就没法编译打包,而且这样的代码只适合于个人开发,尤其团队开发合并代码的时候那真是一个麻烦,相信大家都会深有体会,如果项目很大的话,修改一点简单的页面都要重新编译,Android编译速度大家也都见识过,每次打包都很耗时,并且这样的代码想做单元测试也是无从下手。

所以Android项目组件化就迫在眉睫了,组件化的方向就是由一个项目工程拆分成若干个模块工程,由App主工程提供统一的入口,每个业务独立的模块共享项目的Common依赖库。

2、Android组件化项目实施步骤

1)第一步:配置可自动将组件在Application和Library属性之间切换的方法

我们都知道Android Studio中的Module主要有两种属性,分别为 :

  • application属性,可以独立运行的Android程序,也就是我们的APP;

    apply plugin: ‘com.android.application’

  • library属性,不可以独立运行,一般是Android程序依赖的库文件;

    apply plugin: ‘com.android.library’

当我们在开发单独组件的时候,这个组件应该处于application模式,而当我们要将单独组件合并到主工程的时候,就需要将单独组从application模式改为library模式,也许你可以每次切换的时候都去build.gradle文件中去修改,但是你的项目要是有十几个组件的时候,你确定一个个去改?所以我们必须有一种能够动态切换组件模式的方法,做到一次修改,全局组件生效,这个问题就需要通过配置Gradle来解决了。

在Android Studio项目的根目录下有一个gradle.properties 文件,这个文件主要用来配置Gradle settings的,例如JVM参数等,想要了解这个文件的更多作用请查看www.gradle.org/docs/curren… 
,我们今天需要关注的是这个文件的一个特点:我们在gradle.properties 中配置的字段都可以在 build.gradle文件中直接读取出来,不用任何多余的代码。

现在我们在gradle.properties添加了一行代码,定义一个属性isModule(是否是组件开发模式,true为是,false为否):

每次更改“isModule”的值后,需要点击 "Sync Project" 按钮
isModule=true

然后我们在组件的build.gradle文件中读出这行代码:

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

因为gradle.properties中的数据类型都是String类型,而这里我们需要的是boolean值,所以这里要将String转换为boolean值,如果是‘组件开发模式”就将这个组件应用为application模式,如果不是就将这个组件应用为library模式,也就是一个库。 
这样我们的第一个问题就解决了,首先我们在gradle.properties中定义一个属性isModule,然后在每个组件的 build.gradle中把这个属性读取出来,每当我们需要从组件开发模式和APP整体开发模式转换时,只需要修改“isModule”的值即可,当然注释中也说了修改为这个属性值后,要点击AndroidStudio上的 “Sync Project”按钮同步下整个项目才能生效。

2)第二步:解决组件AndroidManifest和主工程AndroidManifest合并的问题

每个组件是由不同的成员单独开发的,这个时候组件就是一个独立的APP,那么这个组件就会有自己的“AndroidManifest.xml”,但是Android程序只有一个“AndroidManifest.xml”,当我们要把组件作为Library合并到主工程的时候,组件的“AndroidManifest.xml”和主工程的“AndroidManifest.xml”就会产生冲突,因为他们都有自己实现application类以及一些属性,还有自己的MAIN Activity,如果直接把张表合并到一起势必产生冲突。

解决思路就是:每个组件维护两张表,一张用于组件单独开发时使用,另一张用于合并到主工程的注册表中,每当增加一个Android系统的四大组件时都要同时给两张表中添加。

我们在上一节讲了可自动在组件的Application和Library属性之间切换的方法,有了这种方法,维护两张表就很方便了,首先在组件的main文件夹(和java文件夹平级)下创建两个文件夹,如下图:

然后在每个组件的*build.gradle中添加如下的代码:

sourceSets {
    main {
        if (isModule.toBoolean()) {
            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/release/AndroidManifest.xml'
            //release模式下排除debug文件夹中的所有Java文件
            java {
                exclude 'debug/**'
            }
        }
    }
}

这些代码的意思是:当在组件开发模式下,组件的注册表文件使用debug文件夹下的,其他情况使用release文件夹下的注册表文件;那么这两张表的区别在哪里呢?

下面的表示debug文件夹中的:

<application
    android:name="debug.CarApplication"
    android:icon="@mipmap/ic_car_launcher"
    android:label="@string/car_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme">
    <activity
        android:name=".query.QueryActivity"
        android:configChanges="orientation|screenSize|keyboard"
        android:screenOrientation="portrait"
        android:windowSoftInputMode="adjustPan|stateHidden">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity
        android:name=".scan.ScanActivity"
        android:screenOrientation="portrait" />
 </application>

下面的表是release文件夹中的:

 <application android:theme="@style/AppTheme">
    <activity
        android:name=".query.QueryActivity"
        android:configChanges="orientation|screenSize|keyboard"
        android:screenOrientation="portrait"
        android:theme="@style/AppTheme"
        android:windowSoftInputMode="adjustPan|stateHidden" />
    <activity
        android:name=".scan.ScanActivity"
        android:screenOrientation="portrait" />
 </application>
  1. debug文件夹中注册表的标签中指定了具体application类,而release文件夹中的则没有,

  2. debug文件夹中注册表的标签中添加一些application属性,而release文件夹中的则什么都没有添加;

  3. debug文件夹中的注册表指定QueryActivity为MAIN Activity,也就是要启动的 Activity,而release文件夹中的则没有;

3)第三步:解决组件和主工程的Application冲突问题以及组件单独开发初始化(共享)数据问题

当android程序启动时,android系统会为每个程序创建一个Application类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成Application 对象,但是如果我们自定义了Application,那就需要告知系统,实例化的时候,是实例化我们自定义的,而非默认的。但是我们在组件化开发的时候每一个组件可能都会有一个自己的Application类的对象,如果我们在自己的组件中开发时需要获取全局的Context,一般都会直接获取application对象,但是当所有组件要打包合并在一起的时候就会出现问题,因为最后程序只有一个Application,我们组件中自己定义的Application肯定没法使用,总不能每次打包的时候都把全局的application改一遍吧?

解决思路:首先创建一个叫做Common的Library,这个Common库中主要包含整个项目用到公共基类、工具类、自定义View等,例如BaseActivity、BaseFragment、BaseApplication等,并且我们的每一个组件都要依赖这个Common库,现在主要讲Common库中的BaseApplication怎么定义,下面是BaseApplication中的部分代码:

public class BaseApplication extends Application {
        private static BaseApplication sInstance;
        public static Context context;
        public static BaseApplication getIns() {
            return sInstance;
        }
        @Override
        public void onCreate() {
            super.onCreate();
            sInstance = this;
            context = this.getApplicationContext();
            if (isAppDebug(context)) {
                //只有debug模式才会打印日志
                Logger.init("Demo").logLevel(LogLevel.FULL);
            } else {
                Logger.init("Demo").logLevel(LogLevel.NONE);
            }
        }
    }

因为每个组件都依赖了Common库,所以每个组件都能够获取到BaseApplication.context,但是Android程序默认的是系统自己的Application这个类,要想使用自己的就要继承Application并且在AndroidManifest.xml中声明,因此我们先在自己的组件中创建一个组件Application并且继承于BaseApplication,然后在debug文件中的AndroidManifest.xml中声明:

public class CarApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        login();
    }
}

这样我们就可以在组件中使用全局的Context:BaseApplication.context了,但是还有一个问题,我们在自己的组件中定义了CarApplication,那么组件合并到主工程后,主工程也有自己的Application,这样又冲突了,其实这个问题第二节的代码就已经写出来了,我们只是在组件开发时才使用CarApplication,那么我们在合并到主工程的时候把这个代码排除掉不就行了嘛,直接上图:

我们在java文件夹下再建一个debug文件夹,把组件自己的application放在这个文件夹中,然后在build.gradle添加这行代码:

这样在合并到主项目时debug文件夹下的java文件就全部被排除了。并且你可以在组件的Application中做一些初始化的操作,比如登陆,然后把数据保存下来,供组件使用。

4)第四步:解决library重复依赖以及Sdk和依赖的第三方库版本号控制问题

重复依赖问题其实在开发中经常会遇到,比如你 compile 了一个A,然后在这个库里面又 compile 了一个B,然后你的工程中又 compile 了一个同样的B,就依赖了两次。 
默认情况下,如果是 aar 依赖,gradle 会自动帮我们找出新版本的库而抛弃旧版本的重复依赖。但是如果你使用的是 project 依赖,gradle 并不会去去重,最后打包就会出现代码中有重复的类了。

Library重复依赖的解决办法就是给整个工程提供统一的依赖第三方库的入口,在上一节讲解决Application冲突问题时我们建了一个Common库,这个库还有一个作用就是用来为整个项目提供统一的依赖第三方库的入口,我们把项目常用或者必须用到的库全部在Common库的build.gradle中依赖进来,例如Android support Library、网络库、图片加载库等,又因为每个组件都要依赖这个Common库,所以的build.gradle中就不在需要依赖任何其他库了,这样我们就有了统一的依赖第三方库的入口,添加、删除和升级库文件都只需要在Common库中去处理就好了。

下面是组件build.gradle的依赖配置:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':common')
}

当组件合并到主项目的时候,其实就是将组件打包成arr包,所以主工程中在组件开发模式下是还是要单独依赖Common库,等到合并的时候在去依赖其他组件,Common库就不用依赖了,下面是主工程 build.gradle的依赖配置:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    if (!isModule.toBoolean()) {
        compile project(':alert')
        compile project(':car')
    } else {
        compile project(':common')
    }
 }

另外一个问题就是我们每个组件的build.gradle中都要配置一些属性,例如compileSdkVersion、buildToolsVersion还有defaultConfig等,如果我们需要修改项目的compileSdkVersion版本号,那就麻烦了,那么多组的 build.gradle,每个都要去找到修改一遍,想想都头疼,所以我们要把这些build.gradle中都要配置的属性统一起来,类似于java中的静态常量,一处修改到处生效。首先我们在 项目(不是组件的)build.gradle中定义如下代码:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    if (!isModule.toBoolean()) {
        compile project(':alert')
        compile project(':car')
    } else {
        compile project(':common')
    }
}

然后在组件build.gradle中引用这些值,下面贴出的是Common库的build.gradle代码会和组件的 build.gradle有些许差异:

apply plugin: 'com.android.library'
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
 defaultConfig {
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode rootProject.ext.versionCode
    versionName rootProject.ext.versionName
 }
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), '
proguard-rules.pro'        }    } } dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    //Android Support    compile "com.android.support:appcompat-v7:$rootProject.
supportLibraryVersion"    compile "com.android.support:design:$rootProject.supportLibraryVersion"    compile "com.android.support:percent:$rootProject.supportLibraryVersion"    //网络请求相关    compile "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"    compile "com.squareup.retrofit2:retrofit-mock:$rootProject.retrofitVersion"    compile "com.github.franmontiel:PersistentCookieJar:$rootProject.
cookieVersion"    //稳定的    compile "com.github.bumptech.glide:glide:$rootProject.glideVersion"    compile "com.orhanobut:logger:$rootProject.loggerVersion"    compile "org.greenrobot:eventbus:$rootProject.eventbusVersion"    compile "com.google.code.gson:gson:$rootProject.gsonVersion"    //不稳定的    compile "com.github.mzule.activityrouter:activityrouter:
$rootProject.routerVersion"    compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion" }

这样我们修改compileSdkVersion、buildToolsVersion、defaultConfig的值或者依赖库文件的版本号都可以直接在项目build.gradle文件中直接修改了,修改完后整个项目也就都改过来了。

5)第五步:跨Module跳转问题,也是我们最重要的一步了

在组件化开发的时候,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,组件跟组件之间完全没有任何依赖,假如现在我从A组件跳转到B组件,并且要携带参数跳转,这时候怎么办呢?而且组件这么多怎么管理也是个问题,这时候就需要引入“路由”的概念了。

我在项目中使用了一个开源的“路由”库,github地址请点击:ActivityRouter,主页里会有详细的介绍,大家可以去了解一下。另外阿里巴巴也开源了一个组件路由,github地址请点击: ARouter;这两个都是现成拿来就能用的,当然有人可能比较好奇组件Router是什么原理,自己怎么开发,这里有一位作者写出了详细的教程,大家可以去学习下:Android路由实现

接下来我们就讲怎么将路由应用到我们的组件化项目中,首先我们要在项目(不是组件的)build.gradle中依赖下面的代码:

buildscript {
  dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  }
 }

为什么要使用android-apt呢?大家可以看下面的解释,或者自己去搜索:

然后在每个组件build.gradle中加入下面的代码:

apply plugin: 'com.neenbedankt.android-apt'
dependencies {
    compile 'com.github.mzule.activityrouter:activityrouter:1.2.2'
    apt 'com.github.mzule.activityrouter:compiler:1.1.7'
 }

接下来是在主工程的AndroidManifest.xml配置

< activity
android:name="com.github.mzule.activityrouter.router.RouterActivity"
    android:theme="@android:style/Theme.NoDisplay">
    < intent-filter>
        < action android:name="android.intent.action.VIEW" />
        < category android:name="android.intent.category.DEFAULT" />
        < category android:name="android.intent.category.BROWSABLE" />
        < data android:scheme="demo" />
    < /intent-filter>
< /activity>

接下来我们需要在每个组件的java目录下,声明这个组件,向下面的代码那样(声明了两个组件):

@Module("App")
public class AppModule {
}
@Module("Car")
public class Car { 
}

然后在主工程的Application 中声明需要添加到主工程中的所有组件:

@Modules({"App", "Car"})
public class DemoApplication extends BaseApplication {
    @Override
    public void onCreate() {
        super.onCreate();
    }
 }

到这里我们的组件和主工程之间的关系就建立起来了,组件的声明以及添加和删除就都已经解决了。接下来就是组件之间Activity的跳转吗,前面我们做了那么多都是在为Activity的跳转做准备。

首先我们在需要跳转的目标Activity上添加注解:

@Router("main")
public class MainActivity extends Activity {
    ...
 }

这样就可以通过 demo://main来打开MainActivity了。

这一步就算讲完了,至于Router更多进阶功能就要靠大家自己去:ActivityRouter 学习了。

6)Module之间的通信问题

如果在B组件中要通知A组件刷新列表,就要想办法解决组件间的通信问题,这个只要使用EventBus就能解决,并不是什么复杂问题。

7)资源名冲突问题

因为我们拆分出了很多组件,在合并到主工程的时候就有可能会出现资源名冲突问题,比如A组件和B组件都定义了同一个资源名。这个问题一般很很好解决,我们只需要在组件的 build.gradle中添加这样的代码:

resourcePrefix "组件名_"

但是设置了这个属性后有个问题,所有的资源名必须以指定的字符串做前缀,否则会报错,而且resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。所以我并不推荐使用这种方法来解决资源名冲突,我们项目中解决办法是增加资源命名规约,只要遵守这个命名规约就能规避资源名冲突问题。

3、Android组件化项目结语

到这里一个简单的组件化项目就搭建出来了,组件化相比于单一工程优势是显而易见的: 
1. 加快编译速度,提高开发效率 
2. 自由选择开发框架(MVC /MVP / MVVM /) 
3. 方便做单元测试 
4. 代码架构更加清晰,降低项目的维护难度 
5. 适合于团队开发

最后贴出Android组件化Demo地址:

https://github.com/guiying712/AndroidModulePattern

第一时间获得博客更新提醒,以及更多 android,源码分析,最新开源项目推荐,更多有价值的思考,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码