背景
在项目的开发过程中,随着参与人员的增多以及功能的增加,如果没有使用合理的开发架构,代码会越来越臃肿,耦合越来越严重。为了解决这个问题,组件化应运而生。
组件化的优势
组件化可以解决以下问题:
- 可以单独调试和运行单独的业务模块,这样开发人员可以专注于自己负责的业务模块的开发,提高开发的效率。
- 可以降低代码的耦合度,不会导致牵一发而动全身,新加入的开发人员也更容易上手项目。各业务模块之间没有依赖关系,也会减少提交代码冲突的问题。
- 可以灵活配置依赖的模块,让基础模块更好地得到重用。
- 可以让开发人员的分工更加明确,别的模块的开发人员不能轻易修改你负责的模块的代码。
示例
下面我就用一个简单示例来解释如何实现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即表示这几个业务模块可以独立调试和运行:
ini
# Enables the module to run alone
moduleRunAlone = true
然后在各业务模块的build.gradle中读取moduleRunAlone的值:
css
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:
java
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
<?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
<?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模块改造成了一个可以独立调试运行的组件。
页面跳转
组件化的核心是解耦,组件间不能有依赖,那么如何实现组件间的页面跳转呢?
比如这里我想从home组件下的HomeFragment跳转到download组件下的DownloadActivity,由于home组件不依赖download组件,直接跳转是行不通的,这里我们使用ARouter来进行跳转。
ARouter是阿里开发的,是一个帮助安卓App进行组件化改造的路由框架。
前面提到,各个业务模块都依赖base模块,我们在base模块的build.gradle中使用api来添加ARouter依赖:
arduino
api "com.alibaba:arouter-api:1.3.1"
这样在各个业务模块中可以传递依赖ARouter库。
需要注意的是,arouter-compiler的annotationProcessor依赖需要在所有使用到ARouter的模块中单独添加,否则无法生成索引文件,会导致无法跳转成功,并且还要在对应模块的build.gradle中添加特定配置。
例如,在home模块的build.gradle添加的配置如下:
javascript
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对应:
java
@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.getInstance().build("/download/DownloadActivity").navigation()
:
java
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.getInstance()
.build("/download/DownloadActivity")
.navigation();
}
});
...
}
}
此时还无法跳转成功,要在app模块的Application中初始化ARouter:
java
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配置如下:
java
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,代码如下:
java
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模块:
java
//在home模块和download模块的build.grale中添加
dependencies {
implementation project(":export_download")
}
在export_download模块中添加IDownloadService继承IProvider:
java
public interface IDownloadService extends IProvider {
int getDownloadProgress(String name);
}
download模块中是对这个接口的具体实现:
java
@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来获取其中某个应用的下载进度,这样就实现了组件间的通信:
java
@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插件仓:
rust
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中添加:
arduino
//添加使用插件applifecycle
apply plugin: 'com.hm.plugin.lifecycle'
- 在base模块中添加依赖:
arduino
//base模块的build.grale
dependencies {
api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
- 在业务组件中添加依赖:
arduino
dependencies {
annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
}
- Sync Project后,在业务组件中新建类实现IApplicationLifecycleCallbacks接口并添加
@AppLifecycle
注解,用于接收Application生命周期的分发,如download模块新建类如下:
java
@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中触发生命周期的分发:
java
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...