什么是组件化
不用去纠结组件和模块语义上的区别,如果模块间不存在强依赖且模块间可以任意组合,我们就说这些模块是组件化的。
组件化的好处
- 实现组件化本身就是一个解耦的过程,同时也在不断对你的项目代码进行提炼。对于已有的老项目,实现组件化刚开始是很难受的,但是一旦组件的框架初步完成,对于后期开发效率是会有很大提升的。
- 组件间间相互独立,可以减少团队间的沟通成本。
- 每一个组件的代码量不会特别巨大,团队的新人也能快速接手项目。
如何实现组件化
这是本文所主要讲述的内容,本篇文章同时适用于新老项目,文中会逐渐带领大家实现如下目标:
- 各个组件不存在强依赖
- 组件间支持通信
- 缺少某些组件不能对项目主体产生破坏性影响
组件化-理论篇
理论篇不会讲述实际项目,先从技术上实现上面的三个目标。
组件间不存在强依赖
组件间不存在强依赖从理论上来说其实很简单,我不引用你任何东西,你也不要引用我任何东西就行了。但在实际项目中,需要清楚明白那些业务模块应该定义为组件,另外在已有项目中,拆分代码也需要大量的工作。
组件间如何通信
组件间通过接口通信。为每一个组件定义一个或者多个接口,简单起见,我们假定只为每一个组件定义接口(多个接口是类似的)。
便于理解,还是要举实例。假设当前存在两个组件UserManagement(用户管理)和OrderCenter(订单中心),我们为组件接口定义的模块的名为ComponentInterface。UserManagement和OrderCenter都依赖于ComponentInterface。为了有个直观的感受,还是放张图:
在ComponentInterface模块中新建为组件UserManagement的定义接口:
public interface UserManagementInterface
{
//获取用户ID
String getUserId();
}
复制代码
UserManagement实现ComponentBInterface
:
public class UserManagementInterfaceImpl implements UserManagementInterface
{
@Override
public String getUserId()
{
return "UID_XXX";
}
}
复制代码
现在假定OrderCenter组件需要从UserManagement获取用户ID以便加载该用户的订单列表。那么问题来了,OrderCenter怎么才能调用到UserManagement的组件实现呢?这个问题可以通过反射来解决,只是需要满足组件的接口和组件接口的实现的路径和名称满足一定的约束条件。
我们定义组件接口和其实现的路径和名称的约束条件如下:
- 组件的接口和组件接口的实现必须定义在同一个包名下。
- 组件接口的实现的类名可以通过组件的接口的类名推导出来。比如每一个接口的实现的类名都是在该接口的名称后面接上“Impl”。
那么现在,我们的工程目录大概就像这个样子:
接下来,在OrderCenter组件中就可以通过反射获取到UserManagement组件接口的实现了,我们定义一个ComponentManager
类:
public class ComponentManager
{
public static <T> T of(Class<T> tInterface)
{
String interfaceName = tInterface.getCanonicalName();
String implName = interfaceName + "Impl";
try
{
T impl = (T) Class.forName(implName).newInstance();
return impl;
}
catch (Exception ex)
{
ex.printStackTrace();
return null;
}
}
}
复制代码
然后在OrderCenter就可以通过ComponentManager
来获取UserManagement的组件接口实现了:
String userId = ComponentManager.of(UserManagementInterface.class).getUserId();
复制代码
至此,组件间通信的问题就算解决了,而且组件之间还是不存在强依赖。
缺少某些组件不能对项目主体产生破坏性影响
假设打包后的项目不存在UserManagement
组件,上面获取userId的代码会有什么问题?ComponentManager.of(UserManagementInterface.class)
这里的返回必然为null,我们的代码就会产生空指针异常。
那么如何解决这个问题呢?像下面这样吗:
UserManagementInterface userManagementInterface = ComponentManager.of(UserManagementInterface.class);
if (userManagementInterface != null)
{
userId = userManagementInterface.getUserId();
}
复制代码
从程序运行的角度来看,上面的代码没有什么问题。但从码农的角度来看,上面代码写起来必然不是很舒爽,整个项目中会充斥着这样的非空判断。
我们期望,在某个组件不存在时,通过ComponentManager.of
获取的组件接口实现可以具备一个默认值。在Java中,我们可以通过动态代理在运行时动态生成一个接口的实现。 我们修改ComponentManager
的代码:
public class ComponentManager
{
public synchronized static <T> T of(Class<T> tInterface)
{
String interfaceName = tInterface.getCanonicalName();
String implName = interfaceName + "Impl";
try
{
T impl = (T) Class.forName(implName).newInstance();
return impl;
}
catch (Exception ex)
{
ex.printStackTrace();
ClassLoader classLoader = ComponentManager2.class.getClassLoader();
T fakeImpl = (T) Proxy.newProxyInstance(classLoader, new Class[]{tInterface}, new DefaultInvocationHandler());
return fakeImpl;
}
}
private static class DefaultInvocationHandler implements InvocationHandler
{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
Class<?> returnClass = method.getReturnType();
if (!returnClass.isPrimitive())
{
return null;
}
String returnClassName = returnClass.getCanonicalName();
if (returnClassName.contentEquals(boolean.class.getCanonicalName()))
{
return false;
}
if (returnClassName.contentEquals(byte.class.getCanonicalName()))
{
return (byte)0;
}
if (returnClassName.contentEquals(char.class.getCanonicalName()))
{
return (char)0;
}
if (returnClassName.contentEquals(short.class.getCanonicalName()))
{
return (short)0;
}
if (returnClassName.contentEquals(int.class.getCanonicalName()))
{
return (int)0;
}
if (returnClassName.contentEquals(long.class.getCanonicalName()))
{
return (long)0;
}
if (returnClassName.contentEquals(float.class.getCanonicalName()))
{
return (float)0;
}
return (double)0;
}
}
}
复制代码
我们判断了接口方法的返回值,如果返回值为引用类型则直接返回null,否则返回值类型的默认值(boolean返回false,其他返回0)。
通过这样的修改,外部获取到的组件接口的实现就一定是非空的,也就是无论组件存在与否,都不会影响到项目主体,而且外部也并不需要关心组件是否存在。
组件化-实践篇
理论篇从技术的角度介绍了如何实现组件化,不过对于实际项目,我们使用组件化还会遇到诸多问题,下面将从实践的角度来帮助大家更快的实现项目组件化。
ComponentManager优化
技术篇中,我们每次获取组件接口的实现时都会反射一次,这显然是不合理的。我们可以使用Map将组件和组件接口的实现的关系保存下来。
另外还需要考虑多线程并发的问题。
在实际项目中,有时为了方便测试,会期望能够主动为某个组件接口指定一个假的实现,我们可以增加一个注入组件接口实现的方法。
组件化的项目结构
将项目中的基础类库提取出来是组件化应该要做的第一件事情。基础类库不应掺杂过多的业务逻辑,基础类库要考虑不仅能够应用与当前产品,也可以应用于其他产品。
每一个组件化工程都应该存在至少一个以上的Common库,Common库可以依赖下面的基础库。Common库中可以放置一些通用的资源(如返回按钮图标、全局的字体大小、全局的字体样式等)以及对一些业务逻辑的封装(如BaseActivity、HttpClient)
最上面就是组件层了,组件可以依赖Common库,也可以依赖基础库。最后将各个组件组合起来,就一个完整的App。
组件的代码如何隔离
由于组件之间是不能相互直接依赖,所以组件间也不存在代码隔离的问题。问题主要出现在App壳上,App壳依赖了所有的组件,如果采用implementation
依赖方式,在App壳中还是能够访问组件中的代码的,我们可以采用runtimeOnly
这种依赖方式。
组件的资源如何隔离
由于当前没有更好的方式对各个组件的资源进行隔离(runtimeOnly
也不能隔离),所以我们通过命名的约定来避免某个组件引用不属于本组件的资源。
组件中的资源,如字符串、图标、菜单等的名称应该以组件的名称开头,如:
usermanagement_login
ordercenter_delete
复制代码
渐进式组件化
老项目要完全组件化是会有较长一个周期要走,通常也太可能专门拿出几个月让你来实现组件化,所以要实现渐进式组件化,才能真正将组件化应用到实际项目中。
实际项目中,由于本身开发任务就很重,所以不要太期望能够有足够的时间让你将某个模块完全组件化。我这边的做法是:
- 给App主模块也定义一个组件接口
- 日常开发中可以慢慢将某个模块组件化,没有完全组件化也没关系,可以在App组件接口中为那些还未完全组件化的功能定义一系列接口
- 这样,耦合在App模块中的尚未完全组件化的代码就可以在该组件中进行调用了
- 后期有时间完整该组件的组件化的工作后把App组件接口中相关方法删掉就可以了
这样的组件化开发方式几乎不会对日常开发工作造成太大的影响,随着日常开发工作的进行,项目组件化的程度也在慢慢提升。
组件如何单独运行
组件单独运行也是我们开发人员比较强烈的一个需求。主要存在以下方面的原因:
- 单独运行组件需要的编译、打包、安装时间会大大降低,可以节约很多等待时间
- 组件能够单独运行也表示我们不用等待其他组件完成才能开始测试。实际项目协作中,我们可以预先定义好组件间的通信接口,这样通过组件接口实现注入,就可以开始组件的测试,完全不需要等待其依赖的组件完成后才能开始测试。
很多文章都在使用将plugin由com.android.library
修改为com.android.application
,让组件由一个库变成一个应用程序使得组件能够单独运行。这确实是一个办法,不过对于大部分组件,只修改plugin的类型是完全不够的。很多组件都需要一些特定的参数才能运行起来,比如订单列表这个功能肯定是需要用户ID才能展示出来的。所以我们还是要想办法如何在组件独立运行时能够给组件传递参数。
我采用了一种略微不同的方法来运行组件。
我创建了一个Application类型的Module:ComponentTest
来运行组件。在build.gradle中为每一个组件创建一个productFlavor
,示例如下:
productFlavors {
userManager {
applicationIdSuffix ".userManager"
manifestPlaceholders = [appName : "用户管理"]
}
}
<manifest>
<application
android:label="${appName}">
</application>
</manifest>
复制代码
在完成这样的配置之后,每一个组件都具备自己独特的applicationId,也就是手机上可以同时安装不同的组件应用程序。
然后通过每个productFlavor
特有的依赖方式将组件实现依赖进来,例如:
userManagerRuntimeOnly project(':userManager')
复制代码
然后我们就可以在src
目录创建一个和productFlavor
同名的目录。在这个目录下面可以书写每个组件自己的测试代码。当然我们还可以在src/main
下面书写一些各个组件都可能使用到的通用代码,src/main
的内容在其他productFlavor
目录下是可以访问的。
在实际项目中,我会给每个组件程序写一个MainActivity,MainActivity里面很简单,就是一排按钮,每一个按钮对应着组件接口中的一个方法。这样开发时很方便测试,开发完成时至少也能够保证组件基本可用,不太会出现别人一调用你的组件就出错的情况。
最后,运行某个组件时,需要在AS的Build Variants
中选择该组件定义的productFlavor
。
页面跳转
可以为每一个页面跳转定义一个接口方法:
public interface UserManagementInterface
{
//跳转到用户信息页面
String startToUserInfoPage(Context context);
}
复制代码
然后在startToUserInfoPage
的实现中实现具体的跳转逻辑。
现在android上主流的页面导航方式有三种:
- 不同的页面对应不同Activity类型
- 在Activity中使用Fragment导航,在Activity中同时
- 使用Activity导航,和第一种不同的是Activity只充当Fragment的容器
针对第一种导航方式,在直接使用Intent跳转就可以,当然使用当前流行的ARouter也行。
针对第二种导航方式,把把FragmentManager放到Common中可能是比较好的办法。如果有更好的办法,感谢分享。
我个人比较喜欢第三种导航方式,在项目中也是用的这种导航方式。第三种导航方式同时具备第一种和第二种导航方式的优点,当然它也有比较大的缺点。金无足赤,人无完人,选择合适的就好。
首先创建一个Activity用做Fragment的容器,比如就叫TheActivity。(命名规范中肯定不推荐用The,但是实际上项目中就这么个Activity,用The也不会造成什么理解困难)
TheActivity的启动参数至少要包含要包含的Fragment的名称(有了名称就可以通过反射创建Fragment),还要包含Fragment自身需要的参数。
核心代码很简单就像下面这样:
Fragment fragment = createFragment();//使用反射创建Fragment
getSupportFragmentManager().beginTransaction()
.replace(fragmentContainerId, fragment)
.commit();
复制代码
有些东西核心思想很简单,但是实际项目中使用会暴漏很多问题。
比如需要在Activity中解析Intent参数,有多少个跳转你几乎就要写多少个解析方法,然后在Fragment中还要再解析一次。
人天性就不会喜欢做这种重复又毫无营养的事情,我抽空做了一个基于注解和AnnotationProcessor的方案,可以简化参数的传递和解析工作。