1.组件化
组件化是指解耦复杂系统时,将多个功能模板拆分、重组的过程。在Android工程表现上就是把app按照其业务的不同,划分为不同的Module。
组件化架构的目的就是让每个业务模块变得相对独立,各个组件在组件模式下可以独立开发调试,集成模式下又可以集成到“app壳工程”中,从而得到一个具有完整功能的APP。
以美团外卖app为例:
组件化每一个组件都可以是一个APP,可以单独修改调试,而不影响总项目。
①app壳:负责管理各个业务组件和打包APK,没有具体的业务功能;
②业务组件层:最上层的业务,每个组件表示一条完整的业务线,彼此之间相互独立;
③功能/基础组件层:支撑上层业务组件运行的基础业务服务;
④基础库:包含了各种开源库以及和业务无关的一个自研工具库。
在组件化的层次中,下层的组件可以被上层的一个或多个模块调用,比如上图中的登录分享组件可以被美食组件调用,也可以被外卖组件调用。
注意模块化、组件化、插件化的区别:模块化粒度要高一些,以业务划分;组件化粒度更低,更注重基础功能的重用;插件化注重的是运行时动态的加载。
组件化的优势:
①各个组件专注自身功能的实现,模块中代码高度聚合,只负责一项任务,也就是常说的单一职责原则;
②各组件高度解耦,各业务研发可以互不干扰、提升协作效率;
③业务组件可进行拔插,灵活多变。即实现了功能重用, 某一块的功能在另外的组件化项目中使用,只需要单独依赖这一模块即可;
④业务组件之间将不再直接引用和依赖,各个业务模块组件更加独立,降低耦合;
⑤加快编译速度,提高开发效率
2.搭建组件化框架
以登录和个人中心两个功能组件为例:
①新建module
新建登录模块,并且在登录模块里新建一个activity。login和app都有一个绿点证明创建成功。
同理,创建个人中心member模块。
每个模块目前都可以独立运行。
②统一Gradle版本号
每个模块都是一个application,所以每个模块都会有一个build.gradle,各个模块里面的配置不同,因此需要重新统一Gradle。
在主模块创建config.gradle:
在config.gradle里添加一些版本号:
ext{
android = [
compileSdkVersion :30,
buildToolsVersion: "30.0.2",
applicationId :"activitytest.com.example.moduletest",
minSdkVersion: 29,
targetSdkVersion :30,
versionCode :1,
versionName :"1.0",
]
androidxDeps = [
"appcompat": 'androidx.appcompat:appcompat:1.1.0',
"material": 'com.google.android.material:material:1.1.0',
"constaraintlayout": 'androidx.constraintlayout:constraintlayout:1.1.3',
]
commonDeps = [
"arouter_api" : 'com.alibaba:arouter-api:1.5.1',
"glide" : 'com.github.bumptech.glide:glide:4.11.0'
]
annotationDeps = [
"arouter_compiler" : 'com.alibaba:arouter-compiler:1.5.1'
]
retrofitDeps = [
"retrofit" : 'com.squareup.retrofit2:retrofit:2.9.0',
"converter" : 'com.squareup.retrofit2:converter-gson:2.9.0',
"rxjava" : 'io.reactivex.rxjava2:rxjava:2.2.20',
"rxandroid" : 'io.reactivex.rxjava2:rxandroid:2.1.1',
"adapter" : 'com.squareup.retrofit2:adapter-rxjava2:2.9.0'
]
androidxLibs = androidxDeps.values()
commonLibs = commonDeps.values()
annotationLibs = annotationDeps.values()
retrofitLibs = retrofitDeps.values()
}
然后在主模块的build.gradle(最外层)里添加:
apply from: "config.gradle"
然后在app和各模块中引用这些版本号。引用格式如下:
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
或
buildToolsVersion rootProject.ext.android.buildToolsVersion
引用前:
引用后:
使用同样的方法还可以统一依赖库,方法是在config.gradle里添加要依赖的库,然后在各个模块中添加依赖:
implementation rootProject.ext.dependencies.publicImplementation
统一依赖库还有一种写法如下:
dependencies = [
"appcompat" : 'androidx.appcompat:appcompat:1.2.0',
"material" : 'com.google.android.material:material:1.2.1',
"constraintLayout" : 'androidx.constraintlayout:constraintlayout:2.0.4',//约束性布局
//test
"junit" : "junit:junit:4.13.1",
"testExtJunit" : 'androidx.test.ext:junit:1.1.2',//测试依赖,新建项目时会默认添加,一般不建议添加
"espressoCore" : 'androidx.test.espresso:espresso-core:3.3.0',//测试依赖,新建项目时会默认添加,一般不建议添加
]
添加依赖:
dependencies {
implementation rootProject.ext.dependencies.appcompat
implementation rootProject.ext.dependencies["constraintLayout"]
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
androidTestImplementation rootProject.ext.dependencies["espressoCore"]
}
③创建基础库
和新建module一样,这里需要新建一个library,命名为Baselibs:
同样需要统一版本号,由于这是一个library模块,所以它不需要applicationId:
同样可以把它写进config.gradle里:
other:[path:':Baselibs']
然后在每个模块去调用:
implementation project(rootProject.ext.dependencies.other)
注意:当本地库为单独所用,就可以不写入config.gradle,直接调用即可:
implementation project(':Baselibs')
但有时因为gradle版本问题,可能无法依赖到这些公共库,因为在config.gradle里是以数组形式定义的,这时可以用for-each循环的方法将其依次导入config.gradle里:
dependencies = [
......
other:[':Baselibs']
]
其他模块的build.gradle:
dependencies {
......
rootProject.ext.dependencies.other.each{
implementation project(it)
}
}
④组件模式和集成模式转换
在主模块gradle.properties里添加布尔类型选项:
在各个模块的build.gradle里添加更改语句:
if(is_Module.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
每个模块的applicationId也需要处理:
if(is_Module.toBoolean()){
applicationId "activitytest.com.example.login"
}
将is_module改为false时,再次运行编译器,模块都不能单独运行了:
在app模块中添加判断依赖就可以在集成模式下将各模块添加到app主模块中:
// 每加入一个新的模块,就需要在下面对应的添加一行
if (is_Module.toBoolean())]) {
implementation project(path:':login')
implementation project(path:':member')
}
⑤AndroidManifest的切换
为了单独开发加载不同的AndroidManifest这里需要重新区分下。
在组件模块的main文件里新建manifest文件夹:
并且重写一个AndroidManifest.xml文件,集成模式下,业务组件的表单是绝对不能拥有自己的Application和launch的Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有。在这个表单中只声明应用的主题,而且这个主题还是跟app壳工程中的主题是一致的:
package="com.example.login"> android:theme="@style/Theme.MoudleTest">
并且还要使其在不同的模式下加载不同的AndroidManifest,只需在各模块的build.gradle里添加更改语句:
sourceSets {
main {
if (is_Module.toBoolean()) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/mainfest/AndroidManifest.xml'
}
}
}
⑥业务Application切换
每个模块在运行时都会有自己的application,而在组件化开发过程中,主模块只能有一个application,但在单独运行时又需要自己的application,所以需要配置一下。
在业务模块添加新文件夹命名module
在里面新建一个application文件:
然后在build.gradle文件里配置module文件夹,使其在单独运行时能够运行单独的application。
在配置manifest的语句中添加java.srcDir 'src/main/module':
sourceSets {
main {
if (is_Module.toBoolean()) {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDir 'src/main/module'
} else {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
}
}
}
同时在basic基础层内新建application,用于加载一些数据的初始化:
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.e("fff","baseapplication");
}
}
在业务模块内module里重写该模块的application:
public class LoginApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
}
}
至此,组件化框架搭建结束。
2.组件化后的问题--组件之间的跳转
还是以美团app为例,美团app就是以组件化方式构建的,首页的“美食”和“外卖”可以看作是业务组件层的两个不同模块。在app首页可以点击“美食”或“外卖”分别进入到不同页面的首页,而且在“美食”首页也能直接跳转到“外卖”首页。
但是组件化有一个限制,就是禁止横向依赖,也就是说在同一层级下的多个模块是禁止相互依赖的。这样做是为了解耦,防止同一层级的模块高度耦合,同时防止两个模块循环依赖导致编译错误。
由于禁止横向依赖,导致组件化架构面临一个问题:同一层级中模块的通信和跳转不能使用常规的intent显示跳转了。这时候能解决问题的方式有使用intent隐式调用(每个activity都需要注册并添加intent-filter)、EventBus(消息多,代码乱)、广播(重)、Binder(重)等。最常用的就是ARouter框架。
3.使用ARouter实现组件间的跳转
采用阿里的开源库ARouter来实现跳转功能,ARouter是一个用于帮助Android App进行组件化改造的框架 ,支持模块间的路由、通信、解耦。
ARouter可以实现组件间的路由功能。路由是指从一个接口上收到数据包,根据数据路由包的目的地址进行定向并转发到另一个接口的过程。这里可以体现出路由跳转的特点,非常适合组件化解耦。
ARouter实现跳转的原理是在ARouter内部维护了一个路由表,路由表里的数据来自于各个模块的页面信息,每个模块负责向路由框架中注册自己的信息,这样需要页面跳转的时候只需要输入要跳转的页面信息,路由框架就会从路由表中去寻址找到该页面,然后就可以使用startActivity来跳转了。
要使用ARouter需要添加Arouter依赖,由于所有的组件都依赖了Baselibs模块,所以只在Baselibs模块中添加ARouter依赖即可,其它组件共同依赖的库也最好都放到Baselibs中统一依赖。
注意,arouter-compiler依赖需要所有使用到ARouter的模块和组件都单独添加,不然无法在apt中生成索引文件,也就无法跳转成功。并且在每一个使用到ARouter的模块和组件的build.gradle文件中,其android{}中的javaCompileOptions中也需要添加特定配置。
①添加依赖
在Baselibs里的build.gradle添加依赖:
dependencies {
api 'com.alibaba:arouter-api:1.3.1'
// arouter-compiler的注解依赖需要所有使用 ARouter的module都添加依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
//所有使用到ARouter的组件和模块的build.gradle
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
}
}
}
dependencies {
...
implementation project (':base')
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
主模块需要对跳转模块进行依赖:
// 主项目的build.gradle需要添加对login组件和member组件的依赖
dependencies {
// ... 其他
implementation project(':login')
implementation project(':share')
}
②初始化ARouter
添加了对ARouter的依赖后,还需要在项目的 Application中将ARouter初始化,这里将ARouter 的初始化放到主模块Application的onCreate()方法中,在应用启动时将ARouter初始化。
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 初始化 ARouter
if (isDebug()) {
// 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
// 初始化 ARouter
ARouter.init(this);
}
private boolean isDebug() {
return BuildConfig.DEBUG;
}
}
③添加跳转
在首页添加登录和个人中心两个跳转页面。
login.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build( "/login/login").navigation();
}
});
share.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ARouter.getInstance().build( "/member/member").navigation();
}
});
然后,在登录和个人中心组件中分别添加 LoginActivity 和MemberActivity ,并为两个 Activity添加注解 Route,其中path是跳转的路径,这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/login/login")
public class LoginActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
}
}
@Route(path = "/member/member")
public class MemberActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_share);
}
}
这样就可以实现跳转了。
4.组件之间的数据传递
由于主项目与组件、组件与组件之间是不可以直接使用类的相互引用来进行数据传递的,那么在开发过程中如果有组件间的数据传递时,需要采用 [接口 + 实现] 的方式来解决。
在Baselibs基础库里定义组件可以对外提供访问自身数据的抽象方法的Service,并且提供一个 ServiceFactory,每个组件中都要提供一个类实现自己对应的Service中的抽象方法。在组件加载后,需要创建一个实现类对象,然后将实现了Service类的对象添加到ServiceFactory中。这样在不同组件交互时就可以通过ServiceFactory获取想要调用的组件的接口实现,然后调用其中的特定方法就可以实现组件间的数据传递与方法调用。
当然,ServiceFactory中也会提供所有的Service的空实现,在组件单独调试或部分集成调试时避免出现由于实现类对象为空引起的空指针异常。
下面就按照这个方法来解决组件间数据传递与方法相互调用这个问题,这里通过分享组件中调用 登录组件 中的方法来获取登录状态是否登录这个场景来演示。
①定义接口
在service文件夹中定义接口,LoginService接口中定义了Login组件向外提供的数据传递的接口方法,EmptyService是service中定义的接口的空实现,ServiceFactory接收组件中实现的接口对象的注册以及向外提供特定组件的接口实现。
LoginService.java:
public interface LoginService {
//是否已经登录
boolean isLogin();
//获取登录用户的 Password
String getPassword();
}
EmptyService.java:
public class EmptyService implements LoginService {
@Override
public boolean isLogin() {
return false;
}
@Override
public String getPassword() {
return null;
}
}
ServiceFactory.java:
public class ServiceFactory {
private LoginService loginService;
//private禁止外部创建ServiceFactory对象
private ServiceFactory() {
}
//通过静态内部类方式实现ServiceFactory的单例
public static ServiceFactory getInstance() {
return Inner.serviceFactory;
}
private static class Inner {
private static ServiceFactory serviceFactory = new ServiceFactory();
}
//接收Login组件实现的Service实例
public void setLoginService(LoginService loginService){
this.loginService = loginService;
}
//返回Login组件的Service实例
public LoginService getLoginService(){
if(loginService == null){
return new EmptyService();
}else{
return loginService;
}
}
}
②实现接口
在login模块:
public class AccountService implements LoginService {
private boolean login;
private String password;
public AccountService(boolean login, String password) {
this.login = login;
this.password = password;
}
@Override
public boolean isLogin() {
return login;
}
@Override
public String getPassword() {
return password;
}
}
这里新建一个Util类用来存储登录数据:
public class LoginUtil {
static boolean isLogin = false;
static String password = null;
}
实现一下登录操作:
loginBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
LoginUtil.isLogin = true;
LoginUtil.password = "admin";
ServiceFactory.getInstance( ).setLoginService(new AccountService( LoginUtil.isLogin,LoginUtil.password));
}
});
在login模块的application里定义ServiceFactory类:
public class LoginApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
ServiceFactory.getInstance( ).setLoginService(new AccountService( LoginUtil.isLogin,LoginUtil.password));
}
}
在分享模块获取登录信息:
shareBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(ServiceFactory.getInstance( ).getLoginService().isLogin()){
Toast.makeText(ShareActivity.this,"分享成功!",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(ShareActivity.this,"分享失败,请先登录!", Toast.LENGTH_SHORT).show();
}
}
});
一个项目时只能有一个Application,Login作为组件时,主模块的Application类会初始化,而Login 组件中的Applicaiton不会初始化。确实是存在这个问题的,这里先将Service的注册放到其活动里,稍后解决Login作为组件时Appliaciton不会初始化的问题。
5.组件Application的动态切换
现在的问题是:在主模块中有Application的情况下,组件在集中调试时其Applicaiton不会初始化,而组件的Service在ServiceFactory的注册又必须放到组件初始化的地方。
为了解决这个问题可以将组件的Service类强引用到主Module的Application中进行初始化,这就必须要求主模块可以直接访问组件中的类。而我们又不想在开发过程中主模块能访问组件中的类,这里可以通过反射来实现组件Application的初始化。
①定义抽象类BaseApplication继承Application
在Baselibs基础库模块:
public abstract class BaseApplication extends Application {
public abstract void initModuleApp(Application application); //Application 初始化
public abstract void initModuleData( Application application); //所有Application初始化后的自定义操作
}
②所有组件的Application都继承BaseApplication
这里以Login模块为例:
public class LoginApplication extends BaseApplication{
@Override
public void onCreate() {
super.onCreate();
initModuleApp(this);
initModuleData(this);
}
@Override
public void initModuleApp(Application application) {
ServiceFactory.getInstance( ).setLoginService(new AccountService( LoginUtil.isLogin,LoginUtil.password));
}
@Override
public void initModuleData(Application application) {
}
}
③定义AppConfig类
在Baselibs模块定义一个静态的String数组,将需要初始化的组件的Application的完整类名放入到这个数组中。
public class AppConfig {
private static final String LoginApp = "com.example.login.LoginApplication";
public static String[] moduleApps = {
LoginApp
};
}
④主模块application实现两个初始化方法
// 主Module的Applicaiton
public class MainApplication extends BaseApp {
@Override
public void onCreate() {
super.onCreate();
// 初始化组件Application
initModuleApp(this);
// ……其他操作
// 所有Application初始化后的操作
initModuleData(this);
}
@Override
public void initModuleApp(Application application) {
for (String moduleApp : AppConfig.moduleApps) {
try {
Class clazz = Class.forName(moduleApp);
BaseApp baseApp = (BaseApp) clazz.newInstance();
baseApp.initModuleApp(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void initModuleData(Application application) {
for (String moduleApp : AppConfig.moduleApps) {
try {
Class clazz = Class.forName(moduleApp);
BaseApp baseApp = (BaseApp) clazz.newInstance();
baseApp.initModuleData(this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
通过反射完成了组件Application的初始化操作,也实现了组件与化中的解耦需求。
6.主模块使用其他组件的Fragment
开发中使用 Fragment一般都是直接通过访问具体 Fragment类的方式实现Fragment的实例化,但是现在为了实现模块与组件间的解耦,在移除组件时不会由于引用的Fragment不存在而编译失败,就不能模块中直接访问组件的 Fragment 类。
这里介绍两种方法
①ARouter 采用ARouter直接调用
fragment = (Fragment) ARouter.getInstance( ).build("/login/fragment").navigation();
②反射
以Login模块为例,假如在该模块创建一个用户界面,命名为UserFragment。
首先,在 Login组件中创建UserFragment,然后在LoginService接口中添加newUserFragment方法返回一个Fragment,在Login组件中的 AccountService和Baselibs中LoginService的空实现类中实现这个方法,然后在主模块中通过 ServiceFactory获取LoginService的实现类对象,调用其newUserFragment即可获取到UserFragment的实例。
// Baselibs模块的LoginService
public interface LoginService {
//其他代码...
Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag);
}
// Login组件中的AccountService
public class AccountService implements LoginService {
@Override
public Fragment newUserFragment(Activity activity, int containerId, FragmentManager manager, Bundle bundle, String tag) {
FragmentTransaction transaction = manager.beginTransaction();
//创建UserFragment实例,并添加到Activity中
Fragment userFragment = new UserFragment();
transaction.add(containerId, userFragment, tag);
transaction.commit();
return userFragment;
}
}
// 主模块的FragmentActivity
public class FragmentActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fragment);
// 通过组件提供的Service实现Fragment的实例化
ServiceFactory.getInstance().getAccou ntService().newUserFragment(this, R.id.layout_fragment, getSupportFragmentManager(), null, "");
}
}