Android---探究Android组件化

最近折腾了一下组件化,记录下心得。

Android发展到现在,从刚开始的MVC,到后来的MVP和MVVM,它们似乎都在做同一件事:解耦

组件化也是,我们不停的添加新功能和业务逻辑到项目中,随着时间推移和开发人员的增多,项目发展到一定程度的时候,团队开发开始存在很多问题.不得不花费更多的时间去和同事沟通协调。并且在开发过程中,任何一位开发人员没办法专注于自己的功能模块,从而影响整体开发效率。

为了满足各个业务模块的迭代而彼此不受影响,这个时候就需要改变项目架构了。围绕着模块化的这个概念,市面上常见的架构有两种:组件化和插件化,我在网上找了下面这张图很好的表示了两者的区别:
在这里插入图片描述
可以看到,组件化顾名思义,就是由很多不同的组件(身体部分)组成一个APK(身体),这些组件(身体部分)离开了APK(身体)是可以单独存活的,就是说每个组件都可以打包一个独立的apk。

而插件化有且只有一个完整的APK,只不过它可以在运行时动态的去加载各种各样的小插件。

结论:组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的能力,但是插件化是可以的。

下面开始说说组件化的实施流程。

1.组件属性的转换

在Android Studio中,module主要有两种属性:applicationlibrary,你可以看看你目前的项目模块中的build.gradle文件中最上面那行,不出意外的话应该是:

apply plugin: 'com.android.application'

这说明你这个module是一个可以独立运行的Android程序,也就是我们的APP。如果你把它改为:

apply plugin: 'com.android.library'

那它就是不可以独立运行的,一般是作为Android程序依赖的库文件;
当你调试一个组件的时候,你需要把这个组件打包成APK运行调试,这个时候这个组件的属性就应该是application。当你调试完成打包的时候,这个组件的属性应该是library从而被添加到主APP中去。

也就是说,组件化开发中,我们的组件,既可以是application,也可以是library,两者会随时转变。那么问题来了:我们怎么让组件在这两种模式之间相互转换呢?你总不可能每次都去组件的build.gralde中去把最上面那行手动去改成application或者library吧,如果只有两三个组件的话,这看起来也没什么问题,那玩意儿要是有十几个呢?想想都可怕啊喂! (PS:我在看《重构——改善既有代码的设计》这本书的时候有一句话我记得非常清楚,当某一块代码你需要在两个以上的地方去写它的时候,你就要考虑是否能重构优化一下了。这句话导致我每次遇到熟悉的代码的时候脑海里下意识的都在想这块代码好熟悉啊,肯定写过,先把功能实现了,等会儿看能不能封装一下~~)

基于这种思想,我们可以想到可以用一个变量来区别当前组件是否需要独立打包编译成APK,是的话就把组件的属性改为application,反之就是library。那具体怎么做呢?

首先你要知道,我们是要在组件的build.gradle文件中去读取我们定义的这个变量的,那么我们定义在哪儿呢?肯定不能是在java代码里面,因为build.gradle访问不到,那怎么办?别忘了Android Studio使用的代码构建工具可是大名鼎鼎的Gradle!

Gradle有一个重要属性,可以帮我们做到这个功能,那就是每当我们用Android Studio新建一个Android项目的时候,就会在项目的根目录生成一个 gradle.properties文件,
在任何一个module中的build.gradle中,我们都可以访问到根目录的这个gradle.properties文件,那么事情就简单了,我们在gradle.properties文件最下方添加一行代码:

# 是否需要单独打包编译成APP运行 true表示需要,false表示不需要
isRunAsAPP=false

注意添加完之后一定要点一下右上角的sync同步一下
我们定义一个isRunAsAPP变量,具体功能的话我注释已经写的很详细了,然后新建两个module,我这里就叫module1和module2了方便测试:
在这里插入图片描述
至于commomLib先不管它,后面会说,然后在module1和module2中的build.gralde文件中的最上方添加如下代码:

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

可以看到,我们可以在module的build.gradle文件中直接读取project的根目录的gradle.properties中的isRunAsAPP这个变量,但是有一点要注意:gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换,比如上面的toBoolean。

还有该组件的属性如果是application的话,组件就相当于是一个独立的APP,所以我们要给组件添加applicationId
在这里插入图片描述
在主APP的build.gralde中,我们做如下设置
在这里插入图片描述
可以看到如果子组件的属性为application时,我们就不在build.gradle中去导入它们。

另外,module1和module2的build.gradle文件中,都有着大量重复的代码,比如compileSdkVersion、targetSdkVersion等等,这些每个build.gradle都会用到的参数,把它封装到一个build.gradle文件中重复使用,我们在项目根目录新建一个名为common.gradle的文件
在这里插入图片描述
把所有相同的代码放到这个文件里,然后在module1的build.gradle文件里,可以把所有代码都删掉了,只需要引入common.gradle就可了,我们在module1的build.gradle最上方添加如下代码:
在这里插入图片描述
这样就可以正常工作了,当然还有一个问题,就是当组件作为一个单独的APP调试的时候,是要有applicationId的,这也好办,我们在module的build.gradle中重写android代码块:
在这里插入图片描述
到此为止,我们组件的属性切换这个问题算是解决了。

2.各个组件之间manifest文件合并的问题

在Android Studio中每新建一个组件(module),都会有一个与之对应的AndroidManifest.xml文件,用来声明项目运行期间需要的权限,Activity等。

