【Android】组件化搭建

简介

模块:将一个程序按照其功能做拆分,分成相互独立的模块,以便用于每个模块只包含与其功能相关的内容,如登录模块、首页模块等

组件:指单一的功能组件,如登陆组件、视频组件等,每个组件都可以以单独的module开发,且可以单独抽出来作为SDK对外发布使用,一个模块中包含一个或多个组件

组件化的优势:

  • 加快编译速度:每个业务功能都是一个单独的功能,可独立编译运行,编译速度快

  • 解耦:将App分离成多个模块,每个模块都是一个组件

  • 提高开发效率:每个组件模块由单人负责,降低开发沟通成本,减少因代码不一产生的影响

  • 代码复用:类似引用第三方库,可以将组件剥离,在新项目微调或直接使用
    组件化要解决的问题:

  • 组件分层:如何将项目分成多个组件,组件间的依赖关系是什么样的

  • 组件单独运行和集成调试:组件如何独立运行和集成调试

  • 组件间通信:主项目与组件、组件和组件之间如何通信

组件分层

组件依赖关系是上层依赖下层,修改频率是上层高于下层

主工程(app)→业务组件层→功能组件层

​ ↓

​ lib_common

​ ↓

​ 基础组件层

  • 主工程(app):只依赖各组件,通过配置指定依赖哪些组件;除了一些全局配置和主Activity外,不包含任何业务代码,是应用的入口
  • 业务组件层:各组件之间无直接关联,通过路由进行通信;可直接依赖基础组件层,同时也能依赖公用的一些功能组件;业务组件可以在library和application之间切换,但最后打包时必须是library
  • 功能组件层:依赖基础组件层;对一些公用的业务功能进行封装与实现
  • common组件(lib_common):支撑业务组件、功能组件的基础;依赖基础组件层;业务组件、功能组件所需的基础能力只需依赖commmon组件即可获得
  • 基础组件层:封装公用的基础组件(并实现了路由);网络访问框架、图片加载框架等主流的第三方库;各种第三方SDK

组件化项目搭建

application插件:如果一个模块被声明为application,它就会成为一个apk文件,可以直接安装运行

library插件:被声明为library的模块,会成为aar文件,不可以单独运行

Gradle配置统一管理

创建一个config.gradle文件用于全局管理版本号、依赖项等,避免项目中不同模块间依赖版本不一致的问题。

java 复制代码
//在 Gradle 构建脚本中定义了一些 Android 项目的配置和依赖项版本信息。
ext {

    isDebug = false;    //当为true时代表调试模式,组件可以单独运行。如果是false代表编译打包

    /**
     compileSdkVersion:指定用于编译应用程序的 Android SDK 版本。
     minSdkVersion:指定应用程序可以运行的最低 Android 版本。
     targetSdkVersion:指定应用程序目标的 Android 版本。
     versionCode:用于标识应用程序版本的整数值。
     versionName:用于标识应用程序版本的字符串值。
     */

    android = [
            compileSdkVersion: 33,
            minSdkVersion    : 32,
            targetSdkVersion : 33,
            versionCode      : 1,
            versionName      : "1.0"
    ]

    /**
     * 这是每个模块的application地址
     */
    applicationId = [
            "app"  : "com.example.dome",
            "login": "com.example.module.login",
            "net": "com.example.module.mylibBase"
    ]

    //定义了一些常见的 Android 库的依赖项,包括 AppCompat 库、Material Design 库和 ConstraintLayout 库。
    library = [
            "appcompat"       : "androidx.appcompat:appcompat:1.6.1",
            "material"        : "com.google.android.material:material:1.5.0",
            "constraintlayout": "androidx.constraintlayout:constraintlayout:2.1.4"
    ]

    //第三方库单独放置
    libGson = "com.google.code.gson:gson:2.8.6"
    libARouter = "com.alibaba:arouter-api:1.5.2"
    libARouterCompiler = "com.alibaba:arouter-compiler:1.5.2"
}

在project的build.gradle中引入config.gradle文件

java 复制代码
plugins {
alias(libs.plugins.android.application) apply false
}
apply from:"config.gradle"

isDebug=true加入到gradle.properties

业务组件创建及配置build.gradle文件

创建module时有两个选项

  • Phone&Tablet:创建可以单独运行的模块,即业务模块,是一个单独的application,可以在library和application之间切换
  • Android Library:创建一个Library,一般用于创建基础层,不需要单独运行

可以创建一个包,将组件全部放入里面管理,在创建时在Package name中再加一层module,防止在导包时出现冲突

修改buildle.gradle文件

