Android 组件化实战

Posted datian1234

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 组件化实战相关的知识,希望对你有一定的参考价值。

一、什么是模块化

module模块,包含两种格式application和library。概念是一个module是一个小项目,相对于包来说模块更灵活,耦合更低,随意插拨,根据不同关注点将项目共享部分或业务模块抽取出来形成独立module。

二、什么是组件化

基于模块化,核心思想是角色的转换,在打包时是library,开启调试是application。单位是组件(module),目的是解耦与加快开发,隔离不需要关注的部分。分离独立的业务组件如微信朋友圈, 相对于模块化侧重于业务解耦

三、组件化有什么好处

前后端分离、MVP架构这些思想都是为了解耦,解耦就像是把对应的物品放入对应的箱子,这样在修改或者增加时,就能最小程度的不影响其它模块。

四、结构图

五、代码结构怎么设计

1. 根据业务逻辑新建module

假设有三个业务模块,加上app的主Module,加上common公共Module,所以一共需要5个Module,所以代码结构应该是这样:

2. 提取公共配置,避免版本不一致造成的错误

在项目根目录的build.gradle中的最底部添加

ext{

    //SDK 版本号
    compileSdkVersion = 28
    buildToolsVersion = '28.0.3'
    minSdkVersion = 21
    targetSdkVersion = 28

    //app 版本号
    versionCode = 118
    versionName = "1.1.8"

    //依赖库版本号
    supportLibVersion = '28.0.0'

}

然后在每个Module中对应修改成如下代码:

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        applicationId "com.demo.myapplication"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }

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

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation "com.android.support:appcompat-v7:$rootProject.ext.supportLibVersion"
    implementation "com.android.support.constraint:constraint-layout:$rootProject.ext.constraintLibVersion" // 注意这里是双引号
}

3. 设置模块是否可以单独运行

在根目录的gradle.properties末尾添加

# 是否需要单独编译, 是否是application,true为application,false为library
isApplicationModule1=false
isApplicationModule2=false
isApplicationModule3=false

然后在module中的build.gradle中改变 application 还是 library

(1) 区分 application 还是 library

if (isApplicationModule1.toBoolean()){
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}

(2) 增加 applicationId

 defaultConfig {
        if (isApplicationModule1.toBoolean()){
            applicationId "com.demo.myapplication.module1"
        }
    }

