组件化并不是新话题,其实很早很早以前我们开始为项目解耦的时候就讨论过的。但那时候我们说的是功能组件化。比如很多公司都常见的,网络请求模块、登录注册模块单独拿出来,交给一个团队开发,而在用的时候只需要接入对应模块的功能就可以了。

百牛信息技术bainiu.ltd整理发布于博客园

今天我们来讨论一下业务组件化,拿出手机,打开淘宝或者大众点评来看看,里面的美食电影酒店外卖就是一个一个的业务。如果我们在一个项目里面去写的时候,总会出现或多或少的代码耦合,最典型的有时为了赶上线时间而先复制粘贴一段类似的代码过来,结果这段代码引用的资源可能是另一个模块独立的资源或代码。但是如果将一个项目作为独立的工程来运行,就完全可以避免这种情况了。但是这并不是业务组件化最大的优势,我认为最大的优势是它大大缩减了工程结构直接降低了编译时间。

代码实现

注意,组件化不是插件化,插件化是在[运行时],而组件化是在[编译时]。换句话说,插件化是基于多 APK 的,而组件化本质上还是只有一个 APK。

代码实现上核心思路要紧记一句话:开发时是 application,发版时是 library。
来看一段 gradle 代码:

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

非常好理解,我们在开发的时候,module 如果是一个库,会使用com.android.library插件,如果是一个应用,则使用com.android.application插件,我们通过一个开关来控制这个状态的切换。
然后因为我们需要在 library 和 application 之间切换,manifest文件也需要提供两套。

举个栗子?

你可以根据这个项目一起看:https://github.com/kymjs/Modularity

假设有一个项目,这个项目包含一个叫 explorer 的文件浏览器的模块和一个叫 memory-box 的笔记的模块。因为这两个功能相对独立,我们将这两个功能拆分成两个 module,再加上原本项目的 app module,总共三个。
在 explorer 的根目录建立一个作为开关的 properties 文件(写一个全局变量也可以,怎么简单怎么来),方便用来改变当前是开发状态还是发版状态(debug & release)。 从gradle中读取这个文件中的值,来切换不同状态所需要调用的配置。顺便一提,当你修改了 properties 文件中的值时,必须要重新 sync 一下。 详细配置过程可以看看这篇文章:http://www.zjutkz.net/

遇到的问题

阿布他们的项目大量的用了 databinding 和 dagger,然而我们项目并没有用这些,用了这两个库的可以看看他是怎么爬坑的:魔都三帅

当你采用了组件化开发的时候,一定会遇到这几个问题,这几个问题除了第三个都只能规避,没有好的处理办法:

1、module 中 Application 调用的问题
2、跨 module 的 Activity 或 Fragment 跳转问题
3、AAR 或 library project 重复依赖 
4、资源名冲突

解决 Application 冲突

由于 module 在开发过程中是以 application 的形式存在的,如果这个 module 调用了类似 ((XXXApplication)getApplication()).xxx()这种代码的话,最终 release 项目时一定会发生类转换异常。因为在 debug 状态下的 module 是一个 application,而在 release 状态下它只是一个 lib。所以也就是在 debug 和 release 时获取到的 Application 不是同一个类的对象。
这个问题还好,我们只要在 application 里面尽量不要写方法实现,不要做强转操作就好。
如果确实要区分,业务模块在 debug 状态和 release 状态有不同的行为,可以通过扩展 BuildConfig 这个类,在代码中通过 boolean 值来执行不同的逻辑。只需要在 gradle 中加入(具体代码用法可查看【line:48】):

if (isDebug.toBoolean()) {
buildConfigField 'boolean', 'ISAPP', 'true'
} else {
buildConfigField 'boolean', 'ISAPP', 'false'
}

有些人喜欢将 application 单例,写一个静态的对象,然后在代码里面需要context的时候用这个全局单例。这样的情况我送大家一个工具类(其实是从冯老师代码里偷来的):Common

public class App {
public static final Application INSTANCE; static {
Application app = null;
try {
app = (Application) Class.forName("android.app.AppGlobals").getMethod("getInitialApplication").invoke(null);
if (app == null)
throw new IllegalStateException("Static initialization of Applications must be on main thread.");
} catch (final Exception e) {
LogUtils.e("Failed to get current application from AppGlobals." + e.getMessage());
try {
app = (Application) Class.forName("android.app.ActivityThread").getMethod("currentApplication").invoke(null);
} catch (final Exception ex) {
LogUtils.e("Failed to get current application from ActivityThread." + e.getMessage());
}
} finally {
INSTANCE = app;
}
}
}

跨 module 跳转

如果单独是 Activity 跳转,常见的做法是:隐式启动 Activity、或者定义 scheme 跳转。
但是如果界面是一个 Fragment 就比较麻烦了,我推荐的是直接通过类名跳转。

首先创建一个所有界面类名的列表

public class RList {
public static final String ACTIVITY_MEMORYBOX_MAIN = "com.kymjs.app.memory.module.main.MainActivity"; public static final String FRAGMENT_MEMORYBOX_MAIN = "com.kymjs.app.memory.module.list.MainFragment";
}

在获取 Fragment 的时候就可以根据列表中的类名来读取指定的 Fragment 了。

public class FragmentRouter {

