背景
在项目的开发过程中,随着参与人员的增多以及功能的增加,如果没有使用合理的开发架构,代码会越来越臃肿,耦合越来越严重。为了解决这个问题,组件化应运而生。
组件化的优势
组件化可以解决以下问题:
- 可以单独调试和运行单独的业务模块,这样开发人员可以专注于自己负责的业务模块的开发,提高开发的效率。
- 可以降低代码的耦合度,不会导致牵一发而动全身,新加入的开发人员也更容易上手项目。各业务模块之间没有依赖关系,也会减少提交代码冲突的问题。
- 可以灵活配置依赖的模块,让基础模块更好地得到重用。
- 可以让开发人员的分工更加明确,别的模块的开发人员不能轻易修改你负责的模块的代码。
示例
下面我就用一个简单示例来解释如何实现App的组件化。
现在有一个应用市场 App ,该 App 包含5个模块:
app模块
:App 的入口;home模块
:App 的首页,主要用于展示首页推荐下载的应用;game模块
:主要用于展示推荐下载的游戏应用;download模块
:主要用于展示下载项的列表和下载项的控制;base模块
:提供 BaseActivity 、 BaseFragment 、图片加载、网络请求等基础能力,各个业务模块都需要依赖它;
app 模块需要依赖 home 模块和 game 模块,同时,home 模块和 game 模块需要显示应用的下载进度,需要依赖 download 模块,这样,没有实现组件化之前,各模块之间的依赖关系图如下所示:
组件独立调试
实现组件化,首先我们要让 home 模块、 game 模块、 download 模块这几个业务模块可以单独调试和运行,如何实现呢?
我们可以在工程的 gradle.properties 中配置一个常量值 moduleRunAlone
,值为 true 即表示这几个业务模块可以独立调试和运行:
# Enables the module to run alone
moduleRunAlone = true
然后在各业务模块的 build.gradle 中读取 moduleRunAlone 的值:
if(moduleRunAlone.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
android {
sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (moduleRunAlone.toBoolean()) {
manifest.srcFile 'src/main/moduleManifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
Android Studio 开发 Android 项目使用的是 Gradle 来构建的, Gradle 提供了 3 种插件,这里通过读取 moduleRunAlone 的值来配置 module 的类型:
application 插件
:apply plugin: 'com.android.application'library 插件
:apply plugin: 'com.android.library'
在对应的业务模块中,新建 moduleManifest 目录,添加模块单独调试对应的 AndroidManifest.xml 文件,如下图所示:
然后在 build.gradle 中根据 moduleRunAlone 的值来配置不同的 AndroidManifest.xml 文件的路径。
在 home 模块中,集成调试的入口页面是 HomeFragment ,单独调试需要有入口 Activity ,这里我们新建 HomeActivity 作为入口 Activity :
public class HomeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
FragmentManager manager = this.getSupportFragmentManager();
FragmentTransaction ft = manager.beginTransaction();
ft.add(R.id.container_fragment, new HomeFragment());
ft.commit();
}
}
activity_home.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<FrameLayout
android:id="@+id/container_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
这样单独调试的时候就可以使用 HomeActivity 来启动 HomeFragment了。
单独调试的 AndroidManifest.xml 代码如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.home">
<application
android:name=".HomeApp"
android:allowBackup="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity
android:name=".HomeActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这样配置完成后,把 moduleRunAlone 的值改为 true ,然后 Sync Project , home 模块就可以单独运行了,这样就把 home 模块改造成了一个可以独立调试运行的组件。
页面跳转
组件化的核心是解耦,组件间不能有依赖,那么如何实现组件间的页面跳转呢?
比如点击 HomeFragment 中的某个应用后,我需要跳转到 download 组件下的 DownloadActivity 来下载该应用,由于 home 组件不依赖 download 组件,直接跳转是行不通的,这里我们使用 ARouter 来进行跳转。
ARouter 是阿里开发的,是一个帮助安卓 App 进行组件化改造的路由框架。
前面提到,各个业务模块都依赖 base 模块,我们在 base 模块的 build.gradle 中使用 api 来添加 ARouter 依赖:
api "com.alibaba:arouter-api:1.5.2"
这样在各个业务模块中可以传递依赖ARouter库。
需要注意的是, arouter-compiler 的 annotationProcessor 依赖需要在所有使用到 ARouter 的模块中单独添加,否则无法生成索引文件,会导致无法跳转成功,并且还要在对应模块的 build.gradle 中添加特定配置。
例如,在 home 模块的 build.gradle 添加的配置如下:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
}
}
}
dependencies {
//使用ARouter的模块需添加这个依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
在DownloadActivity上添加注解 @Route(path = "/download/DownloadActivity")
,路径注意至少需要有两级,前面的 download 对应模块名,与 build.gradle 中配置的 moduleName 对应:
@Route(path = "/download/DownloadActivity")
public class DownloadActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_download);
...
}
}
从 HomeFragment 跳转到 DownloadActivity 需调用使用 ARouter 进行跳转:
public class HomeFragment extends Fragment {
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_home, container, false);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
...
Button btnDownload = view.findViewById(R.id.btn_download);
btnDownload.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 使用 ARouter 进行跳转
ARouter.getInstance()
.build("/download/DownloadActivity")
.navigation();
}
});
...
}
}
此时还无法跳转成功,要在 app 模块的 Application 中初始化 ARouter :
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
if(BuildConfig.DEBUG){
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(this);
...
}
}
由于组件间不能有依赖, home 组件不能依赖 download 组件,但是 home 组件和 download 组件都需要参与编译,否则不能生成路由,可以让 app 模块依赖 home 模块和 download 模块来完成编译。这里我们使用 Gradle 3.0 新提供的依赖方式 runtimeOnly ,通过 runtimeOnly 方式依赖时,依赖项仅在运行时可见,编译期间依赖项的代码是完全隔离的,则 app 模块的 build.gradle 配置如下:
dependencies {
runtimeOnly project(':home')
runtimeOnly project(":download")
runtimeOnly project(":game")
implementation project(":base")
}
这样在编译期, app 模块与 home 模块、 download 模块、 game 模块都是互相隔离的,那么在 app 模块中的 HomeActivity 中和怎么拿到 home 模块中的 HomeFragment 和 game 模块中的 GameFragment ,实现这两个 Fragment 的切换呢?答案还是使用 ARouter ,代码如下:
public class HomeActivity extends AppCompatActivity implements RadioGroup.OnCheckedChangeListener {
Fragment mHomeTabFragment;
Fragment mGameTabFragment;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
RadioGroup rg_tab = findViewById(R.id.tabs);
rg_tab.setOnCheckedChangeListener(this);
initDefaultFragment();
}
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
hideAllFragments();
switch (checkedId) {
case R.id.main_tab_home:
replaceFragment(0);
break;
case R.id.main_tab_game:
replaceFragment(1);
break;
}
}
private void initDefaultFragment(){
if (mHomeTabFragment == null) {
mHomeTabFragment = (Fragment) ARouter.getInstance().build("/home/homeFragment").navigation();
}
FragmentTransaction transaction = this.getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.container_fragment, mHomeTabFragment, "home");
transaction.commit();
}
private void replaceFragment(int position) {
FragmentTransaction transaction = this.getSupportFragmentManager().beginTransaction();
switch (position) {
case 0:
if (mHomeTabFragment == null) {
mHomeTabFragment = (Fragment) ARouter.getInstance().build("/home/homeFragment").navigation();
transaction.add(R.id.container_fragment, mHomeTabFragment, "home");
}
transaction.show(mHomeTabFragment);
break;
case 1:
if (mGameTabFragment == null) {
mGameTabFragment = (Fragment) ARouter.getInstance().build("/game/gameFragment").navigation();
transaction.add(R.id.container_fragment, mGameTabFragment, "game");
}
transaction.show(mGameTabFragment);
break;
}
transaction.commitAllowingStateLoss();
}
private void hideAllFragments() {
FragmentTransaction transaction = this.getSupportFragmentManager().beginTransaction();
if (mHomeTabFragment != null) {
transaction.hide(mHomeTabFragment);
}
if (mGameTabFragment != null) {
transaction.hide(mGameTabFragment);
}
transaction.commitAllowingStateLoss();
}
}
组件间通信
有时候组件之间没有办法做到完全隔离,比如,home 组件的 HomeFragment 中需要显示某一个应用的下载进度, home 组件与 download 组件之间没有依赖关系,那么它们之间如何通信呢?答案是从 download 模块中拆分出一个暴露的模块:export_download,里面提供接口给 home 组件使用。
平时开发中我们常用接口进行解耦,不需要关心接口的具体实现,避免接口调用与业务逻辑实现之间紧密关联,这里组件间的通信也是采用相同的思路。
我们新建一个 library 模块:export_download , home 模块和 download 模块都依赖 export_download 模块:
// 在 home 模块和 download 模块的 build.grale 中添加
dependencies {
implementation project(":export_download")
}
在 export_download 模块中添加 IDownloadService 继承 IProvider :
public interface IDownloadService extends IProvider {
int getDownloadProgress(String name);
}
download 模块中有对这个接口的具体实现:
@Route(path = "/download/service")
public class DownloadServiceImpl implements IDownloadService {
@Override
public int getDownloadProgress(String name) {
Map<String, Integer> progressMap = new HashMap<>();
progressMap.put("tiktok", 47);
progressMap.put("weibo", 89);
return progressMap.get(name);
}
@Override
public void init(Context context) {
}
}
这样,在集成调试模式下,home 模块中的 HomeFragment 可以通过 ARouter 来获取其中某个应用的下载进度,这样就实现了组件间的通信:
@Route(path = "/home/homeFragment")
public class HomeFragment extends Fragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
TextView tv1 = view.findViewById(R.id.tv1);
IDownloadService downloadService = (IDownloadService) ARouter.getInstance().build("/download/service").navigation();
tv1.setText("Weibo:" + downloadService.getDownloadProgress("weibo") + "%");
}
}
Applicaiton 的生命周期分发
我们通常会在 app 模块的 Application 的 onCreate() 方法中做一些初始化操作,而业务组件有时也需要执行一些初始化操作,你可能会说,直接一股脑都在 app 模块的 Application 中初始化不就行了吗,但是这样做会带来问题,因为我们希望 app 模块与业务组件之间的代码是隔离,并且我们希望组件内部的任务要在组件内部初始化。
这里使用 AppLifeCycle插件 ,它可以让业务组件无侵入地获取 Application 生命周期,它专门用在组件化开发中,使用后 app 模块的 Application 的生命周期会主动分发给业务组件,具体使用方法如下:
- 在项目的 build.gradle 中添加 applifecycle 插件仓:
buildscript {
repositories {
google()
jcenter()
// applifecycle 插件仓也是 jitpack
maven { url 'https://jitpack.io' }
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
// 加载插件 applifecycle
classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3' }
}
allprojects {
repositories {
google()
jcenter()
maven { url 'https://jitpack.io' }
}
}
- 在 app 的 build.grale 中添加:
// 添加使用插件 applifecycle
apply plugin: 'com.hm.plugin.lifecycle'
- 在 base 模块中添加依赖:
// base 模块的 build.grale
dependencies {
api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
- 在业务组件中添加依赖:
dependencies {
annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
}
- Sync Project后,在业务组件中新建类实现 IApplicationLifecycleCallbacks 接口并添加
@AppLifecycle
注解,用于接收 Application 生命周期的分发,如 download 模块新建类如下:
@AppLifecycle
public class HomeAppLifecycleCallbacks implements IApplicationLifecycleCallbacks {
@Override
public int getPriority() {
return 0;
}
@Override
public void onCreate(Context context) {
Log.i("HomeApp", "onCreate");
//可在此处做模块内任务的初始化
}
@Override
public void onTerminate() {
}
@Override
public void onLowMemory() {
}
@Override
public void onTrimMemory(int level) {
}
}
实现的方法有 onCreate() 、 onTerminate() 、 onLowMemory() 、 onTrimMemory() 、 getPriority() ,其中最重要的是 onCreate() 方法,可在此处做模块内任务的初始化。还可以通过 getPriority() 方法设置多个模块中 onCreate() 方法调用的优先顺序。
- 在 app 的 Application 中触发生命周期的分发:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
Log.i("App", "onCreate");
...
ApplicationLifecycleManager.init();
ApplicationLifecycleManager.onCreate(this);
}
@Override
public void onTerminate() {
super.onTerminate();
ApplicationLifecycleManager.onTerminate();
}
@Override
public void onLowMemory() {
super.onLowMemory();
ApplicationLifecycleManager.onLowMemory();
}
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
ApplicationLifecycleManager.onTrimMemory(level);
}
}
App 运行后通过打印可以看到 HomeAppLifecycleCallbacks 的 onCreate() 方法也会随之执行。
HomeAppLifecycleCallbacks 用于集成调试中,模块独立调试可以自己新建一个 HomeApp 来执行独立调试运行需要的初始化操作。
实现组件化后,各模块之间的依赖关系图如下图所示:
app 模块依赖所有的业务模块,所有涉及到下载的模块都需要依赖 export_download 模块,还有,所有模块都需要依赖 base 模块。这里所有模块间的依赖关系都不使用 api 传递依赖,这样做是为了后续依赖的灵活配置。后续可以把各个模块编译生成的 aar 包都上传到 maven 仓库,这样可以像依赖普通插件一样配置依赖的模块,这样做还有一个好处,别的模块的开发人员没有办法轻易修改你负责的模块的代码了。
Demo地址:github.com/EnzoXRay/My…