不用去纠结组件和模块语义上的区别,若是模块间不存在强依赖且模块间能够任意组合,咱们就说这些模块是组件化的。android
实现组件化自己就是一个解耦的过程,同时也在不断对你的项目代码进行提炼。对于已有的老项目,实现组件化刚开始是很难受的,可是一旦组件的框架初步完成,对于后期开发效率是会有很大提高的。git
组件间间相互独立,能够减小团队间的沟通成本。github
每个组件的代码量不会特别巨大,团队的新人也能快速接手项目。bash
这是本文所主要讲述的内容,本篇文章同时适用于新老项目,文中会逐渐带领你们实现以下目标:多线程
理论篇不会讲述实际项目,先从技术上实现上面的三个目标。并发
组件间不存在强依赖从理论上来讲其实很简单,我不引用你任何东西,你也不要引用我任何东西就好了。但在实际项目中,须要清楚明白那些业务模块应该定义为组件,另外在已有项目中,拆分代码也须要大量的工做。hexo
组件间经过接口通讯。为每个组件定义一个或者多个接口,简单起见,咱们假定只为每个组件定义接口(多个接口是相似的)。app
便于理解,仍是要举实例。假设当前存在两个组件UserManagement(用户管理)和OrderCenter(订单中心),咱们为组件接口定义的模块的名为ComponentInterface。UserManagement和OrderCenter都依赖于ComponentInterface。为了有个直观的感觉,仍是放张图:框架
在ComponentInterface模块中新建为组件UserManagement的定义接口:ide
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)。
经过这样的修改,外部获取到的组件接口的实现就必定是非空的,也就是不管组件存在与否,都不会影响到项目主体,并且外部也并不须要关心组件是否存在。
理论篇从技术的角度介绍了如何实现组件化,不过对于实际项目,咱们使用组件化还会遇到诸多问题,下面将从实践的角度来帮助你们更快的实现项目组件化。
技术篇中,咱们每次获取组件接口的实现时都会反射一次,这显然是不合理的。咱们可使用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的方案,能够简化参数的传递和解析工做。感兴趣的同窗能够移步:github.com/a3349384/Fr…
最后欢迎关注个人博客:zhoumingyao.cn/