组件化_组件化开发 - CSDN
精华内容
参与话题
  • Android组件化方案

    万次阅读 多人点赞 2017-11-20 17:17:14
    随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的...

    版权声明:本文为博主原创文章,欢迎大家转载!
    转载请标明出处: http://blog.csdn.net/guiying712/article/details/55213884 ,本文出自:【张华洋的博客】


    Android组件化项目地址:Android组件化项目AndroidModulePattern

    Android组件化之终极方案地址:http://blog.csdn.net/guiying712/article/details/78057120

    1、为什么要项目组件化

    随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个工程师都要熟悉如此之多的代码,将很难进行多人协作开发,而且Android项目在编译代码的时候电脑会非常卡,又因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有更灵活的架构代替过去单一的工程架构。

    单一工程模型

    上图是目前比较普遍使用的Android APP技术架构,往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的;

    单一工程模型下的业务关系

    上图单一工程模型下的业务关系,总的来说就是:你中有我,我中有你,相互依赖,无法分离。
    然而随着产品的迭代,业务越来越复杂,随之带来的是项目结构复杂度的极度增加,此时我们会面临如下几个问题:

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

    为了满足各个业务模块的迭代而彼此不受影响,更好的解决上面这种让人头疼的依赖关系,就需要整改App的架构。


    2、如何组件化

    组件化工程模型

    上图是组件化工程模型,为了方便理解这张架构图,下面会列举一些组件化工程中用到的名词的含义:

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

    **
    Android APP组件化架构的目标是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,而在集成模式下又可以变为arr包集成到“app壳工程”中,组成一个完整功能的APP;
    从组件化工程模型中可以看到,业务组件之间是独立的,没有关联的,这些业务组件在集成模式下是一个个library,被app壳工程所依赖,组成一个具有完整业务功能的APP应用,但是在组件开发模式下,业务组件又变成了一个个application,它们可以独立开发和调试,由于在组件开发模式下,业务组件们的代码量相比于完整的项目差了很远,因此在运行时可以显著减少编译时间。

    组件化工程下的业务关系

    这是组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系,而Android中的路由实际就是对URL Scheme的封装;
    如此规模大的架构整改需要付出更高的成本,还会涉及一些潜在的风险,但是整改后的架构能够带来很多好处:

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


    3、组件化实施流程

    1)组件模式和集成模式的转换

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

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

    apply plugin: ‘com.android.application’

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

    apply plugin: ‘com.android.library’

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

    但是我们如何让组件在这两种模式之间自动转换呢?总不能每次需要转换模式的时候去每个业务组件的 Gralde 文件中去手动把 Application 改成 library 吧?如果我们的项目只有两三个组件那么这个办法肯定是可行的,手动去改一遍也用不了多久,但是在大型项目中我们可能会有十几个业务组件,再去手动改一遍必定费时费力,这时候就需要程序员发挥下懒的本质了。

    试想,我们经常在写代码的时候定义静态常量,那么定义静态常量的目的什么呢?当一个常量需要被好几处代码引用的时候,把这个常量定义为静态常量的好处是当这个常量的值需要改变时我们只需要改变静态常量的值,其他引用了这个静态常量的地方都会被改变,做到了一次改变,到处生效;根据这个思想,那么我们就可以在我们的代码中的某处定义一个决定业务组件属性的常量,然后让所有业务组件的build.gradle都引用这个常量,这样当我们改变了常量值的时候,所有引用了这个常量值的业务组件就会根据值的变化改变自己的属性;可是问题来了?静态常量是用Java代码定义的,而改变组件属性是需要在Gradle中定义的,Gradle能做到吗?

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

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

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

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

    这样我们第一个问题就解决了,当然了 每次改变isModule的值后,都要同步项目才能生效;

    2)组件之间AndroidManifest合并问题

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

    但是大家应该注意到这个问题是在组件开发模式和集成开发模式之间转换引起的问题,而在上一节中我们已经解决了组件模式和集成模式转换的问题,另外大家应该都经历过将 Android 项目从 Eclipse 切换到 AndroidStudio 的过程,由于 Android 项目在 Eclipse 和 AndroidStudio开发时 AndroidManifest.xml 文件的位置是不一样的,我们需要在build.gradle 中指定下 AndroidManifest.xml 的位置,AndroidStudio 才能读取到 AndroidManifest.xml,这样解决办法也就有了,我们可以为组件开发模式下的业务组件再创建一个 AndroidManifest.xml,然后根据isModule指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml,这样表单冲突的问题就可以规避了。

    业务组件的目录结构

    上图是组件化项目中一个标准的业务组件目录结构,首先我们在main文件夹下创建一个module文件夹用于存放组件开发模式下业务组件的 AndroidManifest.xml,而 AndroidStudio 生成的 AndroidManifest.xml 则依然保留,并用于集成开发模式下业务组件的表单;然后我们需要在业务组件的 build.gradle 中指定表单的路径,代码如下:

      sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                }
            }
        }

    这样在不同的开发模式下就会读取到不同的 AndroidManifest.xml ,然后我们需要修改这两个表单的内容以为我们不同的开发模式服务。

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

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.guiying.girls">
    
        <application android:theme="@style/AppTheme">
            <activity
                android:name=".main.GirlsActivity"
                android:screenOrientation="portrait" />
            <activity
                android:name=".girl.GirlActivity"
                android:screenOrientation="portrait"
                android:theme="@style/AppTheme.NoActionBar" />
        </application>
    
    </manifest>
    

    我在这个表单中只声明了应用的主题,而且这个主题还是跟app壳工程中的主题是一致的,都引用了common组件中的资源文件,在这里声明主题是为了方便这个业务组件中有使用默认主题的Activity时就不用再给Activity单独声明theme了。

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

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.guiying.girls">
    
        <application
            android:name="debug.GirlsApplication"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/girls_name"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity
                android:name=".main.GirlsActivity"
                android:screenOrientation="portrait">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
            <activity
                android:name=".girl.GirlActivity"
                android:screenOrientation="portrait"
                android:theme="@style/AppTheme.NoActionBar" />
        </application>
    
    </manifest>

    组件模式下的业务组件表单就是一个Android项目普通的AndroidManifest.xml,这里就不在过多介绍了。


    3)全局Context的获取及组件数据初始化

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

    但是我们在组件化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类,如果我们在自己的组件中开发时需要获取 全局的Context,一般都会直接获取 application 对象,但是当所有组件要打包合并在一起的时候就会出现问题,因为最后程序只有一个 Application,我们组件中自己定义的 Application 肯定是没法使用的,因此我们需要想办法再任何一个业务组件中都能获取到全局的 Context,而且这个 Context 不管是在组件开发模式还是在集成开发模式都是生效的。

    在 组件化工程模型图中,功能组件集合中有一个 Common 组件, Common 有公共、公用、共同的意思,所以这个组件中主要封装了项目中需要的基础功能,并且每一个业务组件都要依赖Common组件,Common 组件就像是万丈高楼的地基,而业务组件就是在 Common 组件这个地基上搭建起来我们的APP的,Common 组件会专门在一个章节中讲解,这里只讲 Common组件中的一个功能,在Common组件中我们封装了项目中用到的各种Base类,这些基类中就有BaseApplication 类

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

    这时候大家肯定都会有个疑问?不是说了业务组件不能有自己的 Application 吗,怎么还让他们继承 BaseApplication 呢?其实我前面说的是业务组件不能在集成模式下拥有自己的 Application,但是这不代表业务组件也不能在组件开发模式下拥有自己的Application,其实业务组件在组件开发模式下必须要有自己的 Application 类,一方面是为了让 BaseApplication 被实例化从而获取 Context,还有一个作用是,业务组件自己的 Application 可以在组件开发模式下初始化一些数据,例如在组件开发模式下,A组件没有登录页面也没法登录,因此就无法获取到 Token,这样请求网络就无法成功,因此我们需要在A组件这个 APP 启动后就应该已经登录了,这时候组件自己的 Application 类就有了用武之地,我们在组件的 Application的 onCreate 方法中模拟一个登陆接口,在登陆成功后将数据保存到本地,这样就可以处理A组件中的数据业务了;另外我们也可以在组件Application中初始化一些第三方库

    但是,实际上业务组件中的Application在最终的集成项目中是没有什么实际作用的,组件自己的 Application 仅限于在组件模式下发挥功能,因此我们需要在将项目从组件模式转换到集成模式后将组件自己的Application剔除出我们的项目;在 AndroidManifest 合并问题小节中介绍了如何在不同开发模式下让 Gradle 识别组件表单的路径,这个方法也同样适用于Java代码;

    业务组件的java目录结构

    我们在Java文件夹下创建一个 debug 文件夹,用于存放不会在业务组件中引用的类,例如上图中的 NewsApplication ,你甚至可以在 debug 文件夹中创建一个Activity,然后组件表单中声明启动这个Activity,在这个Activity中不用setContentView,只需要在启动你的目标Activity的时候传递参数就行,这样就就可以解决组件模式下某些Activity需要getIntent数据而没有办法拿到的情况,代码如下;

    public class LauncherActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            request();
            Intent intent = new Intent(this, TargetActivity.class);
            intent.putExtra("name", "avcd");
            intent.putExtra("syscode", "023e2e12ed");
            startActivity(intent);
            finish();
        }
    
        //申请读写权限
        private void request() {
            AndPermission.with(this)
                    .requestCode(110)
                    .permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,
                            Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE)
                    .callback(this)
                    .start();
        }
    
    }

    接下来在业务组件的 build.gradle 中,根据 isModule 是否是集成模式将 debug 这个 Java代码文件夹排除:

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

    4)library依赖问题

    在介绍这一节的时候,先说一个问题,在组件化工程模型图中,多媒体组件和Common组件都依赖了日志组件,而A业务组件有同时依赖了多媒体组件和Common组件,这时候就会有人问,你这样搞岂不是日志组件要被重复依赖了,而且Common组件也被每一个业务组件依赖了,这样不出问题吗?

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

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

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        compile("com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion") {
            exclude module: 'support-v4'//根据组件名排除
            exclude group: 'android.support.v4'//根据包名排除
        }
    }

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

    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.chrisbanes:PhotoView:$rootProject.photoViewVersion"
    
        compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
        compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
    
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }

    5)组件之间调用和通信

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

    这里我将介绍开源库的“ActivityRouter” ,有兴趣的同学情直接去ActivityRouter的Github主页学习:ActivityRouter,ActivityRouter支持给Activity定义 URL,这样就可以通过 URL 跳转到Activity,并且支持从浏览器以及 APP 中跳入我们的Activity,而且还支持通过 url 调用方法。下面将介绍如何将ActivityRouter集成到组件化项目中以实现组件之间的调用;

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

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }

    2、这一步我们需要先了解 APT这个概念,APT(Annotation Processing Tool)是一种处理注解的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。在这里我们将在每一个业务组件的 build.gradle 都引入ActivityRouter 的 Annotation处理器,我们将会在声明组件和Url的时候使用,annotationProcessor是Android官方提供的Annotation处理器插件,代码如下:

    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
    }

    3、接下来需要在 app壳工程的 AndroidManifest.xml 配置,到这里ActivityRouter配置就算完成了:

     <!--声明整个应用程序的路由协议-->
            <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="@string/global_scheme" /> <!-- 改成自己的scheme -->
                </intent-filter>
            </activity>
            <!--发送崩溃日志界面-->

    4、接下来我们将声明项目中的业务组件,声明方法如下:

    @Module("girls")
    public class Girls {
    }

    在每一个业务组件的java文件的根目录下创建一个类,用 注解@Module 声明这个业务组件;
    然后在“app壳工程”的 应用Application 中使用 注解@Modules 管理我们声明的所有业务组件,方法如下:

    @Modules({"main", "girls", "news"})
    public class MyApplication extends BaseApplication {
    }

    到这里组件化项目中的所有业务组件就被声明和管理起来了,组件之间的也就可以互相调用了,当然前提是要给业务组件中的Activity定义 URL。

    5、例如我们给 Girls组件 中的 GirlsActivity 使用 注解@Router 定义一个 URL:“news”,方法如下:

    @Router("girls")
    public class GirlsActivity extends BaseActionBarActivity {
    
        private GirlsView mView;
        private GirlsContract.Presenter mPresenter;
    
        @Override
        protected int setTitleId() {
            return R.string.girls_activity_title;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mView = new GirlsView(this);
            setContentView(mView);
            mPresenter = new GirlsPresenter(mView);
            mPresenter.start();
        }
    }

    然后我们就可以在项目中的任何一个地方通过 URL地址 : module://girls, 调用 GirlsActivity,方法如下:

           Routers.open(MainActivity.this, "module://girls");

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


    6)组件之间资源名冲突

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

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

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


    4、组件化项目的工程类型

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

    1)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则需要根据isModule的值分别依赖不同的组件,在组件开发模式下app壳工程只需要依赖Common组件,或者为了防止报错也可以根据实际情况依赖其他功能组件,而在集成模式下app壳工程必须依赖所有在应用Application中声明的业务组件,并且不需要再依赖任何功能组件。

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

    apply plugin: 'com.android.application'
    
    static def buildTime() {
        return new Date().format("yyyyMMdd");
    }
    
    android {
        signingConfigs {
            release {
                keyAlias 'guiying712'
                keyPassword 'guiying712'
                storeFile file('/mykey.jks')
                storePassword 'guiying712'
            }
        }
    
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
        defaultConfig {
            applicationId "com.guiying.androidmodulepattern"
            minSdkVersion rootProject.ext.minSdkVersion
            targetSdkVersion rootProject.ext.targetSdkVersion
            versionCode rootProject.ext.versionCode
            versionName rootProject.ext.versionName
            multiDexEnabled false
            //打包时间
            resValue "string", "build_time", buildTime()
        }
    
        buildTypes {
            release {
                //更改AndroidManifest.xml中预先定义好占位符信息
                //manifestPlaceholders = [app_icon: "@drawable/icon"]
                // 不显示Log
                buildConfigField "boolean", "LEO_DEBUG", "false"
                //是否zip对齐
                zipAlignEnabled true
                // 缩减resource文件
                shrinkResources true
                //Proguard
                minifyEnabled true
                proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                //签名
                signingConfig signingConfigs.release
            }
    
            debug {
                //给applicationId添加后缀“.debug”
                applicationIdSuffix ".debug"
                //manifestPlaceholders = [app_icon: "@drawable/launch_beta"]
                buildConfigField "boolean", "LOG_DEBUG", "true"
                zipAlignEnabled false
                shrinkResources false
                minifyEnabled false
                debuggable true
            }
        }
    
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
        if (isModule.toBoolean()) {
            compile project(':lib_common')
        } else {
            compile project(':module_main')
            compile project(':module_girls')
            compile project(':module_news')
        }
    }

    2)功能组件和Common组件

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

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

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

    下面是一份 普通 的功能组件的 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
        }
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
    }

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

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

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

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

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

    下面是一份 Common功能组件的 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
        }
    
    }
    
    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.chrisbanes:PhotoView:$rootProject.photoViewVersion"
    
        compile "com.jude:easyrecyclerview:$rootProject.easyRecyclerVersion"
        compile "com.github.GrenderG:Toasty:$rootProject.toastyVersion"
    
        //router
        compile "com.github.mzule.activityrouter:activityrouter:$rootProject.routerVersion"
    }
    

    2)业务组件和Main组件

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

    1、业务组件中要有两张AndroidManifest.xml,分别对应组件开发模式和集成开发模式,这两张表的区别请查看 组件之间AndroidManifest合并问题 小节。

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

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

    4、业务组件必须在自己的 Java文件夹中创建业务组件声明类,以使 app壳工程 中的 应用Application能够引用,实现组件跳转,具体请查看 组件之间调用和通信 小节;

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

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

    if (isModule.toBoolean()) {
        apply plugin: 'com.android.application'
    } else {
        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
        }
    
        sourceSets {
            main {
                if (isModule.toBoolean()) {
                    manifest.srcFile 'src/main/module/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                    //集成开发模式下排除debug文件夹中的所有Java文件
                    java {
                        exclude 'debug/**'
                    }
                }
            }
        }
    
        //设置了resourcePrefix值后,所有的资源名必须以指定的字符串做前缀,否则会报错。
        //但是resourcePrefix这个值只能限定xml里面的资源,并不能限定图片资源,所有图片资源仍然需要手动去修改资源名。
        //resourcePrefix "girls_"
    
    
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor "com.github.mzule.activityrouter:compiler:$rootProject.annotationProcessor"
        compile project(':lib_common')
    }

    Main组件除了有业务组件的普遍属性外,还有一项重要功能

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

            <activity
                android:name=".splash.SplashActivity"
                android:launchMode="singleTop"
                android:screenOrientation="portrait"
                android:theme="@style/SplashTheme">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>

    5、组件化项目的混淆方案

    组件化项目的Java代码混淆方案采用在集成模式下集中在app壳工程中混淆,各个业务组件不配置混淆文件。集成开发模式下在app壳工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在app壳工程中,各个业务组件的混淆配置规则都应该在app壳工程中的混淆配置文件中添加和修改。

    之所以不采用在每个业务组件中开启混淆的方案,是因为 组件在集成模式下都被 Gradle 构建成了 release 类型的arr包,一旦业务组件的代码被混淆,而这时候代码中又出现了bug,将很难根据日志找出导致bug的原因;另外每个业务组件中都保留一份混淆配置文件非常不便于修改和管理,这也是不推荐在业务组件的 build.gradle 文件中配置 buildTypes (构建类型)的原因。


    6、工程的build.gradle和gradle.properties文件

    1)组件化工程的build.gradle文件

    在组件化项目中因为每个组件的 build.gradle 都需要配置 compileSdkVersion、buildToolsVersion和defaultConfig 等的版本号,而且每个组件都需要用到 annotationProcessor,为了能够使组件化项目中的所有组件的 build.gradle 中的这些配置都能保持统一,并且也是为了方便修改版本号,我们统一在Android工程根目录下的build.gradle中定义这些版本号,当然为了方便管理Common组件中的第三方开源库的版本号,最好也在这里定义这些开源库的版本号,然后在各个组件的build.gradle中引用Android工程根目录下的build.gradle定义的版本号,组件化工程的 build.gradle 文件代码如下:

    buildscript {
        repositories {
            jcenter()
            mavenCentral()
        }
    
        dependencies {
            //classpath "com.android.tools.build:gradle:$localGradlePluginVersion"
            //$localGradlePluginVersion是gradle.properties中的数据
            classpath "com.android.tools.build:gradle:$localGradlePluginVersion"
        }
    }
    
    allprojects {
        repositories {
            jcenter()
            mavenCentral()
            //Add the JitPack repository
            maven { url "https://jitpack.io" }
            //支持arr包
            flatDir {
                dirs 'libs'
            }
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    // Define versions in a single place
    //时间:2017.2.13;每次修改版本号都要添加修改时间
    ext {
        // Sdk and tools
        //localBuildToolsVersion是gradle.properties中的数据
        buildToolsVersion = localBuildToolsVersion
        compileSdkVersion = 25
        minSdkVersion = 16
        targetSdkVersion = 25
        versionCode = 1
        versionName = "1.0"
        javaVersion = JavaVersion.VERSION_1_8
    
        // App dependencies version
        supportLibraryVersion = "25.3.1"
        retrofitVersion = "2.1.0"
        glideVersion = "3.7.0"
        loggerVersion = "1.15"
        eventbusVersion = "3.0.0"
        gsonVersion = "2.8.0"
        photoViewVersion = "2.0.0"
    
        //需检查升级版本
        annotationProcessor = "1.1.7"
        routerVersion = "1.2.2"
        easyRecyclerVersion = "4.4.0"
        cookieVersion = "v1.0.1"
        toastyVersion = "1.1.3"
    }
    

    2)组件化工程的gradle.properties文件

    在组件化实施流程中我们了解到gradle.properties有两个属性对我们非常有用:

    1、在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来,不管这个build.gradle是组件的还是整个项目工程的build.gradle;

    2、gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换;

    利用gradle.properties的属性不仅可以解决集成开发模式和组件开发模式的转换,而且还可以解决在多人协同开发Android项目的时候,因为开发团队成员的Android开发环境(开发环境指Android SDK和AndroidStudio)不一致而导致频繁改变线上项目的build.gradle配置。

    在每个Android组件的 build.gradle 中有一个属性:buildToolsVersion,表示构建工具的版本号,这个属性值对应 AndroidSDK 中的 Android SDK Build-tools,正常情况下 build.gradle 中的 buildToolsVersion 跟你电脑中 Android SDK Build-tools 的最新版本是一致的,比如现在 Android SDK Build-tools 的最新的版本是:25.0.3,那么我的Android项目中 build.gradle 中的 buildToolsVersion 版本号也是 25.0.3,但是一旦一个Android项目是由好几个人同时开发,总会出现每个人的开发环境 Android SDK Build-tools 是都是不一样的,并不是所有人都会经常升级更新 Android SDK,而且代码是保存到线上环境的(例如使用 SVN/Git 等工具),某个开发人员提交代码后线上Android项目中 build.gradle 中的 buildToolsVersion 也会被不断地改变。

    另外一个原因是因为Android工程的根目录下的 build.gradle 声明了 Android Gradle 构建工具,而这个工具也是有版本号的,而且 Gradle Build Tools 的版本号跟 AndroidStudio 版本号一致的,但是有些开发人员基本很久都不会升级自己的 AndroidStudio 版本,导致团队中每个开发人员的 Gradle Build Tools 的版本号也不一致。

    如果每次同步代码后这两个工具的版本号被改变了,开发人员可以自己手动改回来,并且不要把改动工具版本号的代码提交到线上环境,这样还可以勉强继续开发;但是很多公司都会使用持续集成工具(例如Jenkins)用于持续的软件版本发布,而Android出包是需要 Android SDK Build-tools 和 Gradle Build Tools 配合的,一旦提交到线上的版本跟持续集成工具所依赖的Android环境构建工具版本号不一致就会导致Android打包失败。

    为了解决上面问题就必须将Android项目中 build.gradle 中的 buildToolsVersion 和 GradleBuildTools 版本号从线上代码隔离出来,保证线上代码的 buildToolsVersion 和 Gradle Build Tools 版本号不会被人为改变。

    具体的实施流程大家可以查看我的这篇博文 AndroidStudio本地化配置gradle的buildToolsVersion和gradleBuildTools


    7、组件化项目Router的其他方案-ARouter

    在组件化项目中使用到的跨组件跳转库ActivityRouter可以使用阿里巴巴的开源路由项目:阿里巴巴ARouter

    ActivityRouter和ARouter的接入组件化项目的方式是一样的,ActivityRouter提供的功能目前ARouter也全部支持,但是ARouter还支持依赖注入解耦,页面、拦截器、服务等组件均会自动注册到框架。对于大家来说,没有最好的只有最适合的,大家可以根据自己的项目选择合适的Router。

    下面将介绍ARouter的基础使用方法,更多功能还需大家去Github自己学习;

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

        if (BuildConfig.DEBUG) {
                //一定要在ARouter.init之前调用openDebug
                ARouter.openDebug();
                ARouter.openLog();
           }
           ARouter.init(this);

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

    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //router
        compile 'com.alibaba:arouter-api:1.2.1.1'
    }

    3、然后在每一个业务组件的 build.gradle 都引入ARouter 的 Annotation处理器,代码如下:

    
    android {
        defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
            arguments = [ moduleName : project.getName() ]
            }
        }
        }
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor 'com.alibaba:arouter-compiler:1.0.3'
    }

    4、由于ARouter支持自动注册到框架,所以我们不用像ActivityRouter那样在各个组件中声明组件,当然更不需要在Application中管理组件了。 我们给 Girls组件 中的 GirlsActivity 添加注解:@Route(path = “/girls/list”),需要注意的是这里的路径至少需要有两级,/xx/xx,之所以这样是因为ARouter使用了路径中第一段字符串(/*/)作为分组,比如像上面的”girls”,而分组这个概念就有点类似于ActivityRouter中的组件声明 @Module ,代码如下:

    @Route(path = "/girls/list")
    public class GirlsActivity extends BaseActionBarActivity {
    
        private GirlsView mView;
        private GirlsContract.Presenter mPresenter;
    
        @Override
        protected int setTitleId() {
            return R.string.girls_activity_title;
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mView = new GirlsView(this);
            setContentView(mView);
            mPresenter = new GirlsPresenter(mView);
            mPresenter.start();
        }
    }

    然后我们就可以在项目中的任何一个地方通过 URL地址 : /girls/list, 调用 GirlsActivity,方法如下:

          ARouter.getInstance().build("/girls/list").navigation();

    8、结束语

    组件化相比于单一工程优势是显而易见的:

    1. 组件模式下可以加快编译速度,提高开发效率;
    2. 自由选择开发框架(MVC /MVP / MVVM /);
    3. 方便做单元测试;
    4. 代码架构更加清晰,降低项目的维护难度;
    5. 适合于团队开发;

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

    想要学习更多Android组件化知识,请查看 :Android组件化之终极方案


    感谢以下文章提供的帮助
    1. http://www.cnblogs.com/chenxibobo/p/6187954.html
    2. https://kymjs.com/code/2016/10/18/01/
    3. https://zhuanlan.zhihu.com/p/23388989
    4. https://zhuanlan.zhihu.com/p/23147164?refer=moduth

    感谢以下开源项目
    1. https://github.com/mzule/ActivityRouter
    2. https://github.com/alibaba/ARouter

    展开全文
  • Android 组件化,从入门到不可自拔

    千次阅读 2019-07-09 02:21:24
    组件化能够显著提高Android项目开发效率,支持不同业务组件单独打包或者组合打包,可以说是Android开发者必备技能。 本文通过一个极其简单的实践案例,梳理了组件化的配置过程,并辅以全部源码,希望对还没有应用...

    写在前面

    学习容易,总结挺难;
    输入容易,输出挺难;
    学会容易,讲清挺难。
    可不会总结,难以输出,无法讲清,算是真的学会吗?
    ——几句废话,警戒自己。

    项目源码已经放在github上:https://github.com/ZuoHailong/AndroidModuleDemo (文后附的还有链接)

    谈谈模块化

    要聊组件化,惯例是要谈谈模块化的,毕竟它与组件化确实有一些相同点,在组件化的项目中它也会与组件化发生关联。

    什么是模块化

    模块化开发,是每个开发者都熟悉的。就是将常用的UI、网络请求、数据库操作、第三方库的使用等公共部分抽离封装成基础模块,或者将大的业务上拆分为多个小的业务模块,这些业务模块又依赖于公共基础模块的开发方式。
    更宏观上,又会将这些不同的模块组合为一个整体,打包成一个完成的项目。

    模块化的好处

    模块化有哪些好处呢?

    • 复用
      首先,基础模块,可为业务模块所复用;
      其次,子业务模块,可为父业务模块,甚至不同的项目所复用。
    • 解耦
      降低模块间的耦合,避免出现一处代码修改,牵一发而动全身的尴尬局面。
    • 协同开发
      项目越来越大,团队人数越来越多,模块化开发可在尽量解耦的情况下,使不同的开发人员专注于自己负责的业务,同步开发,显著提供开发效率。
    • ……

    模块化的弊端

    那,模块化开发有没有什么弊端呢?
    有。

    任凭模块化做得多么好,还是跳不出组合在单一项目下的范围,项目规模越来越大,业务模块越来越多,团队人数越来越多,模块化开发渐渐出现了以下的问题:

    • 项目代码量越来越大,每次的编译速度越来越慢,哪怕几句代码的修改,都需要等待若干分钟等待编译运行查看执行结果,极大的降低了开发效率;
    • 业务模块越来越多,不可避免地产生越来越多且复杂的耦合,哪怕一次小的功能更新,也需要对修改代码耦合的模块进行充分测试;
    • 团队人数越来越多,却要求开发人员了解与之业务相关的每一个业务模块,防止出现此开发人员修改代码导致其他模块出现bug的情况,这个要求对于开发人员显然是不友好的;
    • ……

    那怎样解决模块化开发的这些弊端呢?

    当然是组件化!不然我下面扯个什么玩意儿?
    调皮一下

    聊聊组件化

    组件化可以说是Android中级开发工程师必备技能了,能有效解决许多单一项目下开发出现的问题。并且我要强调的是,组件化真的不难,还没搞过的小伙伴不要怂。

    什么是组件化

    组件,顾名思义,组装的零件,术语上叫做软件单元,可用于组装在应用程序中。
    从这个角度上看,组件化,要更关注可复用性、更注重关注点分离、功能单一、高内聚、粒度更小、是业务上能划分的最小单元,毕竟是“组装的零件”嘛!(观点来自——阿里巴巴·杨充,文后有感谢链接)
    从这个角度上看,组件化的粒度,确实要比模块化的粒度更小。

    看到这里,懵逼否?

    组件化的概念,有点云里雾里,并且对上述观点,我认为大家还是要在各自的开发中见仁见智。

    就我个人看法而言,要把组件化拆分到如此小的粒度,不可能,也没有必要。在组件化项目的实际开发中,组件化的粒度,是要比模块化的粒度更大的。

    组件化的好处

    首先要说的是,上述模块化的好处,组件化都有,不再赘述;上述模块化的弊端,组件化都给解决了,具体如下:

    • 组件,既可以作为library,又可以单独作为application,便于单独编译单独测试,大大的提高了编译和开发效率;
    • (业务)组件,可有自己独立的版本,业务线互不干扰,可单独编译、测试、打包、部署
    • 各业务线共有的公共模块开发为组件,作为依赖库供各业务线调用,减少重复代码编写,减少冗余,便于维护
    • 通过gradle配置文件,可对第三方库的引入进行统一管理,避免版本冲突,减少冗余库
    • 通过gradle配置文件,可对各组件实现library与application间便捷切换,实现项目的按需加载

    组件化实践

    首先要说明的是,下述是一个简单的不能再简单的组件化案例,只求帮助大家搭建起组件化的架构,不求实现什么具体的功能。

    如果对组件化实现的丰富功能感兴趣,可参考文后的感谢链接,阅读大神们的项目源码。

    九层之台,起于累土。我们还是先搭组件化的架构吧!

    组件化架构

    先上一张组件化项目整体架构图
    组件化项目架构图其中的“业务组件”,既可以单独打包为apk,又可以作为library按需组合为综合一些的应用程序。

    大多数开发者做组件化时面对的业务需求,都是上面这种情况。

    我司的需求略有不同,不是将子业务组件组合为整体应用程序,而需要将已有项目拆分给不同业务体系使用,在不同业务体系下,项目的逻辑和代码会有区别,且版本不一致。

    基于此,我搭建项目架构如下图所示,其中“m_moudle_main”是公司主要的、且逻辑和代码相同的业务组件,“b_moudle_north”和“b_moudle_south”是拆分出来的业务组件,管理各自私有的逻辑和代码,且版本有差别。
    我司项目的组件化架构从Android工程看,结构如下图所示:

    project结构图
    PS:取moudle名,手动加上“b_” “m_” “x_”这样的前缀,只是为了便于分辨组件层次。

    统一配置文件

    在项目根目录下,自建config.gradle文件,对项目进行全局统一配置,并对版本和依赖进行统一管理,源码如下

    /**
     * 全局统一配置
     */
    ext {
        /**
         * module开关统一声明在此处
         * true:module作为application,可单独打包为apk
         * false:module作为library,可作为宿主application的组件
         */
        isNorthModule = false
        isSouthModule = false
    
        /**
         * 版本统一管理
         */
        versions = [
                applicationId           : "com.hailong.amd",        //应用ID
                versionCode             : 100,                    //版本号
                versionName             : "1.0.0",              //版本名称
    
                compileSdkVersion       : 28,
                minSdkVersion           : 21,
                targetSdkVersion        : 28,
    
                androidSupportSdkVersion: "28.0.0",
                constraintlayoutVersion : "1.1.3",
                runnerVersion           : "1.1.0-alpha4",
                espressoVersion         : "3.1.0-alpha4",
                junitVersion            : "4.12",
                annotationsVersion      : "28.0.0",
                appcompatVersion        : "1.0.0-beta01",
                designVersion           : "1.0.0-beta01",
    
                multidexVersion         : "1.0.2",
    
                butterknifeVersion      : "10.1.0",
    
                arouterApiVersion       : "1.4.1",
                arouterCompilerVersion  : "1.2.2",
                arouterAnnotationVersion: "1.0.4"
        ]
    
        dependencies = [
                "appcompat"           : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",
                "constraintlayout"    : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",
                "runner"              : "androidx.test:runner:${versions["runnerVersion"]}",
                "espresso_core"       : "androidx.test.espresso:espresso-core:${versions["espressoVersion"]}",
                "junit"               : "junit:junit:${versions["junitVersion"]}",
                //注释处理器
                "support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}",
                "design"              : "com.google.android.material:material:${versions["designVersion"]}",
    
                //方法数超过65535解决方法64K MultiDex分包方法
                "multidex"            : "androidx.multidex:multidex:2.0.0",
    
                //阿里路由
                "arouter_api"         : "com.alibaba:arouter-api:${versions["arouterApiVersion"]}",
                "arouter_compiler"    : "com.alibaba:arouter-compiler:${versions["arouterCompilerVersion"]}",
                "arouter_annotation"  : "com.alibaba:arouter-annotation:${versions["arouterAnnotationVersion"]}",
    
                //黄油刀
                "butterknife"         : "com.jakewharton:butterknife:${versions["butterknifeVersion"]}",
                "butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}"
        ]
    }
    

    然后在project的build.gradle中引入config.gradle文件

    apply from: "config.gradle"
    

    基础公共组件

    基础公共组件common将一直作为library存在,所有业务组件都需要依赖common组件。common组件主要负责封装公共部分,如网络请求、数据存储、自定义控件、各种工具类等,以及对第三方库进行统一依赖。

    下图为我的common组件的包结构图:
    common包结构图
    上文有言,common组件还负责对第三方库进行统一依赖,这样上层业务组件就不需要再对第三方库进行重复依赖了,其build.gradle源码如下所示:

    apply plugin: 'com.android.library'
    apply plugin: 'com.jakewharton.butterknife'
    
    ……
    
    dependencies {
        // 在项目中的libs中的所有的.jar结尾的文件,都是依赖
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        //把implementation 用api代替,它是对外部公开的, 所有其他的module就不需要添加该依赖
        api rootProject.ext.dependencies["appcompat"]
        api rootProject.ext.dependencies["constraintlayout"]
        api rootProject.ext.dependencies["junit"]
        api rootProject.ext.dependencies["runner"]
        api rootProject.ext.dependencies["espresso_core"]
        //注释处理器,butterknife所必需
        api rootProject.ext.dependencies["support_annotations"]
    
        //MultiDex分包方法
        api rootProject.ext.dependencies["multidex"]
    
        //Material design
        api rootProject.ext.dependencies["design"]
    
        //黄油刀
        api rootProject.ext.dependencies["butterknife"]
        annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
    
        //Arouter路由
        annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
        api rootProject.ext.dependencies["arouter_api"]
        api rootProject.ext.dependencies["arouter_annotation"]
    
    }
    
    

    业务组件

    业务组件在集成模式下,是作为library存在,向上组合为整体性项目;在组件开发模式下,是作为application存在,可独立运行。以分支组件为例,其build.gradle源码如下:

    if (Boolean.valueOf(rootProject.ext.isModule_North)) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }
    apply plugin: 'com.jakewharton.butterknife'
    
    ……
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        //公用依赖库
        implementation project(':x_module_common')
        implementation project(':m_module_main')
        //黄油刀
        annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
        //Arouter路由
        annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    }
    
    

    至此,组件化架构的搭建基本完成。可还有几个问题,是组件化开发中所必需关注的,我们接着往下看。

    组件化必须要关注的几个问题

    Application

    在common组件中有BaseAppliaction,提供全局唯一的context,上层业务组件在组件化模式下,均需继承于BaseAppliaction。

    /**
     * Describe:基础Application,所有需要模块化开发的module都需要继承自此BaseApplication。
     * Created by ZuoHailong on 2019/4/11.
     */
    public class BaseApplication extends Application {
    
        //全局唯一的context
        private static BaseApplication application;
    
        @Override
        protected void attachBaseContext(Context base) {
            super.attachBaseContext(base);
            application = this;
            //MultiDexf分包初始化,必须最先初始化
            MultiDex.install(this);
        }
    
        @Override
        public void onCreate() {
            super.onCreate();
            initARouter();
        }
    
        /**
         * 初始化路由
         */
        private void initARouter() {
            if (BuildConfig.DEBUG) {
                ARouter.openLog();  // 打印日志
                ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
            }
            ARouter.init(application);// 尽可能早,推荐在Application中初始化
        }
    
        /**
         * 获取全局唯一上下文
         *
         * @return BaseApplication
         */
        public static BaseApplication getApplication() {
            return application;
        }
    

    applicationId管理

    可为不同组件设置不同的applicationId,也可缺省,在Android Studio中,默认的applicationId与包名一致。

    组件的applicationId在其build.gradle文件的defaultConfig中进行配置:

    if (Boolean.valueOf(rootProject.ext.isModule_North)) {
        //组件模式下设置applicationId
        applicationId "com.hailong.amd.north"
    }
    

    manifest管理

    组件在集成模式和组件化模式下,需要配置不同的manifest文件,因为在组件化模式下,程序入口Activity和自定义的Application是不可或缺的。

    在组件的build.gradle文件的android中进行manifest的管理:

       /*
        * java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,
        * 可以指定哪些源文件(或文件夹下的源文件)要被编译,
        * 哪些源文件要被排除。
        * */
        sourceSets {
            main {
                if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk
                    manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                    java {
                        //library模式下,排除java/debug文件夹下的所有文件
                        exclude '*module'
                    }
                }
            }
        }
    

    资源名冲突问题

    资源名冲突问题,相信大家多多少少都遇到过,以前最常见的就是第三方SDK导致的资源名冲突了。这个问题没有特别好的解决办法,只能通过设置资源名前缀 resourcePrefix 以及约束自己开发习惯进行解决。

    资源名前缀 resourcePrefix ,是在project的build.gradle中进行设置的:

    /**
     * 限定所有子类xml中的资源文件的前缀
     * 注意:图片资源,限定失效,需要手动添加前缀
     * */
    subprojects {
        afterEvaluate {
            android {
                resourcePrefix "${project.name}_"
            }
        }
    }
    

    这样设置完之后,string、style、color、dimens等中资源名,必须以设置的字符串为前缀,而layout、drawable文件夹下的shape他们的xml文件的命名,必须以设置的字符串为前缀,否则会报错提示。

    另外,资源前缀的设置对图片的命名无法限定,建议大家约束自己的开发习惯,自觉加上前缀。

    建议:
    将color、shape、style这些放在基础库组件中去,这些资源不会太多,且复用性极高,所有业务组件又都会依赖基础库组件。

    butterknife问题

    有博主在去年的博文中建议大家选用butterknife 8.4.0版本,我进行组件化构建时,目前的最新版10.1.0用起来是没有问题的,大家可放心将butterknife升级到最新版。

    butterknife存在是问题是控件ID找不到,只要将R替换为R2即可解决问题。需要注意的是,在如下代码示例外的位置,不要这样做,保持使用R即可,如 setContentView(R.layout.b_module_north_activity_splash)

    /**
     * Describe:
     * Created by ZuoHailong on 2019/4/19.
     */
    public class SplashActivity extends BaseActivity {
    
        @BindView(R2.id.btn_toMain)
        Button btnToMain;
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.b_module_north_activity_splash);
            ButterKnife.bind(this);
        }
    
        ……
    
        @OnClick(R2.id.btn_toMain)
        public void onViewClicked() {
        }
    }
    

    另外要注意的是,每一个使用butterknife的组件,在其 build.gradle 的 dependencies 都要配置注解处理器处理其 compiler 库

    apply plugin: 'com.jakewharton.butterknife'
    
    ……
    
    dependencies {
        ……
        //黄油刀
        annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
    }
    

    组件间跳转

    业务组件间不存在依赖关系,不可以通过Intent进行显式跳转,是要借助于路由的,我使用的是阿里的开源框架ARouter。

    我在案例中只使用了ARouter的基础的页面跳转功能,更复杂的诸如携带参数跳转、声明拦截器等功能的使用方法,大家可到Github上查看其使用文档。

    在每一个需要用到ARouter的组件的build.gradle文件中对其进行配置:

    android {
       ...
           defaultConfig {
             ...
            //Arouter路由配置
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                    includeCompileClasspath = true
                }
            }
        }
    }
    dependencies{
         ...
        //Arouter路由
        annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
    }
    

    跳转目标页面配置:

    @Route(path = "/main/MainActivity")
    public class MainActivity extends BaseActivity {
       ……
    }
    

    跳转发起页面的发起调用:

    ...
       ARouter.getInstance()
              .build("/main/MainActivity")
              .navigation();
    ...
    

    第三方sdk集成问题

    项目不可避免的要使用第三方sdk,如友盟分享、高德地图、腾讯 bugly 等,都需要在相应第三方的开发者中心使用包名、applicationId注册,获取相应的 appkey 、appsecret等。

    那么,在组件化开发中,到底应该使用哪个组件的包名、applicationId 到第三方平台进行注册呢?

    我的想法是使用基础库的包名、applicationId 进行注册,然后将相应的第三方sdk的功能封装为功能组件,供上层业务组件进行调用。

    但也存在一个问题,即在第三方的管理平台上,将无法区分相应统计信息到底属于哪一个组件化app。

    所以还是要根据业务要求、统计要求自己选择了。

    写在后面

    组件化优势多多,本文标题上说不可自拔,快感来的最快的,当属大大提升了编译速度了。

    开心的笑了起来
    项目源码已经放在github上:https://github.com/ZuoHailong/AndroidModuleDemo

    感谢

    鸿洋大神:wanAndroid

    吴蜀黍:Android组件化框架搭建

    杨充:Android组件化开发实践和案例分享

    展开全文
  • 目录 gradle语法 项目详细部署 gradle语法 开发环境 android studio 3.4.1 gradle版本 5.1.1 gradle是什么 gradle在AndroidStudio中的书写 在项目的主模块的gradle文件中写入如下代码: ...则会在控制台中输入如下...

    目录

    • gradle语法
    • 项目详细部署

    gradle语法

    开发环境 android studio 3.4.1

    gradle版本 5.1.1

    gradle是什么

    lRHp7Q.md.png

    gradle在AndroidStudio中的书写

    在项目的主模块的gradle文件中写入如下代码:
    lR7znS.md.png

    则会在控制台中输入如下日志信息:
    在这里插入图片描述

    gradle在多模块代码中进行配置统一

    练习gradle

    准备:创建一个app项目,使得该项目包含app模块和library模块。
    要求:通过创建一个新的gradle文件来统一各个模块中共有的配置。

    操作步骤:

    在项目的根目录下,新建名为config.gradle的配置文件,并且在项目根目录下,引入该配置文件。
    在这里插入图片描述

    测试:

    在config.gradle中声明属性 username。并且在模块lirary中的gradle使用username属性,编译项目,查看输出日志信息。

    config.gradle文件内容如下:
        //添加多个自定义属性,可以使用ext代码块
        ext{
            username = "zfc"
        }
    
    lirarymodule下的build.gradle内容如下:
        ...
        println "${username}"
        rootProject.ext.username = 1234
        println "${rootProject.ext.username}"
        ...
    

    编译项目得结果如下:
    在这里插入图片描述

    抽取统一配置
    //声明变量
    isRelease = true
    
    //打印变量的值
    print isRelease 
    
    //定义字典
    androidId=[
        compileSdkVersion:29,
        buildToolsVersion:"29.0.2",
        applicationId:"com.canjun.components",
        minSdkVersion:22,
        targetSdkVersion:29,
        versionCode:1,
        versionName:"1.0"
    ]
    
    appId = [
            "app":"com.canjun.components",
            "order":"com.canjun.components.order",
            "personal":"com.canjun.components.personal"
    ]
    
    url = [
            debug: "https://10.1.1.1/debug/rest",
            release: "https://10.1.1.1/release/rest",
    ]
    
    supportLirary = "28.0.1"
    
    dependencies = [
            appcompat:'androidx.appcompat:appcompat:1.1.0',
            constraintlayout:'androidx.constraintlayout:constraintlayout:1.1.3'
    ]
    
    使用config.gradle中的统一配置
    apply plugin: 'com.android.application'
    
    def androidId = rootProject.ext.androidId
    def appId = rootProject.ext.appId
    def support = rootProject.ext.dependencies
    
    android {
        compileSdkVersion androidId.compileSdkVersion
        buildToolsVersion androidId.buildToolsVersion
        defaultConfig {
            applicationId appId.app
            minSdkVersion androidId.minSdkVersion
            targetSdkVersion androidId.targetSdkVersion
            versionCode androidId.versionCode
            versionName androidId.versionName
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
        ...
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    //    implementation 'androidx.appcompat:appcompat:1.1.0'
    //    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    
    //    HashMap<String,String> map = new HashMap<>()
    
        support.each{ k,v-> implementation v}
    
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    }
    
    设置常量属性到BuildConfig类中
    buildConfigFiled("String","debug","\"${url.debug}\"")
    
    apply plugin: 'com.android.application'
    ...
    def url = rootProject.ext.url
    ...
    android {
        ...
        buildTypes {
            debug{ buildConfigField("String","debug","\"${url.debug}\"")
            }
            release {
                buildConfigField("String","debug","\"${url.release}\"")
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    }
    

    结果:会在主模块(app)目录下的BuildConfig中生成debug字段

    安卓项目中常见的gradle配置

    android{
        defaultConfig{
            ...
            ...
            //开启分包
            multiDexEnable true
            //设置分包配置
            multiDexKeepFile file('multidex-config.txt')
            
            //使用svg生成指定纬度的png图片
            vectorDrawbles.generatedDensities('xhdpi','xxhdpi')
            //使用v7包兼容(5.0以上版本)
            vectorDrawbles.useSupportLibrary = true
            //只保留指定和默认资源
            resConfig("zh-rCN")
            //ndk 默认保留架构
            ndk{
                abiFilters("armeabi","armeabi-v7a")
            }
            
            //设置源集属性
            sourceSets{
                main{
                    if(!isRelease){
                        manifest.srcFile ['src/main/AndroidManifest.xml']
                        java.srcDirs ['src/main/java']
                        res.srcDirs ['src/main/res']
                        resources.srcDirs ['src/main/resources']
                        aidl.srcDirs ['src/main/aidl']
                        assets.srcDirs ['src/main/assets']
                    }else{
                        mainfest.srcFile 'src/main/AndroidManifest.xml'
                    }
                }
            }
         
        }
        
        //signingConfigs 的定义一定要放在buildTypes的前面
        signingConfigs{
            debug{
                storeFile file('xxx/xxxx/.android/debug.keystore')
                storePassword 'android'
                keyAlias 'androiddebugkey'
                keyPassword 'android'
            }
            release{
                 // 签名证书文件
                storeFile file('D:/NetEase/netease.jks')
                // 签名证书的类型
                storeType "netease"
                // 签名证书文件的密码
                storePassword "net163"
                // 签名证书中密钥别名
                keyAlias "netease"
                // 签名证书中该密钥的密码
                keyPassword "net163"
                // 是否开启V2打包
                v2SigningEnabled true
            }
        }
        
        buildTypes{
            debug{
                signingConfig.debug   
            }
            
            release{
                signingConfig.release
                ...
            }
        }
        
        
    }
    
    //对adb配置
    adbOptions{
        //操作超时时间为5秒
        timeOutInMs = 5 * 1000_0
        
        //adb install选项配置i
        installOptions '-r','-s'
    }
    
    // 对 dx 操作的配置,接受一个 DexOptions 类型的闭包,配置由 DexOptions 提供
    dexOptions {
        // 配置执行 dx 命令是为其分配的最大堆内存
        javaMaxHeapSize "4g"
        // 配置是否预执行 dex Libraries 工程,开启后会提高增量构建速度,不过会影响 clean 构建的速度,默认 true
        preDexLibraries = false
        // 配置是否开启 jumbo 模式,代码方法是超过 65535 需要强制开启才能构建成功
        jumboMode true
        // 配置 Gradle 运行 dx 命令时使用的线程数量
        threadCount 8
        // 配置multidex参数
        additionalParameters = [
                '--multi-dex', // 多dex分包
                '--set-max-idx-number=50000', // 每个包内方法数上限
                // '--main-dex-list=' + '/multidex-config.txt', // 打包到主classes.dex的文件列表
                '--minimal-main-dex'
        ]
    }
    // 执行 gradle lint 命令即可运行 lint 检查,默认生成的报告在 outputs/lint-results.html 中
    lintOptions {
        // 遇到 lint 检查错误会终止构建,一般设置为 false
        abortOnError false
        // 将警告当作错误来处理(老版本:warningAsErros)
        warningsAsErrors false
        // 检查新 API
        check 'NewApi'
    }
    

    项目详细部署

    在这里插入图片描述

    组件化的意义

    在这里插入图片描述

    PhoneModule 和 Android Libray区别

    PhoneModule存在的配置:
    
    apply plugin: 'com.android.application'
    applicationId "com.canjun.components"
    
    AndroidLibrary存在的配置:
    apply plugin: 'com.android.library'
    applicationId "com.canjun.components"(x)
    

    集成化 和 组件化

    集成化是一个project中只有一个为主模块,其他的模块为Lirary,这些Library不能单独运行。他们必须和主模块一起运行成为一个app

    组件化是指一个Project中的各个module均能单独运行。

    集成化用于生产环境,而组件化多用于开发测试阶段,其高度解耦的特性有利于开发测试。

    项目组件化的配置

    在这里插入图片描述

    1. 修改主模块的gradle配置

       apply plugin: 'com.android.application'
       def androidId = rootProject.ext.androidId
       def appId = rootProject.ext.appId
       def url = rootProject.ext.url
       def support = rootProject.ext.dependencies
       def isRelease = rootProject.ext.isRelease
       
       android {
           compileSdkVersion androidId.compileSdkVersion
           buildToolsVersion androidId.buildToolsVersion
           defaultConfig {
               applicationId appId.app
               minSdkVersion androidId.minSdkVersion
               targetSdkVersion androidId.targetSdkVersion
               versionCode androidId.versionCode
               versionName androidId.versionName
               testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
               buildConfigField("boolean","isRelease",String.valueOf(isRelease))
       //        multiDexEnable true
           }
           buildTypes {
               debug{
                   buildConfigField("String","debug","\"${url.debug}\"")
               }
               release {
                   buildConfigField("String","debug","\"${url.release}\"")
                   minifyEnabled false
                   proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
               }
           }
       }
       
       dependencies {
           implementation fileTree(dir: 'libs', include: ['*.jar'])
           ...
           support.each{ k,v-> implementation v}
           implementation project(":common")
       
           //主模块不能依赖主模块
           if(isRelease){
               implementation project(":order")
               implementation project(":personal")
           }
           ...
       }
      
    2. 修改library模块的gradle配置

       // 要点一
       if(isRelease){
           apply plugin: 'com.android.library'
       }else {
           apply plugin: 'com.android.application'
       }
       
       def androidId = rootProject.ext.androidId
       def appId = rootProject.ext.appId
       def support = rootProject.ext.dependencies
       
       android {
           compileSdkVersion androidId.compileSdkVersion
           buildToolsVersion androidId.buildToolsVersion
           defaultConfig {
               minSdkVersion androidId.minSdkVersion
               // 要点二
               if(!isRelease){
                   applicationId appId.order
               }
       
               targetSdkVersion androidId.targetSdkVersion
               versionCode androidId.versionCode
               versionName androidId.versionName
       
               testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
               consumerProguardFiles 'consumer-rules.pro'
           }
       
           buildTypes {
               release {
                   minifyEnabled false
                   proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
               }
           }
       
       }
       dependencies {
           implementation fileTree(dir: 'libs', include: ['*.jar'])
       
           support.each{ k,v -> implementation v}
       
           implementation project(":common")
       
           testImplementation 'junit:junit:4.12'
           androidTestImplementation 'androidx.test.ext:junit:1.1.1'
           androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
       }
      

    3.效果展示

    集中式效果展示:

    在这里插入图片描述

    组件化效果展示:

    在这里插入图片描述

    组件化开发的临时代码,集中化打包时动态隔离

    由于在组件化开发时,会在响应的moduel中写一些本地化代码,但是这些代码,不过不处理,会默认打包到主模块中。如下图所示:

    lWD1vq.md.png

    因此需要特殊处理一下:

    1. 配置gradle,进行代码隔离

    在这里插入图片描述

    1. 将本地化代码按照gradle配置进行存储

    在这里插入图片描述

    3.进行验证

    lWDNaF.md.png

    展开全文
  • 目录 模块间交互 APT介绍与使用 APT高阶用法JavaPoet 模块间交互 常见的交互方式 1)EventBus一对一通讯,会造成Bean对象泛 2)反射技术 维护成本较高,高版本容易出现@hide限 3)隐式意图 维护成本较高,action...

    目录

    • 模块间交互
    • APT介绍与使用
    • APT高阶用法JavaPoet

    模块间交互

    常见的交互方式

    1)EventBus一对一通讯,会造成Bean对象泛
    2)反射技术 维护成本较高,高版本容易出现@hide限
    3)隐式意图 维护成本较高,action比较难以维护
    4) 广播 7.0后需要动态注册
    5)类加载器 需要全类名路径

    解决方案

    类加载模式

    在这里插入图片描述

    public void jumpApp(View v){
        try {
            //通过全类名的方式进行跳转
            Class<?> clzz = Class.forName("com.canjun.myapplication.MainActivity");
            Intent intent = new Intent(this,clzz);
            startActivity(intent);
        }catch (Exception e){
    
        }
    }
    
    全局map记录的方式

    在这里插入图片描述

    1. 在公共模块(common模块)添加全局记录类

       /**
        * RecorderPathManager
        * 记录全局的path信息
        *
        * @author zfc
        * @date 2020-01-09
        */
       public class RecorderPathManager {
       
           private static Map<String, List<PathBean>> paths = new HashMap<>();
       
           /**
            * 根据组名和路径名记录字节码信息
            * @param groupName
            * @param pathName
            * @param clazz
            */
           public static void joinGroup(String groupName,String pathName,Class clazz){
               List<PathBean> path = paths.get(groupName);
               if(path==null){
                   //添加
                   path = new ArrayList<>();
                   paths.put(groupName,path);
               }else {
                   for (PathBean p: path){
                       if(p.getPath().equals(pathName)){
                           return;
                       }
                   }
               }
               path.add(new PathBean(pathName,clazz));
           }
       
           /**
            * 获取目标字节码对象
            * @param groupName 组名
            * @param pathName 路径名
            * @return
            */
           public static Class getTargetClass(String groupName,String pathName){
               List<PathBean> path = paths.get(groupName);
               if(path==null){
                   return null;
               }
               for (PathBean p: path){
                   if(p.getPath().equals(pathName)){
                       return p.getClzz();
                   }
               }
       
               return null;
           }
       }
      
       /**
        * PathBean
        *
        * 记录Activity字节码及其路径
        * 例如
        *      path:'order/OrderMainActivity'
        *      clzz: OrderMainActivity.class
        *
        * @author zfc
        * @date 2020-01-09
        */
       public class PathBean {
       
           private String path;
       
           private Class clzz;
       
           public PathBean(String path, Class clzz) {
               this.path = path;
               this.clzz = clzz;
           }
       
           public String getPath() {
               return path;
           }
       
           public void setPath(String path) {
               this.path = path;
           }
       
           public Class getClzz() {
               return clzz;
           }
       
           public void setClzz(Class clzz) {
               this.clzz = clzz;
           }
       }
      
    2. 在应用启动时,添加需要记录的字节码对象

       /**
        * MyApp
        *
        * @author zfc
        * @date 2020-01-09
        */
       public class MyApp extends BaseApplication {
       
           @Override
           public void onCreate() {
               super.onCreate();
               //注册activity
               RecorderPathManager.joinGroup("app","MainActivity",MainActivity.class);
               RecorderPathManager.joinGroup("order","OrderMainActivity", OrderMainActivity.class);
               RecorderPathManager.joinGroup("personal","PersonalMainActivity", PersonalMainActivity.class);
           }
       }
      
    3. 在页面跳转时,使用字节码对象

        try {
           Class<?> clzz = RecorderPathManager.getTargetClass("app","MainActivity");
           Intent intent = new Intent(this, clzz);
           startActivity(intent);
       }catch (Exception e){
      
       }
      

    APT介绍与使用

    APT是什么

    在这里插入图片描述

    结构体语言

    语言元素分类
    在这里插入图片描述

    在这里插入图片描述

    常用的API

    在这里插入图片描述

    在这里插入图片描述

    APT基本使用

    1. 环境描述

      a.创建java library 名为compiler

      b.为compiler添加依赖

        // 注册注解,并对其生成META-INF的配置信息,rc2在gradle5.0后有坑
       // As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
       // implementation 'com.google.auto.service:auto-service:1.0-rc2'
      
       // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
       compileOnly'com.google.auto.service:auto-service:1.0-rc4'
       annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
      
       implementation project(':annotation')
      
    2. 创建注解处理器

       /*
        * 通过autoService通过生成文件
        */
       @AutoService(Processor.class)
       
       @SupportedAnnotationTypes({"com.canjun.annotation.ARouter"})
       @SupportedSourceVersion(SourceVersion.RELEASE_7)
       @SupportedOptions({"content"}) //外部传入的参数
       public class ARouterProcessor extends AbstractProcessor {
       
           /**
            * 操作Element的工具类
            */
           private Elements elementUtils;
       
           /**
            * 类信息工具类
            */
           private Types typesUtils;
       
           /**
            * 日志信息输出工具类
            */
           private Messager messager;
       
           /**
            * 文件生成器
            */
           private Filer filer;
       
           //初始化工作
           @Override
           public synchronized void init(ProcessingEnvironment processingEnv) {
               super.init(processingEnv);
               elementUtils = processingEnv.getElementUtils();
               typesUtils = processingEnv.getTypeUtils();
               messager = processingEnv.getMessager();
               filer = processingEnv.getFiler();
               
               //可以获取外部模块传入的参数
               //传参方式见下小结
               String content = processingEnv.getOptions().get("content");
               messager.printMessage(Diagnostic.Kind.NOTE,content);
           }
       //
       //    //需要处理的注解类型
       //    @Override
       //    public Set<String> getSupportedAnnotationTypes() {
       //        return super.getSupportedAnnotationTypes();
       //    }
       //
       //    //jdk版本去编辑
       //    @Override
       //    public SourceVersion getSupportedSourceVersion() {
       //        return super.getSupportedSourceVersion();
       //    }
       //
       //    //接收外部参数
       //    @Override
       //    public Set<String> getSupportedOptions() {
       //        return super.getSupportedOptions();
       //    }
       
           @Override
           public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
       }
      
    3. 通过AndroidModule向java library compiler传入参数

       // 在gradle文件中配置选项参数值(用于APT传参接收)
       // 切记:必须写在defaultConfig节点下
        javaCompileOptions {
           annotationProcessorOptions {
               arguments = [content : 'hello apt']
           }
       }
      
    4. AndroidModule使用注解处理器

      a.gradle中依赖的声明

        implementation project(':annotation')
       //使用注解处理器
       annotationProcessor project(':compiler')
      

      b.Activity中使用注解

       @ARouter(path="/app/MainActivity")
       public class MainActivity extends AppCompatActivity {
           ...
           ...
       }
      
    5. 注解处理中核心方法的实现

       @Override
      public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
      
          if(annotations == null){
              return false;
          }
      
          messager.printMessage(Diagnostic.Kind.NOTE,annotations.toString());
      
          //获取被注解的类对象
          Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ARouter.class);
          for (Element e:elements){
              //获取包名
              String pkgName = elementUtils.getPackageOf(e).getQualifiedName().toString();
              //获取类名
              String className = e.getSimpleName().toString();
      
              messager.printMessage(Diagnostic.Kind.NOTE,pkgName + ">>" + className);
      
              //生成ARouter文件
              String finalClassName = className+"$$ARouter";
      
      
              try {
                  JavaFileObject sourceFile =  filer.createSourceFile(pkgName+"."+finalClassName);
                  Writer writer = sourceFile.openWriter();
                  //设置包名
                  writer.write("package "+pkgName + ";\n");
                  writer.write("public class "+finalClassName+" {\n");
                  writer.write("public static Class<?> findTargetClass(String pathName){\n");
                  //获取注解的path的value(注意)
                  String path = e.getAnnotation(ARouter.class).path();
                  writer.write("if(pathName.equalsIgnoreCase(\""+path+"\")){\n");
                  writer.write("return "+className+".class;\n");
                  writer.write("}\n");
                  writer.write("return null;\n");
                  writer.write("}\n");
                  writer.write("}\n");
                  writer.close();
              } catch (Exception ex) {
                  ex.printStackTrace();
              }
          }
      
          return true;
      }
      
    6. 验证注解处理器生成的类

      通过build->make project
      在这里插入图片描述

    使用autoService注解 注解处理器,会在如下图中的位置生成注册信息。
    在这里插入图片描述

    APT高阶用法和JavaPoet

    JavaPoet概述

    JavaPoet官往地址

    JavaPoet是什么

    在这里插入图片描述

    JavaPoet运行环境

    在这里插入图片描述

    implementation 'com.squareup:javapoet:1.9.0'
    
    // 注册注解,并对其生成META-INF的配置信息,rc2在gradle5.0后有坑
    // As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
    // implementation 'com.google.auto.service:auto-service:1.0-rc2'
    
    // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
    
    JavaPoet常用类

    在这里插入图片描述

    javaPoet格式化字符串

    在这里插入图片描述

    使用javaPoet

    要求 生成如下格式的代码
       package com.canjun.myapplication;
       public class MainActivity$$ARouter {
    
           public static Class findTargetClass(String name){
               if(name.equalsIgnoreCase("/app/MainActivity")){
                   return MainActivity.class;
               }
               return null;
           }
        }
    
    javaPoet编写内容如下:
    @AutoService(Processor.class)
    @SupportedAnnotationTypes({"com.canjun.annotation.ARouter"})
    @SupportedSourceVersion(SourceVersion.RELEASE_7)
    @SupportedOptions({"content"})
    public class ARouterProcessor extends AbstractProcessor {
    
        private Elements elementUtils;
        private Types typeUtils;
        private Messager messager;
        private Filer filer;
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            elementUtils = processingEnv.getElementUtils();
            typeUtils = processingEnv.getTypeUtils();
            messager = processingEnv.getMessager();
            filer = processingEnv.getFiler();
            Map<String, String> options = processingEnv.getOptions();
            String content = options.get("content");
            messager.printMessage(Diagnostic.Kind.NOTE, content);
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
           if(annotations==null||annotations.isEmpty()){
               return false;
           }
    
           //获取被ARouter注解的类
            Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ARouter.class);
    
           for (Element e : elements){
               //获取e的包名
               String pkgName = elementUtils.getPackageOf(e).getQualifiedName().toString();
               //获取类名
               String className = e.getSimpleName().toString();
    
               String newFileName = className+"$$ARouter";
               //通过javaPoet写新生成的文件
               //javaPoet项目地址https://github.com/square/javapoet
    
               try {
                   String pathName = e.getAnnotation(ARouter.class).path();
                   MethodSpec methodSpec = MethodSpec.methodBuilder("findTargetClass")
                           .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                           .returns(Class.class)
                           .addParameter(String.class, "name")
                           //需要注意的是 语句中的参数 不需要显式添加“;”
                           .addStatement(" if(name.equalsIgnoreCase($S)){\n" +
                                   "                       return $T.class;\n" +
                                   "                   }\n" +
                                   "                   return null", pathName, ClassName.get((TypeElement)e))
                           .build();
    
                   TypeSpec typeSpec = TypeSpec.classBuilder(newFileName)
                           .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                           .addMethod(methodSpec)
                           .build();
    
                   JavaFile javaFile = JavaFile.builder(pkgName,typeSpec)
                           .build();
    
    
                   javaFile.writeTo(filer);
               } catch (IOException ex) {
                   ex.printStackTrace();
               }
           }
    
            return true;
        }
    
    }
    

    生成结果:

    lf7keU.png

    lf7ZFJ.png

    展开全文
  • 什么是 Android 组件化

    千次阅读 2019-06-17 14:48:16
    我希望阅读本片博客的童鞋都是有一定的 Android 基础的,并且近期想实施组件化方案的.希望这篇文章能帮助到你,让你知道什么是组件化,有哪些可选的方案 什么是组件化 下面这幅图相信大家平常多多少少都能看见类似的. ...
  • 组件化开发

    2020-09-28 10:03:51
    (十一)组件化开发 11.1 组件化的基本使用 ​ 简单的组件示例 <div id="app"> <!-- 3.使用组件 --> <my-cpn></my-cpn> <my-cpn></my-cpn> <my-cpn></my-cpn>...
  • 组件化

    2019-08-30 17:23:21
    组件化: 本文章编写前参考于:顾老师的有道云笔记 为什么组件化? 随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,...
  • 组件化

    2020-10-16 14:18:51
    文章目录组件化1.模块化2.组件化3.模块化与组件化的区别4.Calces配置组件化5.Calces如何使用项目build.gradle子模块(user)配置管理依赖及版本号1.创建config.gradle2.引入配置文件3.修改各模块内容并指定清单文件4....
  • 组件化开发步骤

    2020-08-20 17:20:49
    Android组件化开发引言1、在工程根目录的build.gradle中添加cc-register插件的classpath。最新版本号1.1.22、在工程根目录创建一个名为"cc-settings-2.gradle"的文件,并将以下代码复制到该文件中(也可直接下载...
  • 什么叫组件化开发

    万次阅读 2018-02-24 21:39:32
    转载:什么叫组件化开发? - aloo的回答 - 知乎 https://www.zhihu.com/question/29735633/answer/90873592  从第一代码农写下第一行代码开始到上个世纪的80年代的软件危机,码农一直在考虑一个问题,怎么让写代码...
  • 再写这篇文章之前我看了很多关于组件化的文章,但大多数都是讲了一些'玄而又玄'的东西
  • 从模块化到组件化再到插件化

    万次阅读 多人点赞 2017-04-17 22:02:35
    从模块化到组件化再到插件化 参考: http://blog.xiaohansong.com/2015/10/21/IoC-and-DI/ http://blog.csdn.net/dd864140130/article/details/53645290 ... ...
  • 组件化在业界已经炒的水深火热,关于组件化的好处和组件化的方案网上已经有大篇的文章了。笔者通过拆分一个现有的demo来简单聊一下项目实施组件化的过程(将分为上、中、下三篇)。demo可以从github下载(下载之后...
  • Android 开发:由模块化到组件化(一)

    万次阅读 多人点赞 2017-03-20 08:25:47
    组件化?到底是什么鬼?有啥区别. 有这种感觉才是对的,模块化和组件化本质思想是一样的,都是"大化小",两者的目的都是为了重用和解耦,只是叫法不一样.如果非要说区别,那么可以认为模块化粒度更小,更侧重于重用,而组件化...
  • Android组件化之终极方案

    万次阅读 多人点赞 2017-10-19 21:00:44
    如果你把开源的三方库当做一个功能组件的话,那么很显然,我们在使用这些三方库的时候是通过什么方式呢?难道你会下载它的源代码吗,应该很少有人会这样做吧。那么让我们看看我们是怎么引入三方库的:
  • 组件化开发和模块化开发概念辨析

    万次阅读 多人点赞 2018-01-29 00:58:30
    组件化开发和模块化开发概念辨析 网上有许多讲组件化开发、模块化开发的文章,但大家一般都是将这两个概念混为一谈的,并没有加以区分。而且实际上许多人对于组件、模块的区别也不甚明了,甚至于许多博客文章专门...
  • Android模块化、组件化、插件化区别

    千次阅读 2019-05-27 16:04:48
    组件化(lib)主要解决问题是功能拆分,强调单独编译 插件化(application)是所有组件都为apk的特殊组件化,特点可热更新 通讯方式不同点: 模块化相互引入,需要引入需要的module 组件化通讯方式分为隐式和...
  • react之组件介绍

    万次阅读 2020-06-19 10:40:43
    ReactJS是基于组件化的开发。 那么有些朋友会有疑问,组件是什么?   所谓组件,即封装起来的具有独立功能的UI部件。React推荐以组件的方式去重新思考UI构成,将UI上每一个功能相对独立的模块定义成组件,然后将小...
  • 前端组件化开发和模块化开发的区别 之前一直以为模块化开发和组件化开发是一个意思,有次看到了类似这样的题,发现自己还是太年轻,现在整理一点出来。 首先,组件化和模块化的意义都在于实现了分治,目前我们开发...
  • Android组件化框架项目详解

    千次阅读 2020-09-10 22:46:44
    项目发展到一定阶段时,随着需求的增加以及频繁地变更,项目会越来越大,代码变得越来越臃肿,耦合会越来越多,开发效率也会降低,这个时候我们就需要对旧项目进行重构即模块的拆分,官方的说法就是组件化。...
1 2 3 4 5 ... 20
收藏数 1,021,095
精华内容 408,438
关键字:

组件化