java 复制代码
/*plugins {
    //将静态启动模式修改为动态启动模式
   id 'com.android.application'
}*/
//动态配置插件,注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}
//简化手写,rootProject.ext的次数,提高性能
def cfg=rootProject.ext
android {
    //命名空间
    namespace cfg.applicationId.login
    compileSdkVersion cfg.android.compileSdkVersion

    defaultConfig {
       //在defaultConfig中判断是否需要应用标识符,如果是调试模式则需要一个标识符启动这个单独的模块
        if(cfg.isDebug){
            applicationId cfg.applicationId.login
        }
        //版本号
        minSdkVersion cfg.android.minSdkVersion
        targetSdkVersion cfg.android.targetSdkVersion
        versionCode cfg.android.versionCode
        versionName cfg.android.versionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
    //选择合适的manifest,指定Android项目的源极配置,用于指定在构建Android应用时要使用的源代码、资源和清单文件的位置
    sourceSets {
        main {
            //在debug模式下需要详细的AndroidManifest去启动一个活动
            if (cfg.isDebug) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'

                //注意我们还需在正式环境下屏蔽Debug包下的所有文件
                java{
                    exclude "**/debug/**"
                }
            }
        }
    }
}

dependencies {
    //导入基础模块
    implementation project(':modulesBase:libBase')
    implementation libs.appcompat
    implementation libs.material
    implementation libs.activity
    implementation libs.constraintlayout
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core

因为在debug模式下需要详细的AndroidManifest去启动一个活动,所以在该model的main包下新建一个debug包,并将自动生产的AndroidManifest拷贝到debug包下

在打包模式下,就不需要其他的活动清单里,索引将自动生成的AndroidManifest修改为以下模式

xml 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application>
        <activity
            android:name=".MainActivity"
            android:exported="true">
        </activity>
    </application>

</manifest>

sourceSets 定义了一个 main 源集。

对于 main 源集,使用了一个条件语句(if (cfg.isDebug))来决定使用哪个清单文件。

"/debug/ ": 这是一个 Ant 风格的通配符。** 表示匹配任意数量的目录,因此 /debug/ 表示匹配任何包含 "debug" 目录的路径。

基础组件创建及配置
java 复制代码
plugins {
    alias(libs.plugins.android.library)
}
def cfg = rootProject.ext
android {
    namespace cfg.applicationId.net
    compileSdkVersion cfg.android.compileSdkVersion

    defaultConfig {
        //版本号
        minSdkVersion cfg.android.minSdkVersion
        targetSdkVersion cfg.android.targetSdkVersion

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
}

dependencies {
     api cfg.library.appcompat
    api cfg.library.material
    api cfg.library.constraintlayout
    api cfg.libGson
    //也可以使用这样的方式一行代码完成:,相当于一个for循环遍历library
    //library.each{k,v->implementation v}
    implementation libs.appcompat
    implementation libs.material
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core
}

在 Gradle 构建中,rootProject.ext是指向项目根目录的引用,在dependencies引入在最外部定义的公用依赖。

这里采用Api的方式引用,这样做的好处是依赖可以传递。

比如A模块使用api导入了x依赖,B模块此时只需要导入A模块就可以使用x依赖。

app层配置
java 复制代码
plugins {
    alias(libs.plugins.android.application)
}
def cfg=rootProject.ext
android {
    namespace cfg.applicationId.app

    compileSdkVersion cfg.android.compileSdkVersion

    defaultConfig {
        applicationId cfg.applicationId.app

        minSdkVersion cfg.android.minSdkVersion
        targetSdkVersion cfg.android.targetSdkVersion
        versionCode cfg.android.versionCode
        versionName cfg.android.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_11
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation project(':modulesBase:libBase')
    if (!isDebug.toBoolean()){
        implementation project(":modulesCore:live")
        implementation project(":modulesCore:login")
    }
    implementation libs.appcompat
    implementation libs.material
    implementation libs.constraintlayout
    implementation libs.navigation.fragment
    implementation libs.navigation.ui
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core
}

主要是在dependencies中导入基础模块的依赖,并且还需要导入其他组件的依赖

在这里需要判断是否为debug模式,是的化就不用导入这些组件

组件间跳转

在组件化开发中,不允许组件层模块横向依赖,所以不能直接访问彼此的类,需要通过路由的方式跳转。

自定义路由

在base包下新建一个libRouter模块

新建一个Java文件实现路由功能

Router要是单例模式,才能保证每次共用的是同一个对象共享相同的数据。

java 复制代码
public class Router {
    private final static class Holder{
        static Router INSTANCE=new Router();
    }
    public static Router getInstance(){
        return Holder.INSTANCE;
    }
}

Router的作用是管理不同的类,可以统一协调和使用这些类,因此我们用Map存储这些类和其包名路径的对应关系。

groupMap(key,value)

key:GroupName(类的包名)

value:routerMap(key , value)

​ ↓ ↓

Path(路径名,用于寻找具体的类)Class<?>cla(用于存储指定的类)

java 复制代码
static Map<String,Map<String,Class<?>>> groupMap=new HashMap<>();
    Map<String,Class<?>> routerMap=new HashMap<>();

之后设计这些类的注册方法,需要一个Path和Class

java 复制代码
public void register(String path,Class<?> cla){
        //解析包名
        String[] strArray=path.split("/");
        if(strArray.length>2){
            String groupName=strArray[1];
            Map<String,Class<?>> routerMap=null;
            if(groupMap.containsKey(groupName)){
                routerMap=groupMap.get(groupName);
            }else{
                routerMap=new HashMap<>();
                groupMap.put(groupName,routerMap);
            }
            routerMap.put(path, cla);
        }
    }

实现跳转

java 复制代码
    /**
     * @param activity 当前的Activity
     * @param path     需要启动的路径
     */
    public void startActivity(Activity activity, String path) {
        String[] strArray = path.split("/");

        if (strArray.length > 2) {
            String groupName = strArray[1];
            String routeName = path;

            Map<String, Class<?>> group = null;

            if (groupMap.containsKey(groupName)) {
                group = groupMap.get(groupName);

                if (group != null && group.containsKey(routeName)) {
                    Class<?> clz = group.get(routeName);
                    activity.startActivity(new Intent(activity, clz));
                }

            }
        }
    }

之后在App下的Activity实现注册,就能在其他模块调用startActivity实现跳转

java 复制代码
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Router.getInstance().register("/live/LiveActivity", LiveActivity.class);
        Router.getInstance().register("/login/LoginActivity", LoginActivity.class);
    }
}
ARouter

ARouter是一个可以用于组件间跳转的第三方库,支持模块间的路由、通信和解耦

添加依赖

先在统一的config.gradle中添加版本等信息

java 复制代码
ext{
    ...
    libARouter= 'com.alibaba:arouter-api:1.5.2'
    libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}

之后配置libBase

java 复制代码
dependencies {
    ...
    api cfg.libARouter
  ...
}

再配置其他组件(如login)

java 复制代码
android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
                //如果项目内有多个annotationProcessor,则修改为以下设置
                //arguments += [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}
 
dependencies {
    //arouter-compiler的注解依赖需要所有使用 ARouter 的 module 都添加依赖
    annotationProcessor cfg.libARouterCompiler
    ...
}

在项目最外面的gradle.properties下加入这段代码:

java 复制代码
android.useAndroidX=true
android.enableJetifier=true
添加注解

在要跳转的Activity之前加上注解

java 复制代码
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path="/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        Log.d("login","login");
    }
}
初始化SDK
java 复制代码
public class App extends Application {
    public void onCreate(){
        super.onCreate();
        if(isDebug()){//这两行代码必须写在init之前,否则这些配置无效
            ARouter.openLog();//打印日志
            ARouter.openDebug();//开启调试模式
        }
        ARouter.init(this);//对ARouter进行初始化
    }
    private boolean isDebug(){
        return BuildConfig.DEBUG;
    }
    public final class  BuildConfig{
        public static final  boolean DEBUG=false;
        public static final String LIBRARY_PACKAGE_ANME="com.alibaba.android.arouter";
        public static final String BUILD_TYPE="release";
        public BuildConfig(){

        }
    }
}

