【Android】保姆级教程一步步教你完成组件化搭建过程

组件化基础

一、什么是组件化?

在项目开发中,一般将公共的代码提取出来制作基础组件库Base module,将某些单独的功能封装到Libaray module中,更具业务划分module。

但是随着项目更迭,功能越来越多代码结构越来越复杂。

image-20231121163104227

这时候就出现了组件化模块化插件化这些规则。

多module划分业务和基础功能,这个概念将作为组件化的基础

**组件:**指的是单一的功能组件,如:视频组件(VideoSDK)、支付组件(PaySdk)

模块 : 指的是独立的业务模块,如直播模块(LiveModule)

二、组件化架构介绍

image-20231121164553826

  1. 基础层包含一些基础库和对基础库的封装,包括图片加载、网络加载、数据存储等
  2. 组件层包含简单的业务、比如登录注册、支付、直播等等
  3. 应用层用于统筹全部组件

聚合和解耦是项目架构的基础

三、组件化项目的搭建

1、了解组件化项目框架

在这里介绍一下项目搭建的两种插件

  • application插件:如果一个模块被声明为aoolication,那么它会成为一个apk文件,是可以直接安装运行的项目。
  • library插件:被声明为library的模块,它会成为一个aar文件,不可以单独运行。

下面是一个构建的模块,可以先了解一下大致框架,后面会一步步介绍。

image-20231122224232084

这里创建了两个包,一个是modulesBase这里保存了基础的公用代码和依赖。另一个是modulesCore这个写的是主要的业务模块。

2、抽离共用的build.gradle版本数据

我们在APP开发过程中肯定会涉及到不同的手机版本信息,系统信息等等。如果每个模块的信息都是独立的,那么一旦出现版本问题需要修改的情况,将会变得非常麻烦。

这时就需要使用config.gradle统一管理项目的依赖库

2.1 项目下新建一个Gradle文件

用于存储项目基本信息,在这里面,我们写上所有依赖库,以及项目中sdk等等的版本号,然后直接让buil.gradle去apply它,之后有什么版本的修改升级就可以直接在这里改。

image-20231123112453342

2.2 定义一个ext扩展区域

用于定义额外的属性和变量,以便在整个构建脚本中共享和重复使用。

//在 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",
            "live" : "com.example.module.live",
            "net"  : "com.example.module.net",
            "login": "com.example.module.login",
            "router": "com.example.module.librouter"
    ]

    //定义了一些常见的 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"
}

相信大家对这些东西都不陌生了,第三方库后面会说到怎么用,这里可以先写出来。

2.3 在build.gradle中导入config.gradle

image-20231123112936348

3、创建基础层

3.1 建立一个libBase模块

image-20231123113405255

就是这里的modulesBase模块,接下来我们尝试创建一个libBase。

image-20231123113820424

注意:

在这个地方我们一般有两个选项

第一个选项是创建可以单独运行的模块,也就是业务模块,是一个application。

第二个是创建一个Library,一般是用于创建基础层,不需要单独运行。

image-20231123114605354

在这里我们创建Module name时多加了一层,这样可以用一个专门的包去管理所有的模块。

就是这样的效果:

image-20231123113405255

创建Package name时,也多加了一个module,这个也是为了放在冲突,因为默认的这种创建方式的包名和主App的命名方式一致,导包的过程就会出现冲突。当然这个module可以根据自己的情况命名。

image-20231123114842153

3.2 修改libbase的builde.gradle

我们创建的library模块会自动设置启动模式为apply plugin: 'com.android.library',这样基础模块就会被打包成一个arr文件,配合其他的业务模块application使用。

apply plugin: 'com.android.library'

//简化手写,rootProject.ext的次数,提高性能
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"
    }

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

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


    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

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

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

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

4、创建组件层

4.1 建立一个login组件模块

image-20231123122806475

image-20231123122940629

这里的和刚才libbase是一样的命名思想。

4.2 修改login的builde.gradle

首先我们将login的静态启动模式修改为动态启动模式

plugins {
    
    
    id 'com.android.application'
}

还记得我们在ext定义的isDebug吗?在这里我们就需要用到它了。

image-20231123123413774

修改为:

//判断属于打包模式还是调试模式
if (rootProject.ext.isDebug) {
    
    
    apply plugin: 'com.android.application'
} else {
    
    
    apply plugin: 'com.android.library'
}

这样做的好处是在调试的模式下就是单独的application进行运行,在打包的模式就会变成一个arr文件,作为组件配合主App使用。

接下来还是同样的操作,将涉及到共用部分的写成ext的形式。

//判断属于打包模式还是调试模式
if (rootProject.ext.isDebug) {
    
    
    apply plugin: 'com.android.application'
} else {
    
    
    apply plugin: 'com.android.library'
}


def cfg = rootProject.ext

