Android 业务组件化开发实践

组件化并非新话题,其实很早很早之前咱们开始为项目解耦的时候就讨论过的。但那时候咱们说的是功能组件化。好比不少公司都常见的,网络请求模块、登陆注册模块单独拿出来,交给一个团队开发,而在用的时候只须要接入对应模块的功能就能够了。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 重复依赖 
四、资源名冲突

解决 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

相关文章
相关标签/搜索