从去年开始,就陆陆续续的愈来愈多的app开始进行了组件化重构。也有不少很是好的组件化方案博客分享,因此这篇文章并不以介绍组件化方案做为主题,而是咱们应该如何一步步的从一个古老的项目,慢慢一步步拆分,完成组件化重构的。java
组件化的思想是好的,可是并非全部的项目都适合使用组件化的方式进行开发,因此通常须要使用组件化的项目。基本都是具有项目迭代时期久远、项目大而臃肿、项目组成员多沟通成本大、项目复杂维护成本很高等特色。这类的项目才会有组件化的用武之地。android
而其余的一些人员少、功能简单的小项目。就别去直接考虑组件化了。老老实实直接撸码就好了。强行使用组件化只会增长维护成本与开发成本。得不偿失~git
组件化历来不是一个说重构就能重构的东西,在进行组件化重构以前。最好先对组件化的结构有一个基本的理解:github
上图为组件化最基本的结构。大体能够看出。组件化主要分为三层:json
app壳:api
此为组件化的运行容器,壳中定义app入口,依赖业务组件进行运行。安全
业务组件:cookie
此为组件化的中间层,在一个大型项目组中。都有细分下来的不一样的业务组,好比管登陆的、管购物的、管视频的等等。这些不一样业务组分别维护一个各自的业务组件,以达到各自业务组业务解耦的效果。网络
原则上来讲:各个业务组件之间不能有直接依赖!全部的业务组件均须要能够作到独立运行的效果。对于测试的时候,须要依赖多个业务组件的功能进行集成测试的时候。可使用app壳进行多组件依赖管理运行。app
基础组件:
基础组件也叫基础功能组件。此部分组件为上层业务组件提供基本的功能支持。如基础网络组件、基础视图组件、基础数据存取组件等,以及组件化的核心通讯组件:路由组件。
以上便是组件化的最基本结构,固然在真正的项目之中,不可能会存在这么简单的结构,都是须要根据你的具体现状进行扩充的。好比你能够在基础组件与业务组件之间,添加一层特殊的功能组件。此层的功能组件只被一个或者多个组件进行依赖,只要不破坏这层由下到上单向依赖链便可:
组件化重构历来都不是说重构就能重构的,首先得有个强有力的领导去支持执行,而后你才可能去具体的进行重构。
其次,你得提早对大家的项目进行大方向的分层结构划分,哪些东西须要放在什么层。须要提早有个明确的划分。
重构你的基础组件:即你的各类基础功能框架须要提早从项目中拆分出来。包括网络、图片加载、数据存储、埋点、路由等。
你须要建立一个基础library module。用于依赖全部的基础功能组件,如baselib。
做用:用于统一依赖基础功能库,并统筹、关联好各功能框架的关系,作好各功能库的初始化封装操做。提供上层业务组件直接调用。
与普通的组件化方案作法不一样,普通的组件化方案是使用一个变量进行控制。使得业务组件能够在application与library之间进行灵活切换。使得组件也是application。application也是组件。
可是这种作法,由于老是在libraryModule与applicationModule之间进行切换。很容易致使各类混乱问题:好比Manifest冲突,R文件冲突等。
因此咱们采用的是多app壳分组加载的方式:
能够看到,每一个业务线的业务组件。都分别有一个各自的app壳模块。而主app壳依赖全部的业务组件. 在进行业务开发时。各自业务组成员能够直接运行各自的app壳模块进行测试,主app壳进行全量打包。
在拆分初期,这个时候的建议以本来的项目application做为主app壳。
预留一个核心业务组件出来。好比登陆组件:此类组件为业务组件,可是又被全部其余组件所须要,因此将其单独做为核心业务组件独立出来。而后别的业务组件。经过各自的app壳工程。依赖进入便可:再次提醒业务组件之间不能直接进行依赖:
这种分层结构的好处有:
组件化重构后,module变多了,因此就须要对全部module的一些gradle脚本进行统一配置管理。避免混乱。
ext {
COMPILE_SDK_VERSION = 25
BUILD_TOOLS_VERSION = '25.0.0'
MIN_SDK_VERSION = 16
TARGET_SDK_VERSION = 19
// SUPPORT
SUPPORT_VERSION = '23.2.0'
SUPPORTDEPS = [
supportV4 : "com.android.support:support-v4:${SUPPORT_VERSION}",
supportV13 : "com.android.support:support-v13:${SUPPORT_VERSION}",
appcompatV7 : "com.android.support:appcompat-v7:${SUPPORT_VERSION}",
cardview : "com.android.support:cardview-v7:${SUPPORT_VERSION}",
design : "com.android.support:design:${SUPPORT_VERSION}",
annotations : "com.android.support:support-annotations:${SUPPORT_VERSION}",
multidex : 'com.android.support:multidex:1.0.1'
]
...
}
复制代码
此脚本统一配置管理全部的版本号相关的数据。外部须要使用版本号及依赖时,须要统一今后文件配置属性中进行读取。好比要依赖supportV4包:
compile "${SUPPORTDEPS.supportV4}"
复制代码
boolean isAppModule = project.plugins.hasPlugin('com.android.application')
android {
compileSdkVersion Integer.parseInt("${COMPILE_SDK_VERSION}")
buildToolsVersion "${BUILD_TOOLS_VERSION}"
lintOptions {
abortOnError false
}
defaultConfig {
if (isAppModule) {
applicationId "com.haoge.component.demo"
}
minSdkVersion Integer.parseInt("${MIN_SDK_VERSION}")
targetSdkVersion Integer.parseInt("${TARGET_SDK_VERSION}")
versionCode Integer.parseInt("${DEFAULE_CONFIG.versionCode}")
versionName "${DEFAULE_CONFIG.versionName}"
}
}
复制代码
这样。就可使用apply语法。让全部组件module。都统一依赖此gradle脚本。进行统一环境配置了。
细心点的能够发现。我在baseConfig中,添加了默认的applicationId的指定。这是由于对于大部分应用而言。都有用过各类的第三方sdk。特别是第三方登陆,这种的sdk框架。不少都会须要进行包名验证的,因此建议有此种状况的,在此添加上默认的applicationId指定较好。
若是有嫌麻烦的又动手能力强的。能够考虑本身封装个gradle插件来进行统一配置管理
咱们须要对各自的组件,分别设置他自身的资源前缀来做为命名约束,避免出现不一样的组件对不一样的资源起了同一个命名,致使编译冲突等问题。
android {
resourcePrefix 'lg_'
}
复制代码
这个资源前缀的做用是:当你在该module下建立了一个资源命名时,若名字不能与此前缀进行匹配,则将会进行即时提醒。避免冲突。
组件化以后。资源管理也是个问题,图片资源、assets资源、raw文件资源等。都具备占用资源大、基本不多修改等特色。因此这里最好将其单独拆分出来。统一提供给全部组件进行使用:
因此,能够考虑将此类大文件资源,统一放入组件化的最底层。使得不一样组件不用本身单独维护一份此大文件资源。避免资源浪费的现象。好比能够直接将此部分资源。直接放入baselib中,做为基础功能提供库进行使用。
可能有人会问:为何要作组件的application的生命周期派发?
举个栗子:都知道。网络库、图片加载库等,都须要进行对应的初始化操做才能进行使用的,可是在组件化中,若是不进行各自application的派发。不能进行一个统一流程的初始化操做。那么可能你组件A须要本身手动写基础库的初始化操做。组件B、组件C也须要。最后你的主app壳也须要,这个时候。就容易乱了!
因此须要有个结构。来让各自的组件。分别完成自身的组件的功能初始化。
好比基础功能组件:初始化网络、图片框架等,上层的业务组件A,初始化自身的其余功能操做。各自的组件分别只初始化自身这部分的操做。而不用管所依赖的其余组件须要进行什么初始化。
这部分的生命周期派发能够参考demo中的baselib的delegate包下的类:
demo连接放在了文章末尾。
组件间通讯的核心是路由框架,这部分框架须要放置在最底层的基础功能组件中,提供上层进行使用,这里我使用的是我本身的路由框架Router:一款单品、组件化、插件化全支持的路由框架。
此路由框架支持在单品、组件化、插件化中均能使用。若是你想要为组件化以后,能在后期有须要的状况下,方便的从组件化切换到插件化的环境中去,建议使用此Router
若是大家项目中已经有使用本身的路由框架,且也直接支持组件化环境使用。建议这块就最好别考虑换了。实话说换一个路由框架任务仍是挺重的。
由于基本全部的介绍组件化的blog,都对其中的路由框架,作了很是详细的说明,因此这块我就不许备展开进行详细的赘述了,若有感兴趣的,能够参考上方的连接进行了解使用。
与路由通讯不一样的是:路由主要用于作界面跳转通讯,对于普通的事件通讯做用不大。好比说我是组件A,须要调组件B中的某个接口,并获取返回数据进行操做。这个时候,就须要别的方式来进行实现了。
不少人一说到事件通讯。可能就会想起使用EventBus了。的确EventBus是个很好的事件通讯框架,可是相信用过的人都知道。一旦EventBus被滥用。随着时间的迭代,因为其独特的解耦特性,会使得你的代码很难进行调试、维护。
因此这个时候,咱们摒弃了使用EventBus来做为组件间时间通讯的桥梁。而是简单的使用控制反转的手段。将组件间通讯协议定义在底层基础组件中,上层的业务组件分别实现底层对应的各自的协议接口来进行通讯。
咱们以登陆组件为例:
首先,在基础组件层添加一个协议接口。这个接口用于定义登陆组件所对外提供的时间通讯入口,好比退出登陆、清理cookie等:
public interface LoginPipe extends Pipe{
void logout();
void clearCookie();
}
复制代码
而后。在登陆组件中。实现此协议接口。并注册入对应的通讯管理器:
// 实现协议接口。
public final class LoginPipeImpl implements LoginPipe {
@Override
void logout() {
// do something
}
void clearCookie() {
// do something
}
}
复制代码
// 注册此实现进协议管理器中
// PipeManager也位于基础组件中。
PipeManager.register(LoginPipe.class, new LoginPipeImpl());
复制代码
而后便可在别的组件中。经过此PipeManager协议管理器。根据协议类。获取到对应的实现类进行直接调用了:
PipeManager.get(LoginPipe.class).logout();
复制代码
上面这种作法,虽然的确很简单,可是具有如下几点优势:
最后一条可能相对比较复杂一点。因此下面咱们针对这条进行展开描述:
上面咱们提到了。在对老旧项目进行组件化重构的时候。使用主module做为的主app壳,而app壳实际上是须要没有具体的业务代码的。因此这个地方存在冲突。可是咱们组件化拆分也不是能够一蹴而就的,只能慢慢一步步、一个页面一个页面的进行拆分并测试。因此拆分过程实际上是个漫长的痛苦的过程。
而在拆分过程当中,很难避免的就是新旧代码均须要同时存在的尴尬场面。而这种尴尬的场面会一直持续到全部组件均拆分完毕以后。
而后拆分过程当中,你也会遇到另一个问题:就是各业务组的拆分计划实际上是不一样步的,也就是说极可能你当前拆的业务。须要调用到别的业务组的功能,而这个功能这个时候。极可能还根本没有被提交到拆分计划表上来。因此这个时候。你就必需要在你拆分的组件中,仍是先直接调用老项目中的逻辑代码。
因此使用上面的事件通讯机制。你会须要在主app中建议一个临时的协议接口。好比:
public interface MainPipe {
void doSomething();
}
复制代码
而后主项目实现并注册它。提供给你的组件进行使用。而其余组件遇到此种相似问题时,也于此相似。在此MainPipe种继续添加对应的通讯协议方法并实现便可。
因为这样的作法。将全部的主app的临时协议接口。均放置于此MainPipe中。提高了协议的内聚性。当全部业务组均完成组件化重构以后。那么就能够统一的直接对此MainPipe进行重构,将其中各自组件的协议迁移至各自组件的协议类中,而后就能够安全地进行主app中无关业务代码统一删除了。使其成为真正的主app壳工程。
不少时候,其实组件间通讯。传递的数据都是普通的简单数据,可是也有一些时候。会须要传递复杂数据。好比进行跨组件调用api接口并获取返回数据时,或者说读取用户完整数据时。
以读取用户完整数据为例,数据通讯的协议定义仍旧以上方的事件通讯机制做为实现载体:
public class User {
String uid;
String nickname;
String email;
String phone;
...
}
复制代码
这个User类包含了全部的用户信息在里面。而后如今须要将此user实例进行跨组件传递时。你就须要定义一个协议方法。提供获取此User实例的入口:
public interface LoginPipe {
User getUser();
}
复制代码
这是正常的作法,可是这样作的话,你就须要将此User实例也一块儿拷贝到协议定制层,即基础组件中来。
而在开发过程当中,这种现象很常见。并且不少时候,随着需求一更改,所须要传递的数据也不同。也不可能每次都去将对应的实体bean进行迁移,放入协议定制层。这样就太麻烦了。
因此对于这种跨组件通讯的作法。建议的方式是经过json数据来进行数据通讯
json通讯的机制,便可完美的避免实体bean迁移的问题。也能让接收方按需解析读取数据:
好比我接收方的组件。当前只须要nickname与uid两个数据。其余数据我无论。那么我就能够只解析此两个字段的数据便可。作到按需解析。
说到这里。推荐一波个人另外一个框架Parceler, 此框架是封装的Bundle的存取操做。也支持json的自动转换功能。具体用法能够参考我另外一篇博客,有兴趣的能够看看。
Parceler: 优雅的使用Bundle进行数据存取就靠它了!(文章最后有关于组件化、插件化下应该如何使用此框架的说明)
随着组件化拆分重构的进行。你会发现项目下的组件被拆分得愈来愈多,虽然你已经对组件的拆分粒度。进行过把控了。可是组件化后module持续增长是不争的事实,这个时候。随着module的持续增长。你的项目编译时间也会出现暴涨。
咱们知道。项目编译流程中,第一步会将全部的library module先进行打包编译。生成对应的aar。提供给app进行使用,app等待全部module打包完毕后,再解压aar。进行资源、代码合并,并打包成apk执行运行。
因此咱们制做了一个gradle加速插件。用于提早将module进行aar编译好。跳过module打包aar的过程。实现编译加速的效果。
具体原理能够参考这篇文章:Speedup:专为项目下Library project过多所设计的加速插件
由于在组件化开发环境下,你将会遇到的问题远远不止以上这么点,固然上面这些都是很主要的。
因此这里添加此小贴士环节,用于添加一些平时咱们开发时。可能会遇到的问题。或者说,一些在特定环境下的编码建议之类的。(这些要点极可能不能在demo中获得体现,因此请尽可能认真看下描述)
由于组件化有个特色: 各自业务组能够任意选择本身的开发模式,如mvp,mvvm,RN等。