(3) AndroidManifest 启动入口

 sourceSets {
        main {
            if (isApplicationModule1.toBoolean()) {
                manifest.srcFile 'src/main/module/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }

4. 在主 app module 中引入其他Module

要注意的一点是每个组件如果是独立运行的话,那它是不能作为依赖库供app模块使用的,所以在app模块下的build.gradle文件中需要进行依赖判断

    if (!isApplicationModule1.toBoolean()){
        implementation project(':module1')
    }

    if (!isApplicationModule2.toBoolean()){
        implementation project(':module2')
    }
    if (!isApplicationModule3.toBoolean()){
        implementation project(':module3')
    }

    implementation project(':common')

在模块 Module中引入 common module

    implementation project(':common')

至此通过修改配置,每个Module就可以单独运行。

5. 引入Arouter

由于需要模块之间的解耦,平时的Intent跳转需要导包,而Arouter引入之后,就能在跳转的同时不用导包。

集成Arouter

(1) 引入依赖

common中添加

    api("com.alibaba:arouter-api:$rootProject.ext.arouterLibVersion") { exclude group: 'com.android.support' }
    annotationProcessor "com.alibaba:arouter-compiler:$rootProject.ext.arouterAnnotationLibVersion"

在每个Module下都需要添加注解

annotationProcessor "com.alibaba:arouter-compiler:$rootProject.ext.arouterAnnotationLibVersion"

(2) 每个引入Arouter的Module的build.gradle中加入下面代码

defaultConfig {

        ...

        //arouter
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
    }

(3) 在Application中进行初始化

       if (isDebug) {
            ARouter.openLog();
            ARouter.openDebug();
        }
        ARouter.init(this);

(4)使用Arouter跳转

定义路由

@Route(path = "/app/main")
public class MainActivity extends AppCompatActivity {
...
}

跳转路由

ARouter.getInstance().build("/app/main").navigation();

6. ButterKnife 的坑

单工程使用这个库是没有问题的,但是在组件化使用的时候就报错了。 除了一系列的配置之外,还有个难受的地方就是需要把Library中的R改成R2,但当你想把Library改成Application时,又要将R2改成R,这岂不是很难受。

!!! 所以我目前放弃在Library中使用ButterKnife

7. switch (view.getId()) 的坑

由于 Library中的view.getId 可能是个变量,所以需要把 switch 改成 if。

快捷操作:

光标移至switch这行代码上,同时按下 alt + Enter, 此时点击 Replace switch to if

8. 资源重复的坑

 No static field XXX of type I in class Lcom/XXX/R$id错误

出现这个问题是layout 和 主布局名字重复了,养成习惯,在Library中资源文件前加上标识,比如 library_layout

另外有一种方法只是帮忙增加约束,提醒。

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    // 增加资源的约束,提醒
    if (toolsIsLibrary.toBoolean()) {
       resourcePrefix "${project.name}_"
    }
}

为了保证不与其他module冲突,建议在string、style、color、dimens 等资源文件都加上前缀。

六、踩坑

1. ARouter 组件化打包不成功

出现问题:Program type already present: com.alibaba.android.arouter.routes.ARouterGroupGroupservice

在ModuleA 和 ModuleB 中不能使用相同的 Group。
根据提示报错 service 就是重复的Group,全局搜索,于是发现在ModuleA 和 ModuleB中都存在以下代码。

package com.bp.tech.common.util;

import android.content.Context;

import com.alibaba.android.arouter.facade.annotation.Route;
import com.alibaba.android.arouter.facade.service.SerializationService;

import java.lang.reflect.Type;

@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {

    @Override
    public void init(Context context) {

    }

    @Override
    public <T> T json2Object(String text, Class<T> clazz) {
        return GsonUtils.parseObject(text, clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return GsonUtils.toJsonString(instance);
    }

    @Override
    public <T> T parseObject(String input, Type clazz) {
        return GsonUtils.parseObject(input, clazz);
    }
}


把它提取到公共的Module中就好了。

  1. Arouter 无法跳转的问题
    开启InstantRun之后无法跳转(高版本Gradle插件下无法跳转)?

解决办法:

if(debug环境){
    ARouter.openDebug(); 
 }
 ARouter.init(this); 

补充问题出现的原因:
Arouter 原理

路由框架会在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中。

我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activity.class映射关系的类文件,然后app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里)。

public class MyRouters{

    //项目编译后通过apt生成如下方法
    public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) {
        route.put("/main/main", MainActivity.class);
        route.put("/module1/module1main", Module1MainActivity.class);
        route.put("/login/login", LoginActivity.class);
    }
}

那么映射关系是通过什么生成的呢?

在Activity类上加上@Route注解之后,便可通过apt生成对应的路由表。 被众多框架使用的是apt及javapoet技术,那么什么是apt,什么是javapoet呢?

APT是Annotation Processing Tool的简称,即注解处理工具。 apt是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。

因为开启InstantRun之后,很多类文件不会放在原本的dex中,需要单独去加载,ARouter默认不会去加载这些文件,因为安全原因,只有在开启了openDebug之后 ARouter才回去加载InstantRun产生的文件,所以在以上的情况下,需要在ARouter.init(this)之前调用openDebug

以上是关于Android 组件化实战的主要内容,如果未能解决你的问题,请参考以下文章

从活动向上导航到片段打开相同的片段 - Android 导航组件

Android 调用组件 w/listener 或让 viewmodel 调用组件与片段通信

Android组件化开发快速上手,附强化实战

如何使用底部导航视图和 Android 导航组件将参数传递给片段?

Android导航组件:如何保存片段状态

Android:当用户在导航组件、单活动应用程序中回击片段 B 时更新片段 A