    public static Fragment getFragment(String name) {
Fragment fragment;
try {
Class fragmentClass = Class.forName(name);
fragment = (Fragment) fragmentClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
return fragment;
}
}

同理,Activity 其实也可以用这种方法来跳转:

public static void startActivityForName(Context context, String name) {
try {
Class clazz = Class.forName(name);
startActivity(context, clazz);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}

最后,对于这个RList类,我们还可以通过 Gradle 脚本来生成,就像 R 文件一样,这样子开发就要方便很多了。

重复依赖

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

可以将所有的依赖写在 shell 层的 module,这个 shell 并不做事情,他只用来将所有的依赖统一成一个入口交给上层的 app 去引入,而项目所有的依赖都可以写在 shell module 里面。

资源名冲突

因为分了多个 module,在合并工程的时候总会出现资源引用冲突,比如两个 module 定义了同一个资源名。
这个问题也不是新问题了,做 SDK 基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。
但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。

项目结构

app 是最终工程的目录
explorer 和 memory-box 是两个功能模块,他们在开发阶段是以独立的 application,在 release 时才会作为 library 引入工程。
router 有两个功能,一个是作为路由,用于提供界面跳转功能。另一个功能是前面讲的 shell ,作为依赖集合,让各业务 module 接入。 base-res 是一些通用的代码,即每个业务模块都会接入的部分,它会在 router 中被引入。

最终代码可以查看:https://github.com/kymjs/Modularity

Android 业务组件化开发实践的更多相关文章

  1. Android业务组件化之子模块SubModule的拆分以及它们之间的路由Router实现

    前言: 前面分析了APP的现状以及业务组件化的一些探讨(Android业务组件化之现状分析与探讨),以及通信的桥梁Scheme的使用(Android业务组件化之URL Scheme使用),今天重点来聊 ...

  2. Android业务组件化之现状分析与探讨

    前言: 从个人经历来说的话,从事APP开发这么多年来,所接触的APP的体积变得越来越大,业务的也变得越来越复杂,总来来说只有一句话:这是一个APP臃肿的时代!所以为了告别APP臃肿的时代,让我们进入一 ...

  3. Android业务组件化之Gradle和Sonatype Nexus搭建私有maven仓库

    前言: 公司的业务组件化推进的已经差不多三四个月的时间了,各个业务组件之间的解耦工作已经基本完成,各个业务组件以module的形式存在项目中,然后项目依赖本地的module,多少有点不太利于项目的并行 ...

  4. Android业务组件化之URL Scheme使用

    前言: 最近公司业务发展迅速,单一的项目工程不再适合公司发展需要,所以开始推进公司APP业务组件化,很荣幸自己能够牵头做这件事,经过研究实现组件化的通信方案通过URL Scheme,所以想着现在还是在 ...

  5. 【转】Android业务组件化之URL Scheme使用

    前言: 最近公司业务发展迅速,单一的项目工程不再适合公司发展需要,所以开始推进公司APP业务组件化,很荣幸自己能够牵头做这件事,经过研究实现组件化的通信方案通过URL Scheme,所以想着现在还是在 ...

  6. vue.js组件化开发实践

    前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了下面的内容.借油开车. 组件化 需求一到,接就是怎么实现,技术选型自然 ...

  7. vue组件化开发实践

    前言 公司目前制作一个H5活动,特别是有一定统一结构的活动,都要码一个重复的轮子.后来接到一个基于模板的活动设计系统的需求,便有了一下的内容.首先会对使用Vue进行开发的一些前期需要的技术储备进行简单 ...

  8. Android适合组件化开发的路由框架:Launch

    1.概述 最近越来越不想写代码了,特别是一些重复性的代码,比如由于每次启动一个 Activity,我们都会很习惯的在 Activity 中写下: public static void launch(A ...

  9. Android组件化开发实践

    转载请注明出处:http://blog.csdn.net/crazy1235/article/details/76533115 http://mdsa.51cto.com/art/201707/544 ...

随机推荐

  1. 初学Java经典例子

    我自己看的书的理解学习Java就是学习对象,就像谈恋爱,你对她多付出,收货就多(跑题了对象是啥??对象就是实体,通过类可以生成具有特定状态(或者叫属性)和行为或动作的实例,问题来了怎么创建? new一 ...

  2. Maven打包时过滤测试代码或指定特定的测试类(maven-surefire-plugin)

    1.过滤整个测试代码,可以直接在命令行上指定 mvn clean install -Dmaven.test.skip=true 提示:以上为举例,具体的构建阶段可以自定义,其中maven.test.s ...

  3. java内部类理解使用

    这是我学习Java内部类的笔记 1.为什么使用内部类?使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响1.1 ...

  4. Javascript:如何调用全局变量?

    怎样使用全局变量呢? window.globalVariableName 参考: https://blog.csdn.net/zyz511919766/article/details/7276089

  5. Setup and Teardown Thread Group in Jmeter

    setup和teardown有点类似于每个测试用例开始和结束时要做的动作 A Thread Group is the starting point of any Jmeter Test Plan. A ...

  6. ivy 入门

    ivy 入门 http://www.blogjava.net/aoxj/archive/2009/03/31/263012.html https://www.cnblogs.com/end/archi ...

  7. 推断dxf文件的版本号

    打开DXF參考手冊,在DXF參考手冊中,点击"索引"-->输入"HEADER",在ACADVER字段有acd的版本号信息: 以下是用C语言,写的推断dxf ...

  8. CloudStack系统虚拟机启动但不在Host表中

    有网友问到这个问题,CloudStack中,系统虚拟机正常启动,Running状态. 可是在host表中没有对应项,上传下载模板等功能也不正常. 原因:系统虚拟机启动之后,会通过管理网段主动连接man ...

  9. LINQ解决依据某个字段去重

    想要List结果反复 的数据非常easy.仅仅要.Dinstinct()就好了 可是假设想要依据某个字段去除反复的数据,上面的方法就帮不上忙了.我们须要重写一个方法.直接上样例吧 [Serializa ...

  10. linux输入子系统(5) - 学习框架

    注:本系列转自: http://www.ourunix.org/post/290.html input子系统学习系列文章,是我在实际开发过程中遇到也是必须啃下去的第一个Linux驱动,所以有必要记载下 ...