组件化

背景

在项目的开发过程中,随着参与人员的增多以及功能的增加,如果没有使用合理的开发架构,代码会越来越臃肿,耦合越来越严重。为了解决这个问题,组件化应运而生。

组件化的优势

组件化可以解决以下问题:

  1. 可以单独调试和运行单独的业务模块,这样开发人员可以专注于自己负责的业务模块的开发,提高开发的效率。
  2. 可以降低代码的耦合度,不会导致牵一发而动全身,新加入的开发人员也更容易上手项目。各业务模块之间没有依赖关系,也会减少提交代码冲突的问题。
  3. 可以灵活配置依赖的模块,让基础模块更好地得到重用。
  4. 可以让开发人员的分工更加明确,别的模块的开发人员不能轻易修改你负责的模块的代码。

示例

下面我就用一个简单示例来解释如何实现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的生命周期会主动分发给业务组件,具体使用方法如下:

  1. 在项目的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' }
    }
}
  1. 在app的build.grale中添加:
arduino 复制代码
//添加使用插件applifecycle
apply plugin: 'com.hm.plugin.lifecycle'
  1. 在base模块中添加依赖:
arduino 复制代码
//base模块的build.grale
dependencies {
    api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
}
  1. 在业务组件中添加依赖:
arduino 复制代码
dependencies { 
    annotationProcessor 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
}
  1. 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()方法调用的优先顺序。

  1. 在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...

相关推荐
NoneCoder1 天前
CSS系列(26)-- 动画性能优化详解
前端·css·性能优化
苏三说技术1 天前
Redis 性能优化的18招
数据库·redis·性能优化
程序猿会指北1 天前
【鸿蒙(HarmonyOS)性能优化指南】内存分析器Allocation Profiler
性能优化·移动开发·harmonyos·openharmony·arkui·组件化·鸿蒙开发
程序猿会指北1 天前
【鸿蒙(HarmonyOS)性能优化指南】启动分析工具Launch Profiler
c++·性能优化·harmonyos·openharmony·arkui·启动优化·鸿蒙开发
彭亚川Allen2 天前
优化了2年的性能,没想到最后被数据库连接池坑了一把
数据库·后端·性能优化
MClink2 天前
Go怎么做性能优化工具篇之pprof
开发语言·性能优化·golang
京东零售技术2 天前
Taro小程序开发性能优化实践
性能优化·taro
理想不理想v2 天前
wepack如何进行性能优化
性能优化
fantasy_arch3 天前
CPU性能优化-磁盘空间和解析时间
网络·性能优化
码农老起3 天前
企业如何通过TDSQL实现高效数据库迁移与性能优化
数据库·性能优化