BuildConfig可以将gradle的数据映射到Java类里面,实现在java代码中使用gradle数据

并且在AndroidManifest.xml中注册这个application

发起路由操作

之后就可以在类中使用跳转

java 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        ARouter.getInstance().build("/login/LoginActivity").navigation();
    }
}
实现不同组件间的数据传递

ARouter在跳转时可以携带参数

java 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        ARouter.getInstance().build("/login/LoginActivity")
                .withString("key1","app")
                .withBoolean("key2",false)
                .withSerializable("key3",new User("mm",5))
                .navigation();
    }
}

其中User是在基础组件中定义的一个实现了Serializable接口的类

java 复制代码
public class User implements Serializable {
    private String name;
    private int cnt;
    public User(String name,int cnt){
        this.cnt=cnt;
        this.name=name;
    }

    public String getName() {
        return name;
    }
}

之后就可以在跳转到的类中接收参数,注意接收的参数必须使用@Autowired 注解

java 复制代码
@Route(path="/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
    @Autowired
    public String key1;

    @Autowired
    public boolean key2;
    @Autowired
    public User key3;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        Log.d("login","login");
        ARouter.getInstance().inject(this);
        Log.e("TAG","key1"+key1);
        Log.e("TAG","key2"+key2);
        Log.e("TAG","key3"+key3.getName());
    }
}
相关推荐
工程师老罗24 分钟前
如何在Android工程中配置NDK版本
android
崔庆才丨静觅28 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
曹牧1 小时前
Spring Boot:如何测试Java Controller中的POST请求?
java·开发语言
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
爬山算法2 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
kfyty7252 小时前
集成 spring-ai 2.x 实践中遇到的一些问题及解决方案
java·人工智能·spring-ai
猫头虎2 小时前
如何排查并解决项目启动时报错Error encountered while processing: java.io.IOException: closed 的问题
java·开发语言·jvm·spring boot·python·开源·maven