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

百牛信息技术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. HDFS api操作

    import java.net.URI;import java.util.Iterator;import java.util.Map.Entry; import org.apache.hadoop.c ...

  2. 全卷积网络FCN详解

    http://www.cnblogs.com/gujianhan/p/6030639.html CNN能够对图片进行分类,可是怎么样才能识别图片中特定部分的物体? (图像语义分割) FCN(Fully ...

  3. 【C语言天天练(九)】动态内存分配

    引言:数组的元素存储于内存中连续的位置上.当一个数组被声明时.它所须要的内存在编译时就被分配. 可是,我们能够使用动态内存分配在执行时为它分配内存. 一块内存的生命周期能够分为四个阶段:分配.初始化. ...

  4. Android命令行下蓝牙使用

    注意:此部分仅仅适用于broadcom 系列蓝牙芯片,比如RK903, AP6xxx 系列 通过su 命令切换到root 用户 1.先确认RFKILL 驱动已经载入 ls /sys/class/rfk ...

  5. 提高系统性能——对SQL语句优化的思考

    软件在研发的过程中自始至终都在留意着系统的可扩展性.但与此同一时候也在关注着系统的性能,SQL语句作为系统性能的一环不容忽视.从今天開始结合开发的经验,谈一下我对SQL语句优化的理解和认知: 1.在联 ...

  6. zabbix基于SNMP 协议监控路由器

    zabbix基于SNMP 协议监控路由器 步骤 步骤超级方便. 1. 路由器上开启snmp 2. 确保外网能訪问到 3. 用snmpwalk測试 4. 加入zabbix主机,SNMP interfac ...

  7. Linux主要命令

    pwd  查看当前路径 cd ..    表示后一级目录级 cd .   表示当前目录 cd ../..   后退两级 cd  表示进入当前家目录 date 返回当前的一个具体时间    -s  修改 ...

  8. 浅谈JavaScript的Canvas(绘制图形)

    HTML5中新增加的一个元素canvas,要使用canvas元素,浏览器必须支持html5.通过canvas标签来创建元素,并需要为canvas指定宽度和高度,也就是绘图区域的大小. <canv ...

  9. javascript模块化编程:CommonJS和AMD规范

    AMD规范,异步模块定义.与CommonJS规范齐名并列. 作用都是利于JavaScript的模块化编程. 模块化编程的好处就是: 1.可重用 2.独立 3.能解决加载的依赖性问题 4.能解决重复加载 ...

  10. [rabbitmq-discuss] Exactly Once Delivery

    [rabbitmq-discuss] Exactly Once Delivery http://lists.rabbitmq.com/pipermail/rabbitmq-discuss/2010-A ...