组件化并非新话题,其实很早很早之前咱们开始为项目解耦的时候就讨论过的。但那时候咱们说的是功能组件化。好比不少公司都常见的,网络请求模块、登陆注册模块单独拿出来,交给一个团队开发,而在用的时候只须要接入对应模块的功能就能够了。java
百牛信息技术bainiu.ltd整理发布于博客园android
今天咱们来讨论一下业务组件化,拿出手机,打开淘宝或者大众点评来看看,里面的美食电影酒店外卖就是一个一个的业务。若是咱们在一个项目里面去写的时候,总会出现或多或少的代码耦合,最典型的有时为了遇上线时间而先复制粘贴一段相似的代码过来,结果这段代码引用的资源多是另外一个模块独立的资源或代码。可是若是将一个项目做为独立的工程来运行,就彻底能够避免这种状况了。可是这并非业务组件化最大的优点,我认为最大的优点是它大大缩减了工程结构直接下降了编译时间。nginx
注意,组件化不是插件化,插件化是在[运行时],而组件化是在[编译时]。换句话说,插件化是基于多 APK 的,而组件化本质上仍是只有一个 APK。git
代码实现上核心思路要紧记一句话:开发时是 application,发版时是 library。
来看一段 gradle 代码:github
if (isDebug.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
很是好理解,咱们在开发的时候,module 若是是一个库,会使用com.android.library
插件,若是是一个应用,则使用com.android.application
插件,咱们经过一个开关来控制这个状态的切换。
而后由于咱们须要在 library 和 application 之间切换,manifest文件也须要提供两套。shell
你能够根据这个项目一块儿看:https://github.com/kymjs/Modularity浏览器
假设有一个项目,这个项目包含一个叫 explorer 的文件浏览器的模块和一个叫 memory-box 的笔记的模块。由于这两个功能相对独立,咱们将这两个功能拆分红两个 module,再加上本来项目的 app module,总共三个。
在 explorer 的根目录创建一个做为开关的 properties 文件(写一个全局变量也能够,怎么简单怎么来),方便用来改变当前是开发状态仍是发版状态(debug & release)。 从gradle中读取这个文件中的值,来切换不一样状态所须要调用的配置。顺便一提,当你修改了 properties 文件中的值时,必需要从新 sync 一下。 详细配置过程能够看看这篇文章:http://www.zjutkz.net/网络
阿布他们的项目大量的用了 databinding 和 dagger,然而咱们项目并无用这些,用了这两个库的能够看看他是怎么爬坑的:魔都三帅架构
当你采用了组件化开发的时候,必定会遇到这几个问题,这几个问题除了第三个都只能规避,没有好的处理办法:app
一、module 中 Application 调用的问题
二、跨 module 的 Activity 或 Fragment 跳转问题
三、AAR 或 library project 重复依赖
四、资源名冲突
因为 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; } } }
若是单独是 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