【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());
    }
}
相关推荐
野生技术架构师1 小时前
Java 经典面试题汇总:多线程 +spring+JVM 调优 + 分布式 +redis+ 算法
java·jvm·spring
魂梦翩跹如雨1 小时前
P8723 [蓝桥杯 2020 省 AB3] 乘法表——Java解答
java·蓝桥杯
小小8程序员1 小时前
Android 性能调优与故障排查:ADB 诊断命令终极指南
android·adb
原野-1 小时前
PHP女程序猿学习Java的Day-5
java·开发语言·学习
零基础的修炼1 小时前
[项目]基于正倒排索引的Boost搜索引擎---服务和前端模块
前端
重生之我是Java开发战士1 小时前
【Java SE】TCP/IP协议栈:从分层架构到核心机制
java·tcp/ip·架构
CoderYanger1 小时前
递归、搜索与回溯-综合练习:22.优美的排列
java·算法·leetcode·深度优先·1024程序员节
想做后端的前端1 小时前
Lua基本数据类型
java·junit·lua
一勺菠萝丶1 小时前
Vue组件状态同步问题:为什么修改了DOM值,提交时还是默认值?
前端·javascript·vue.js