当某一个组件处于application属性时(isRunAsAPP为true),作为一个完整的app,Manifest文件是需要具有作为完整APP的所有的属性的,但是当组件处于library属性时(isRunAsAPP为false),打包的时候这个组件是要合并到主APP中的,那如果这时组件的manifest里面还有自己的Application和launch activity的话,是肯定会发生冲突的,因为一个APP是不可能会有多个Application和Launch Activity的。

不知道你还有没有印象,以前用Eclipse开发Android的(想想真怀念啊,有点年代感了),把项目从Eclipse移植到Android Studio的时候,AndroidManifest.xml文件的位置是不一样的,所以我们要在mudule的build.gralde中 声明我们manifest文件的位路径,这样一想的话我们可以利用上一小节gradle.properties定义的isRunAsAPP变量来动态的加载不同的manifest文件就好了!
我在module2的main包里面新建一个manifest包,用来存放我们的新建的AndroidManifest.xml
在这里插入图片描述
这个manifest文件是子组件合并到主APP项目的时候用到的,所以它里面不应该有launch activity,aplication等主app组件的manifest文件中有的东西它都不能有,所以一个标准的组件的manifest文件应该是这样:

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

    <application android:theme="@style/AppTheme">
        <activity android:name="com.koma.module2.Module2MainActivity">

        </activity>
    </application>

</manifest>

然后我们修改一下组件不同属性时manifest文件的路径,在组件的build.gralde文件中的android代码块中加入下面的代码:

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

这样一来,当isRunAsAPP为true时,组件属性为application,它所加载的manifest文件就是创建项目时Studio自动为我们创建的那个文件了,反之,作为library库被合并进主APP时,就会加载我们自己创建的那个manifest文件了,就不会再有launch activity等不需要的东西了。

3.各个组件中获取全局Context和各个组件的数据初始化

每一个APP都运行在它自己的虚拟机中,Android系会为每一个app创建一个Application类的对象,application的生命周期是最长的,就是应用程序的生命周期,默认情况下系统会自动生成Application对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中以name=".BaseApplication"形式声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的。

但是如果我们在单独编译运行子组件来调试的时候需要获取全局的Application,一般都是直接获取Application,但是当所有组件合并打包的时候就会出现问题,因为最后只有一个Application,所以我们要想一个办法,用来在所有的子组件中都能获取到全局的Application对象,这个时候就回过头来看看我们刚刚创建module1和module2的时候顺便创建的commomLib组件。

Common 有公共、公用、共同的意思,所以这个组件中封装了所有组件都用到的Base类,其中也包括BaseApplication,我在commonLib中创建了BaseApplication类

在这里插入图片描述
这样我在组件中去引入commonlib库的时候就可以访问到application了,可以在该组件的AndroidManifest文件中引用commonlib的application了,当然你最好在该组件中新建一个application去继承common中的application,然后在manifest中放入你自己新建的该组件的application,这里这么写只是测试:
在这里插入图片描述
除了application,所有需要用到的第三方类库都应该放在commonlib组件中,这样一来就不用在每个组件里面去引入这些第三方库了,比如Glide,直接在commonlib中引入的话,我的module2组件只需要引入commonlib这个lib,就可以使用Glide了,为了方便管理这些类库,我们在project的根目录中的build.gralde统一管理这些类库
在这里插入图片描述
在这里插入图片描述
还有应用的主题和所有共同资源,drawable等,也要放在commopnlib组件中。注意主题一定要放在commonlib组件中然后在其他组件中去引用,不然不同的组件主题不一样编译时会报错。

4.组件间通信
在组件化开发的时候,各个组件之间是没有直接互相依赖的,所以我们不能再用显示Intent来跳转页面了,毕竟我们组件化的目的之一就是解决模块间的强依赖问题,那现在假如我要在MainActivity(为了方便测试)中跳转到Module2MainActivity中怎么办?这个时候就要引入“路由”的概念了,这里我使用的是阿里开源的“ARouter”库,所以在每个非common的库(包括主Application)中我都强烈建议加入对ARouter和commonlib的依赖,下面我们来看看页面跳转:

@Route(path = "/module2/module2main")
public class Module2MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.home_activity_main);
		ARouter.getInstance().inject(this);
    }
}

在module2中的mainActivity中:

  1. 首先要用@Route注解标注页面,并在path变量中给页面定义一个路径
  2. 我们还需要将该页面注入到ARouter中(原理类似ButterKnife),让他帮我们完成我们需要的工作

然后我们就可以在主APP的MainActivity中使用:

ARouter.getInstance().build("/module2/module2main").navigation();

这样我们就可以跳转 module2的主页面了。如果需要携带参数的话请参照ARouter官网的使用教程。

5.组件化中Fragment怎么被访问的问题

类似于微信这种,底部四个Fragment,来回切换的这种功能在组件化里面怎么实现呢?要知道各个子组件之间是不能直接依赖的,所以fragment不能被访问到。这个时候也可以利用ARouter
首先,在Module1和Module2两个组件里面创建两个Fragment,两个Fragment都只有一句话“这是Module1Fragment”和这是Module1Fragment”,分别给他们设置路由:

@Route(path = "/module1/fragment")
public class Module1Fragment extends BaseFragment
@Route(path = "/module2/fragment")
public class Module2Fragment extends BaseFragment

设置路由然后在主APP工程里放了两个textview,用来点击切换Fragment
在这里插入图片描述
然后在MainActivity里通过ARouter可以拿到Fragment的实例:
在这里插入图片描述
在这里插入图片描述
这个时候就可以实现点击切换Fragment了!

不过这种思路虽然可以,但是显然没有达到理想的组件化的效果,理想状态下主APP只是一个空壳子,用来管理和组装其他组件,不应该和业务逻辑有关的,是可以随意被替换掉的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值