android {
    
    
    namespace cfg.applicationId.login

    compileSdkVersion cfg.android.compileSdkVersion

    defaultConfig {
    
    
        
        //判断如果是调试模式则需要当做一个application启动,则需要一个y
        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_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    
    //选择合适的manifest
        sourceSets {
    
    
        main {
    
    
            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')

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

接下来是详细解释:

defaultConfig中判断是否需要应用标识符,如果是调试模式则需要一个标识符启动这个单独的login模块。

if (cfg.isDebug) {
    
    
    applicationId cfg.applicationId.login
}

接下来在android中添加sourceSets用于指定Android项目的源集配置,这特定的配置块用于指定在构建 Android 应用时要使用的源代码、资源和清单文件的位置。

在debug模式下需要有详细的AndroidManifest去启动一个活动,新建一个debug包,我们将自动生产的AndroidManifest拷贝到debug包下

image-20231123125548481

image-20231123125705534

但是在打包模式下,就不需要其他的的活动清单了,我们将原来自动生成的AndroidManifest修改为以下样式:

image-20231123125751190

  • sourceSets 定义了一个 main 源集。
  • 对于 main 源集,使用了一个条件语句(if (cfg.isDebug))来决定使用哪个清单文件。
  • "**/debug/**": 这是一个 Ant 风格的通配符** 表示匹配任意数量的目录,因此 **/debug/** 表示匹配任何包含 “debug” 目录的路径。
    sourceSets {
    
    
        main {
    
    
            if (cfg.isDebug) {
    
    
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
    
    
                manifest.srcFile 'src/main/AndroidManifest.xml'
                                   //注意我们还需在正式环境下屏蔽Debug包下的所有文件
                java{
    
    
                    exclude "**/debug/**"
                }
            }
        }
    }

然后在dependencies中导入了刚才的libBase模块,此时login就具有了libBase所存在的依赖。

implementation project(':modulesBase:libBase')

5、搭建App层

5.1 修改App的build.gradle

基本和刚才login组件的修改方法相似,plugins不用改动,因为App层不会拿来当作library。

plugins {
    
    
    id 'com.android.application'
}

需要注意的就是dependencies。同样的app也需要导入基础模块的依赖,并且app是关联其他的多个组件,这里还需要导入其他组件例如:login

因此在这里也需要判断是否是debug模式,如果是debug模式就不需要导入这些组件。

implementation project(':modulesBase:libBase')

//如果不是Debug的情况下则导入各个组件的模块
    if (!cfg.isDebug){
    
    
        implementation project(":modulesCore:live")
        implementation project(":modulesCore:login")
    }

完成的修改格式如下:

plugins {
    
    
    id 'com.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"
            
                    //让java代码也可以使用isDebug
        buildConfigField("boolean","isDebug",String.valueOf(isDebug))
    }

    //启用buildConfigField功能
    buildFeatures {
    
    
        buildConfig true
    }
    
    buildTypes {
    
    
        release {
    
    
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
    
    
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    
    

    implementation project(':modulesBase:libBase')
    
    //如果不是Debug的情况下则导入各个组件的模块
    if (!cfg.isDebug){
    
    
        implementation project(":modulesCore:live")
        implementation project(":modulesCore:login")
    }

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
5.2 将Gradle的数据导入到java中

在这里介绍一下buildConfigField,他可以将gradle的数据映射到java类里面,实现在java代码中使用gradle数据。

image-20231128221104768

注意是项目自带的BuildConfig而不是Arouter的BuildConfig。

image-20231128221202689

6、检验搭建成果

当ext下的isDebug为true时,刚才的app和login组件都可以独立运行。

image-20231123131042917

image-20231123131141079

当ext下的isDebug为false时,只有app可以运行。login被打包成arr文件。

image-20231123131239550

四、使用路由(Router)跳转不同组件的Activity

在我们没有应用组件化开发之前,界面跳转我们主要用显式Intent和隐式Intent两种方法。但是组件化开发中,是不允许组件层的模块横向依赖的,所以不可以直接访问彼此的类,就不能用显式Intent来跳转,此时我们就需要使用路由的方式跳转。

1、自定义路由

这里主要是自己尝试手写一个简单的路由,便于后面更好的理解路由工作机制

我们首先右键点击刚才的modulesBase包,然后在这个包下创建一个libRouter模块

image-20231123132026525

创建方式还是和刚才一样,我们此时就得到了一个libRouter模块。

image-20231123131709466

接下来我们新建立一个java文件,实现路由功能。

1.1 设计模式的选择

很明显这个Router一定是一个单例模式,这样才能保证每次共用的是同一个对象共享相同的数据。

private final static class Holder{
    
    
    static Router INSTANCE = new Router();
}

public static Router getInstance(){
    
    
    return Holder.INSTANCE;
}
1.2 使用什么结构保存不同的Class

Router的作用就是管理不同的类,可以统一协调和使用这些类,那么我们自然就需要一个存储这些类的方式。

显然这些类和他们的包名,路径存在对应关系,我们可以通过这些包名找到这些类。那么就需要一个Map了。

Map的结构模式如下:

image-20231123135031366

    static Map<String,Map<String,Class<?>>>  groupMap = new HashMap<>();

    Map<String,Class<?>> routerMap = new HashMap<>();
1.3 具体如何实现保存不同的Class

首先我们思考如果设计一个注册这些类的方法,那么应该需要什么样的参数?

回到刚才的存储结构当中,可以看出我们需要两个key,一个GroupName和一个Path,一个Path当中肯定包含一个GroupName。那么这个注册方法就需要一个Path,既然保存Class当然还需要一个Class.

public  void  register(String path,Class<?> cla)

函数参数就出来了,接下来是分析函数结构:

1、既然是Path中包含GroupName,那么我们需要解析Path,我们从文件的角度很容易知道Path是这样的一个字符串:

/main/MainActivity

我们需要将main解析出来,然后创建一个保存Class结构即可。

String[] strArray = path.split("/");
//解析结果如下:
""  "main" "MainActivity"

2、实现注册的逻辑如下:

image-20231123195948620

    /**
     * @param path /main/MainActivity
     * @param cla  需要存储的class
     */
    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);
        }
    }
1.4 如何使用路由跳转到不同的Activity

还是同样的分析函数参数,实现不同Activit的跳转,我们需要知道当前的Activit和需要跳转的Activity。

而不同的Activit实际上我们刚才已经放入groupMap当中了,我们只需要知道一个Path就可以取出这个Class

此时函数的参数就有了:

 public void startActivity(Activity activity, String path) 

代码逻辑也很简单两个if语句判断一下就行了

    /**
     * @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));
                }

            }
        }
    }
1.5 体验自定义路由

在modulesCore模块下新建一个live业务,方法和上述一样。

image-20231123201635408

然后我们进入App中,新建一个MyApplication继承Application.在onCreate这里注册路由表

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);
    }
}

然后我们进入login业务中,设置一个点击事件,用于跳转到live业务。

public class LoginActivity extends AppCompatActivity {
    
    

    private Activity activity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        activity = this;

        TextView view = findViewById((int) R.id.logView);

        view.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
                Router.getInstance().startActivity(activity,"/live/LiveActivity");
            }
        });
    }
}

在app下的MainActivity设置启动login模块,然后将ext下的isDebug设置为true即可。

==注意:==在这里如果你的app、live、login这几个模块下的layout布局的名称都是activity_main.xml就会出现资源冲突的问题,所以需要改一下不同的名字。

Peek 2023-11-23 20-28

2、使用第三方路由插件——ARouter

github仓库地址:ARouter

当前版本是:

image-20231123210545752

2.1导入依赖的方法

如下:

image-20231123212014711

需要在哪个业务下使用Arouter就需要在该组件的build.gradle下配置Arouter注解器。也就是1和2

ARouter的基本依赖可以通过Api放在基础模块libRouter中

image-20231123211011604

然后在libBase下导入libRouter即可:

image-20231123211239493

导入依赖后运行一下看看会不会报错,如果出现类似于这样的错误:

1: Task failed with an exception.

  • What went wrong:
    Execution failed for task ‘:modulesCore:login:processDebugMainManifest’.

    Manifest merger failed with multiple errors, see logs

  • Try:

Run with --info or --debug option to get more log output.

Run with --scan to get full insights.

可能是Arouter没有引入AndroidX库,需要转化为Androidx库支持的模式

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

android.useAndroidX=true
android.enableJetifier=true

image-20231123211637576

这样就可以自动转为Androidx库支持的模式,具体参考谷歌官方文档:迁移至 AndroidX

2.2 基本使用方法——实现不同组件的跳转

首先给login业务和live业务和app都加上这个注解处理器的依赖修改build.gradle。

和上面自定义路由的使用方法一样,我们首先肯定要保存这些Class,然后才能使用这些Class。

这里我们需要使用Arouter的注解器将每个Class加入进来。

image-20231123212906638

第一个路径是包名,第二个路径是类名和刚才自定义路由的用法一样。

接下来还是在MyApplication下进行初始化,BuildConfig.DEBUG可以获取当前的模式

image-20231123214509093

image-20231123214336931

然后在刚刚所有使用自定义路由跳转的方法改为Arouter即可

image-20231123213842458

这样就可以实现跳转了,如果无法正常跳转可能是gradle版本过高只需要, 把这个if语句去掉重新加载一下即可

image-20231123223646436

image-20231123223717437

2.3 实现不同组件数据的传递

1、在libBase中创建一个类用于传递

image-20231123231526088

2、在MainActivity中发送数据

image-20231123231444181

3、在LoginActivity中接受数据

@Route(path = "/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
    
    

    private Activity activity;

    @Autowired
    public String key1;

    @Autowired(name = "key2")
    public int str;

    @Autowired
    public Test key3;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        activity = this;

        TextView view = findViewById((int) R.id.logView);

        view.setOnClickListener(new View.OnClickListener() {
    
    
            @Override
            public void onClick(View view) {
    
    
//                Router.getInstance().startActivity(activity,"/live/LiveActivity");
                ARouter.getInstance().build("/live/LiveActivity").navigation();
            }
        });

        ARouter.getInstance().inject(this);

        Log.e("TAG", "onCreate: key1"+key1);
        Log.e("TAG", "onCreate: key2"+str);
        Log.e("TAG", "onCreate: key3"+key3.getName());
    }
}

更多用法参考:Arouter开发教程

猜你喜欢

转载自blog.csdn.net/m0_72983118/article/details/134938793