组件化会从项目中剥离出功能组件与业务组件。
功能组件是一个有独立功能的组件,例如分享组件、日志组件、工具类组件等,这些组件与业务无关,但可以为业务组件提供某些功能。拆分出的功能组件可以被不同的业务组件依赖,也可以被其他项目的业务组件依赖。
业务组件需要根据项目的具体情况进行拆分,将一个相对独立的业务拆分成一个单独的模块。每个业务组件都是一个独立的工程,可独立编译运行,且不同业务组件之间没有依赖关系。在集成打包时所有的业务组件会被打包成一个APK。
组件化的项目架构如下图所示:
app壳工程是一个空模块,没有任何业务逻辑。在开发时,各个业务组件能够作为一个APP独立运行,而在打包时,APP壳工程模块依赖所有业务组件,最终打包成一个完整的Apk。可以通过一个参数配置来表示各个模块是集成打包,还是独立运行。
组件化带来的好处
- 通过模块拆分降低项目耦合度。每个业务组件互不依赖,开发时也不会相互影响,测试时也只需要关注改动的组件,避免全盘回归。因此可以极大的提升开发效率。
- 多团队共用组件,节省开发和维护成本。拆分出来的功能组件具有独立的功能,提交到自己内部的maven私服后可以像依赖第三方库一样方便。如果公司项目有多个项目,那么功能组件可以被多个项目依赖,避免每个项目都单独维护。
- 各组件单独开发,节省编译时间,提高工作效率。每个业务组件都可以单独的作为一个APP运行,开发时只需要编译运行单个组件即可,编译速度更快。
-
如何让业务组件既能独立运行又能集成运行?
-
各个没有依赖关系的业务组件之间如何实现页面跳转?
-
各个没有依赖关系的业务组件之间如何获取Fragment的实例?
-
各个没有依赖关系的业务组件之间如何实现数据通信?
在项目跟目录的build.gradle下添加一个isBundleMode的boolean类型的参数,true表示各个模块集成打包,false表示各个模块可以独立运行。
ext {
isBundleMode = true
}
APP壳工程模块只有在所有模块集成打包时才有用,因此,只有当isBundleMode为true时才会依赖所有子模块。相关配置如下:
dependencies {
if (rootProject.ext.isBundleMode) { // 集成模式依赖所有子模块
implementation project(':home')
implementation project(':publish')
implementation project(':find')
implementation project(':user')
implementation project(':main')
} else { // 子模块独立运行模式,无需依赖子模块
implementation project(':common')
}
}
子模块即可以作为library集成打包到APP模块,也可以作为一个单独的APP独立运行。即可以通过isBundleMode参数来决定子模块是library还是application。在子模块的build.gradle中添加如下配置:
if (Boolean.valueOf(rootProject.ext.isBundleMode)) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
除此之外,如果是作为一个APP单独运行,还需要添加applicationId,因此可以添加如下配置。
android {
defaultConfig {
if (!rootProject.ext.isBundleMode) {
applicationId "com.zhpan.cypoem.home"
}
// ...
}
}
另外,由于application与library模式下AndroidManifest文件配置也是不同的,因此每个模块都需要两个AndroidManifest文件,一个作为单独运行的配置文件,一个作为集成运行的模块。
可以在src/main下边新建一个module目录,然后添加一个AndroidManifest文件作为独立运行的配置,如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhpan.module_me" >
<!--android:name属性——是用来设置所有activity属于哪个application的,默认是android.app.Application。-->
<application
android:name="com.zhpan.library.base.BaseApp"
android:allowBackup="true"
android:label="Me"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.zhpan.module_me.MeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
原本AndroidManifest文件作为集成环境下的配置,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.zhpan.module_me">
<application>
<activity android:name=".MeActivity"/>
</application>
</manifest>
接下来在build.gradle中根据isBundleMode参数加载不同的AndroidManifest清单文件,如下:
android {
sourceSets {
main {
if (rootProject.ext.isBundleMode) {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//排除java/debug文件夹下的所有文件
exclude '*module'
}
} else {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
}
}
}
}
这样便完成了组件化模块独立运行与集成运行的配置,通过修改isBundleMode参数即可实现。
不同模块间Activity的跳转、加载其他模块的Fragment以及模块间的通信都可以通过ARouter来实现。关于ARouter的配置这里不再赘述,详情参考ARouter主页。
为“发现”模块中的Activity设置Route路径,如下:
@Route(path = ACTIVITY_FIND)
public class FindActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_find);
}
}
在其他模块通过ARouter以及对应的Path即可跳转到FindActivity页面。如下:
ARouter.getInstance()
.build(RoutingTable.ACTIVITY_FIND)
.navigation();
在“发现”模块为Fragment添加Route路径,如下:
@Route(path = RoutingTable.FRAGMENT_FIND)
public class FindFragment extends BaseFragment {
@Override
protected int getLayout() {
return R.layout.fragment_find;
}
@Override
protected void initTitle() {
}
@Override
protected void initView(Bundle savedInstanceState) {
}
public static FindFragment getInstance() {
return new FindFragment();
}
}
接着在其他模块可以通过ARouter与对应的path获取这个Fragment的实例,代码如下:
BaseFragment findFragment = (BaseFragment) ARouter.getInstance().build(RoutingTable.FRAGMENT_FIND).navigation();
子模块之间避免不了的需要进行数据交换,比如用户模块提供用户信息,其他模块需要调用用户模块来获取用户信息。子模块之间没有相互依赖,因此需要通过其他方法来实现模块间的数据交换。
ARouter也提供了组件间数据交换的能力,实现方式如下。
首先在common模块定义一个提供用户信息的接口,并继承IProvider。如下:
import com.alibaba.android.arouter.facade.template.IProvider;
public interface IARouterUserService extends IProvider {
String getUserId();
String getUserName();
int getUserAge();
}
然后在用户模块添加上述接口的实现,如下:
@Route(path = RoutingTable.USER_DATA)
public class IARouterUserServiceImpl implements IARouterUserService {
@Override public String getUserId() {
return UserHolder.getUserId();
}
@Override public String getUserName() {
return UserHolder.getUserName();
}
@Override public int getUserAge() {
return UserHolder.getUserAge();
}
@Override public void init(Context context) {
}
}
注意这个实现类需要使用Route注解,并设置path。在这个类的各个方法中返回用户的信息。
接着,在其他模块的Activity或者Fragment中获取IARouterUserServiceImpl的实例。需要注意的是在使用时一定要通过 ARouter.getInstance().inject(this);
进行注入。
public class HomeFragment extends BaseFragment {
IARouterUserService userInfo;
// 可以通过Autowired进行自动初始化
@Autowired(name = RoutingTable.USER_DATA)
IARouterUserService userInfo2;
@Override
protected void initView(Bundle savedInstanceState) {
ARouter.getInstance().inject(this);
// 手动初始化
userInfo = (IARouterUserService) ARouter.getInstance()
.build(RoutingTable.USER_DATA)
.navigation();;
userInfo.getUserName();
userInfo2.getUserAge();
}
}
首先在common组件中定义一个获取用户信息的接口,如下:
public interface ISPIUserService {
String getUserId();
String getUserName();
int getUserAge();
}
然后在user模块实现这个接口,并返回用户信息。如下:
public class ISPIUserServiceImpl implements ISPIUserService {
@Override public String getUserId() {
return UserHolder.getUserId();
}
@Override public String getUserName() {
return UserHolder.getUserName();
}
@Override public int getUserAge() {
return UserHolder.getUserAge();
}
}
接着需要在APP模块下注册服务。在src/main目录下添加resources/META-INF/services目录,并且添加一个文件,这个文件的名字必须为com.zhpan.library.ISPIUserService
,即ISPIUserService的全路径名,文件中添加 com.zhpan.module_me.ISPIUserServiceImpl
一行配置,即ISPIUserServiceImpl这个类的全路径名。
完成配置后便可以通过JDK提供的API ServiceLoader 来获取ISPIUserServiceImpl的实例了,对ServiceLoader进行如下封装:
public class ServiceLoaderManager {
public static <T> T provide(Class<T> tClass) {
// 返回一个ISPIUserService实例的集合
Iterator<T> iterator = ServiceLoader.load(tClass).iterator();
if(iterator.hasNext()) {
// 这里只取第一个实例
return iterator.next();
}
throw new IllegalStateException(
"Can not find the implement class of " + tClass.getCanonicalName());
}
}
最后,便可以在common模块通过ServiceLoaderManager获取到ISPIUserServiceImpl的实例了。添加一个UserInfoTools的工具类,如下:
public class UserInfoTools {
// 获取用户名称
public static String getUserName() {
return getUserService().getUserName();
}
// 获取用户ID
public static String getUserId() {
return getUserService().getUserId();
}
// 获取用户年龄
public static int getUserAge() {
return getUserService().getUserAge();
}
// 获取ISPIUserServiceImpl实例
private static ISPIUserService getUserService() {
return ServiceLoaderManager.provide(ISPIUserService.class);
}
}
由于UserInfoTools是位于common模块中,所有依赖了common模块的子模块都可以通过UserInfoTools获取到用户信息。
例如APPJoint等第三方库都可以实现类似的功能。