组件化
通常单一模块的应用里会将所有代码放在一个app module下。
弊端:
-
随着项目的扩大,项目失去层次感,代码冗杂。
-
包名约束作用不大,可能会出现业务之间相互调用,代码高度耦合。
-
多人联合开发中容易出现冲突和代码覆盖的情况。
-
测试运行时耗时,每次都会将整个项目重新编译打包。
组件是对数据和方法的简单封装,功能单一,高内聚,并且是业务能划分的最小粒度。
组件化是基于可重用的目的,将大型软件系统按照分离关注点的形式拆分成多个独立的组件,做到少耦合和高内聚。
组件化架构的目的时是告别结构臃肿,让各个业务变得相对独立,业务组件在组件模式下可以独立开发,相当于一个个application;而在集成模式下又可以变为arr包集成到“app壳工程中”组成一个完整功能的app(作为不同的library)。
组件化的业务之间不再直接引用和依赖,而是通过“路由”作为中转站简介产生联系。
组件化的优势
-
可以加快项目的编译速度:各个组件可以单独编译运行,可以按需启动组件测试。
-
高度解耦:模块之间不能随便调用,降低模块之间的耦合。
-
功能重用:其他项目可以通过依赖该组件复用其功能。
-
组件可有自己独立的版本,业务线互不干扰,可单独编译、测试、打包、部署;
-
通过gradle配置文件,可对第三方库进行统一的管理,避免版本冲突,减少冗杂;
-
等。
组件分层
Android---组件化_android 组件化-CSDN博客
Android 手把手带你搭建一个组件化项目架构 - 掘金 (juejin.cn)
组件化的具体实现
创建组件Module
创建一个主项目,作为app壳工程,是整个应用的入口。
创建2个业务组件,一个main,一个login。
选中项目右击,New->Module。
业务组件都选择Phone&Tablet。
为了分层更加明显,在Module name中加入了moduleCore,业务组件都放入这个moduleCore文件夹下。在Package name中加入了module是为了避免命名冲突。
之后Next->Finish完成创建。
同样的方法再创建login业务组件。直接右击moduleCore文件夹创建新的Module。
创建基础组件
这次选择Android Library。并在Module name中加入moduleBase表示创建基础组件文件夹。
创建功能组件
同样选择Android Library,在Module name中加入modulePublic表示创建modulePublic文件夹用于存储功能组件。
统一配置文件
对第三方依赖做一个统一的管理,避免因依赖的版本不同而产生冲突。
在项目的根目录下创建一个config.gradle文件,对项目进行全局的统一配置,并对版本和依赖进行管理。
ext { //extend
// false: 集成模式
// true :组件模式
isModule = false
android = [
compileSdkVersion: 33,
minSdkVersion : 24,
targetSdkVersion : 33,
versionCode : 1,
versionName : "1.0",
applicationId : "com.example.componentmoduletest2"
]
applicationId = ["app" : "com.example.componentmoduletest2",
"main": "com.example.module.main",
"login" : "com.example.module.login" ]
dependencies = [
glide : 'com.github.bumptech.glide:glide:4.16.0',
cicleimagview : 'de.hdodenhof:circleimageview:3.1.0',
swiperefreshlayout : 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0',
eventbus : 'org.greenrobot:eventbus:3.3.1',
gson : 'com.google.code.gson:gson:2.10.1'
]
libARouter = 'com.alibaba:arouter-api:1.5.2'
libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
libEventBusCompiler = 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}
然后再project的build.gradle中引入config.gradle文件:
apply from: 'config.gradle'
之后点击Sync Now同步。
业务组件配置build.gradle
修改main业务组件的build.gradle文件(login业务组件也一样)
-
因为业务组件在组件化开发过程中需要作为application单独运行,所以要指定是application还是library。
-
将这些配置都改成ext中定义的全局参数。
如果业务组件在组件化开发过程中,需要作为application单独运行就需要指定applicationId。否则不需要。
-
业务组件需要依赖基础组件,他要用的第三方库的依赖通过基础组件去依赖。
implementation project(':文件夹:组件名')
基础组件配置build.gradle文件
修改基础组件libBase中的build.gradle文件。通常业务组件都是要依赖基础组件的,所以业务组件所需要用到的第三方库都可以在基础组件中依赖。
-
因为基础组件不需要作为Application单独运行,所以一直是library就行。定义了变量,可以取到config.gradle文件中et
-
将配置修改为全局参数。基础组件不会单独作为application运行,所以不需要applicationId。
-
基础组件依赖全局提供的第三方库和SDK。其他业务组件和功能组件只要以来基础组件就能使用。
通常需要添加依赖的时候,会在config.gradle文件中添加全局变量,然后在libBase(基础组件)中去依赖,这样都可以使用新添加的依赖,对依赖的包进行统一管理。
功能组件配置build.gradle文件
功能组件也同样不会单独作为一个application去运行,他和基础组件一样作为library配置。
功能组件和业务组件一样通过依赖基础组件去使用第三方库。
app配置build.gradle文件
app是一个特殊的module。他作为应用的启动层,一定是一个application。
这里app壳工程也依赖基础组件。
组件之间AndroidManifest合并问题
每个组件都会有自己的AndroidManifest.xml文件,用于声明权限和配置Application、Activity、Service等。当处于组件化开发过程中,每个业务组件都应该具有一个完整配置的AndroidManifest文件,特别是声明的Application和launch的Activity上会出现不同。当变成集成模式时,这些AndroidManifest.xml文件都需要合并到app壳工程中,合并的时候可能会因为这些不同的Application和launch的Activity发生冲突。
其他组件不会单独作为application运行,永远只是一个library就不用担心这个问题。
解决办法:我们可以为业务组件创建一个新的AndroidManifest,然后根据isModule的值判断当前时组件开发模式还是集成开发模式,给两种模式指定不同的AndroidManifest的路径。
我们在一个组件下面的main新建一个release文件夹表示集成开发时Androidmanifest.xml的路径。
右击此文件夹,New-->Other-->Android Manifest File。
勾选Change File Location之后填入详细路径,点击Finish。
之后在业务组件的build.gradle中指定表单(AndroidManifest)的路径。
检查AndroidManifest中的内容。
因为我们通过New Module创建出来的业务组件自带的AndroidManifest.xml文件是该组件作为一个Application单独运行时使用的。我们创建的release文件夹下的AndroidManifest.xml文件是集成模式下的。它不需要app壳工程中指定的属性,否则会重复冲突。
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:theme="@style/Theme.ComponentModuleTest">
<activity android:name=".MainActivity"/>
</application>
</manifest>
在业务组件的build.gradle文件中指定AndroidManifest.xml的路径。
我看到有些博客还有用这种方法的
组件化的简单配置就到此结束。
可以看到当isModule为false时,是不能启动main和login业务组件的。
将isModule改为true,点击Sync Now。
可以看到app壳工程和两个业务组件都可以运行。
Android---组件化_android 组件化-CSDN博客
组件间的跳转(ARouter)
这些代码都是在集成模式下运行的。
ARouter依赖配置到组件化项目中
-
在全局的config.gradle文件中添加依赖和配置
ext { ... libARouter = 'com.alibaba:arouter-api:1.5.2' libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2' }
-
在基础组件中依赖ARouter
dependencies { ... api cfg.libARouter ... }
-
在其他业务组件和功能组件中配置注解编译器
android { defaultConfig { ... javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName()] //如果项目内有多个annotationProcessor,则修改为以下设置 //arguments += [AROUTER_MODULE_NAME: project.getName()] } } } } dependencies { implementation project(':moduleBase:libBase') //每个使用ARouter的组件都需要依赖ARouter注解处理器 annotationProcessor cfg.libARouterCompiler ... }
编写注解
在支持路由的页面上添加注解。
@Route(path = "/main/MainActivity")
//路径至少需要两级
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
初始化
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
//
ARouter.openLog();//开启日志打印
ARouter.openDebug();//开启调试模式
}//需要写在init之前
ARouter.init(this);
}
}
ARouter的初始化应该尽可能早,一般推荐卸载Application(onCreate())中进行。
注意:
在配置ARouter时,如果运行出现错误:显示资源文件合并出现问题。可以尝试在项目根目录下的gradle.properties文件中添加:
android.enableJetifier=true
发起路由操作(跳转)
在app壳工程的主活动里,也就是应用一开始进入的活动。直接使用ARouter实现跳转到main组件中的MainActivity中。
public class StartActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ARouter.getInstance().build("/main/MainActivity").navigation();
}
}
在MainActivity中设置点击监听,点击文本时跳转LoginActivity。
@Route(path = "/main/MainActivity")
public class MainActivity extends AppCompatActivity {
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, "This is Main Module.", Toast.LENGTH_SHORT).show();
findViewById(R.id.click).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance().build("/login/LoginActivity").navigation();
}
});
}
}
其他一些点
-
在跳转的时候还可以自定义跳转动画
传入R.anim.*动画资源作为参数。
-
Fragment的跳转
Fragment fragment = (Fragment) ARouter.getInstance().build("/targetmodule/TheFragment").navigation(); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment) .commit();
携带参数跳转
跳转启动方
ARouter.getInstance().build("/login/LoginActivity")
.withBoolean("boolean", true) //传递布尔值,key,value
.withString("content", "data") //还可以传递String,int,Bundle, Object等类型的数据
.navigation();
请注意这里携带参数会有一个withObject()方法。这个方法的使用更有一些特殊,我会在后面ARouter的进阶使用中介绍。
接收方
@Route(path = "/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
//为每一个参数声明一个字段,并使用@Autowired注解标记
@Autowired(name = "boolean")
public boolean d1;
//可以通过name来映射URL中的不同参数
//指定该字段接收发送方定义key为content的数据
@Autowired(name = "content")
public String d2;
@SuppressLint("MissingInflatedId")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
...
//自动注入参数
ARouter.getInstance().inject(this);
Log.d("receiveLOGIN", "onCreate: " + d1 + " " + d2);
}
}
结果正常
我确实出现了这个问题,在组件开发过程,如果启动了main组件,在main组件中跳转到login组件会显示There`s no matched route!但是尝试了一下没能解决。
Android 手把手带你搭建一个组件化项目架构 - 掘金 (juejin.cn)
组件间的通信
刚才实现的时组件之间的跳转包括携带数据的不不携带数据的。但如果我们只想要传递数据而不想要跳转页面怎么办?
假设现在在main_module中想要获取login_module的登陆状态数据,之后再进行后续逻辑。
实现方法:接口 + ARouter
这个接口可以直接写在基础组件libBase的包下。
也可以另开一个基础组件,写在那个基础组件里。之后在libBase依赖那个基础组件。
implementation project(':moduleBase:libInterface')
比如新建的基础组件是moduleBase文件夹下的libInterface。这样依赖在libBase中就可以了。因为libBase在前面的配置中已经被每个组件依赖过了,所以现在实现的这个接口是个全局都可以调用的。
我学习的一篇博客是定义在一个新建的基础组件中的。我想这样更合理,将libBase的功能更明确是为了依赖配置。
Android 手把手带你搭建一个组件化项目架构 - 掘金 (juejin.cn)
我写的练习demo是直接写在libBase中的。就不用再添加新的依赖。都可以。
创建接口,暴露服务
接口一定要继承IProvider
public interface IAccountService extends IProvider {
//判断是否登陆
boolean isLogin();
//获取账号信息 id
String getAccountId();
}
这个接口继承IProvider之后写了两个方法,用来获取信息。
在login_module组件中实现接口
@Route(path = "/login/AccountServiceImpl")
public class AccountServiceImpl implements IAccountService {
@Override
public boolean isLogin() {
return true;
}
@Override
public String getAccountId() {
return "10000";
}
@Override
public void init(Context context) {
}
}
记得要添加Route注解和path,因为要在其他组件中使用ARouter去注入参数。
在main_module中发现服务,接收数据
@Route(path = "/main/MainActivity")
public class MainActivity extends AppCompatActivity {
@Autowired
IAccountService accountService;
//1. 这里添加了注解@Autowired,声明接收数据参数的变量
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, "This is Main Module.", Toast.LENGTH_SHORT).show();
ARouter.getInstance().inject(this);
//2. 注入参数
Log.d("accountService", "onCreate: " + accountService.isLogin() + " " + accountService.getAccountId());
}
}
结果正常。
利用接口(IProvider) + ARouter这种思路也可以实现组件之间相互调用一些方法等。
Android 手把手带你搭建一个组件化项目架构 - 掘金 (juejin.cn)
Android组件化开发_豆Android的博客-CSDN博客
Android---组件化_android 组件化-CSDN博客
ARouter的一些进阶使用
GitHub地址ARouter/README_CN.md at master · alibaba/ARouter (github.com)
1. 如何在组件之间传递自定义的对象
使用上面介绍的 接口 + ARouter的方式进行传递。
自定义的类分两种请况:
-
如果可以放在基础组件中,基本所有组件都依赖了它所在的基础组件,那么这个类就相当于全局的。它的对象也可以直接传递,通过ARouter + 接口(暴露服务)的方法,和上面没什么区别。但这种方法让数据类放在基础组件中,其他组件依赖基础组件进行配置的话,会不会让这个类重复多次加载,导致性能下降,运行缓慢?我是个初学者,我也不太懂。
-
在官方文档中介绍了SerializationService这个接口。
// 如果需要传递自定义对象,新建一个类(并非自定义对象类),然后实现 SerializationService,并使用@Route注解标注(方便用户自行选择序列化方式),例如: @Route(path = "/yourservicegroupname/json") public class JsonServiceImpl implements SerializationService { @Override public void init(Context context) { } @Override public <T> T json2Object(String text, Class<T> clazz) { return JSON.parseObject(text, clazz); } @Override public String object2Json(Object instance) { return JSON.toJSONString(instance); } }
官方文档是这么写的。根据搜索浏览,这个JsonServiceImpl类在实现了SerialzationService之后可能是配合着withObject()方法使用的。
withObject()方法的使用要求传递的数据类不能实现Serializable或者Parcelable。正是因为withObject()方法内部使用了JsonServiceImpl类进行了序列化和反序列化。
如果要传递的数据类因为特殊原因必须实现实现Serializable或者Parcelable完成序列化,那么请使用withParcelable()或者withSerializable()方法传递。
目前Demo中尚未实现JsonServiceImpl类。可以试着用withObject()方法传递一个简单的自定义类。
ARouter.getInstance().build("/main/MainActivity") .withObject("object", new Student("张三", 23)) .navigation();
发现程序崩溃,报错:
java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.String com.alibaba.android.arouter.facade.service.SerializationService.object2Json(java.lang.Object)' on a null object reference
证实了withObject()是配合SerialzationService的实现类使用的。
正确使用withObject()
-
定义一个类实现SerializationService接口。
这个类好像无所谓定义在哪个组件中,只要声明好Route路径就好。
@Route(path = "/main/json") public class JsonServiceImpl implements SerializationService { Gson gson; @Override public <T> T json2Object(String input, Class<T> clazz) { return parseObject(input, clazz); } @Override public String object2Json(Object instance) { //对象转成Json字符串 return gson.toJson(instance); } @Override public <T> T parseObject(String input, Type clazz) { //字符串转成对象 return gson.fromJson(input, clazz); } @Override public void init(Context context) { gson = new Gson(); } }
这里我借用了Gson实现对象和json字符串之间的转换。
需要在基础组件中添加Gson的依赖。
简单说一下过程,在config.gradle的dependencies中添加依赖的包的字符串;在基础组件libBase的build.gradle中添加api依赖的语句;点击Sync同步就好了。
-
使用withObject()方法发起携带自定义对象的跳转
tv.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { ARouter.getInstance().build("/main/SecondActivity") .withObject("object", new Student("张三", 23)) .navigation(LoginActivity.this, new NavigationCallback() { @Override public void onFound(Postcard postcard) { //当路径目标成功被找到时 Log.d("NavigationCallback", "onFound: 找到了"); } @Override public void onLost(Postcard postcard) { //当找不到路径目标的时候 Log.d("NavigationCallback", "onLost: 找不到目标路径"); } @Override public void onArrival(Postcard postcard) { //当导航至目标路径时调用 Log.d("NavigationCallback", "onArrival: 跳转完成"); } @Override public void onInterrupt(Postcard postcard) { //当导航被拦截器拦截时调用 Log.d("NavigationCallback", "onInterrupt: 跳转至MainActivity被中断"); } }); } });
发送到"/main/SecondActivity"中。
-
在跳转目标页面的类中接收
@Route(path = "/main/SecondActivity") public class SecondActivity extends AppCompatActivity { @Autowired(name = "object") Object stu; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_second); Toast.makeText(this, "This is SecondActivity in MainModule", Toast.LENGTH_SHORT).show(); ARouter.getInstance().inject(SecondActivity.this); if (stu != null) { Log.d("NavigationCallback", "onCreate: " + stu.toString()); } else { Log.d("NavigationCallback", "onCreate: stu == null"); } } }
完成传递。
2. 拦截器实现
拦截器就是在跳转过程中,设置的对跳转的检测和预处理等操作
声明拦截器
@Interceptor(priority = 1, name = "myTestInterceptor")
//注解中的优先级必须是一个唯一的int值,不能存在两个相同的优先级,否则程序会崩溃。
//设置的priority越小优先级越高
public class MyInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
//在这里处理拦截后的操作
//...
//处理完成后使用callback.onContinue(postcard);交还控制权
callback.onContinue(postcard);//通过继续执行跳转
// callback.onInterrupt(new RuntimeException("有异常"));
//如果觉得有问题,中断路由流程
//以上两种必须至少使用一种,否则路由不会继续
}
@Override
public void init(Context context) {
//在ARouter初始化的时候执行
}
}
假设:我们现在要实现拦截器,判断路由,如果是跳转向路径“/main/MainActivity”的路由就拦截,让他跳转向路径“/main/SecondActivity”。
@Interceptor(priority = 1, name = "myTestInterceptor")
public class MyInterceptor implements IInterceptor {
@Override
public void process(Postcard postcard, InterceptorCallback callback) {
Log.d("MyInterceptor", "process: 拦截");
if (postcard.getPath().equals("/main/MainActivity")) {
//中断
callback.onInterrupt(null);
//跳转
Log.d("MyInterceptor", "process: 跳转到SecondActivity");
ARouter.getInstance().build("/main/SecondActivity").navigation();
} else {
//继续
callback.onContinue(postcard);
}
}
@Override
public void init(Context context) {
}
}
在LoginActivity中设置点击事件跳转到路径“/main/MainActivity”
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance().build("/main/MainActivity").navigation();
}
});
查看日志:
成功拦截。
ARouter的拦截器一旦定义成功,运行即可生效,不需要调用。而且不论是在哪个组件中定义的拦截器,都能拦截全局的跳转。
可以在拦截器中通过postcard.getExtra()获取到。
3. 处理跳转结果
这里是上面再LoginActivity中跳转到“/main/MainActivity”的代码,稍作修改。给navigation()方法添加两个参数:
第一个参数是上下文;第二个参数是NavigationCallback()接口对象。
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance().build("/main/MainActivity")
.navigation(LoginActivity.this, new NavigationCallback() {
@Override
public void onFound(Postcard postcard) {
//当路径目标成功被找到时
Log.d("NavigationCallback", "onFound: 找到了");
}
@Override
public void onLost(Postcard postcard) {
//当找不到路径目标的时候
Log.d("NavigationCallback", "onLost: 找不到目标路径");
}
@Override
public void onArrival(Postcard postcard) {
//当导航至目标路径时调用
Log.d("NavigationCallback", "onArrival: 跳转完成");
}
@Override
public void onInterrupt(Postcard postcard) {
//当导航被拦截器拦截时调用
Log.d("NavigationCallback", "onInterrupt: 跳转至MainActivity被中断");
}
});
}
});
我们可以通过这几个回调方法查看到跳转导航的信息并且在这里处理跳转失败或成功的结果。
4. ARouter类似startActivityForResult()的使用
在跳转页面:
navigation()的重载方法,参数一是上下文,参数二是请求码。和startActivityForResult()方法一样。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
...
//跳转到SecondActivity页面
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance().build("/main/SecondActivity")
.withObject("object", new Student("张三", 23))
.navigation(LoginActivity.this, 1);
}
});
}
//重写onActivityResult()方法。
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1 && requestCode == 2) {
Log.d("NavigationCallback", "onActivityResult: 跳转活动正常返回");
}
}
目标页面:
...其他代码省略
@Override
public void onBackPressed() {
//重写onBackPressed()方法 这个方法会在退出活动时调用
setResult(2, new Intent());
super.onBackPressed();
}
或者
@Override
public void finish() {
//或者重写这个方法也行,这个方法会在活动结束的时候调用
setResult(2, new Intent());
super.finish();
}
跳转完成后,退回到LoginActivity就会调用到onActivityResult()方法