Android组件化工程结构以及项目实施

组件化优点

1、代码解耦
2、方便多人协作开发
3、可复用性高,不同的APP可复用不同组件
4、每个组件可独立运行,方便开发调试

组件化工程结构

Android组件化工程结构以及项目实施_第1张图片

**第一层:**空壳app。应用的入口,存放启动页,依赖所有业务组件

**第二层:**业务组件。根据不同业务横向拆分出来的业务组件。任何一个业务组件都可以独立出来成为一个应用

**第三层:**功能组件。通用业务是从应用业务中抽取出来的交集,从应用上说,他属于业务,而针对应用业务而言则更像是一种功能,好比登录这种业务功能,不需要关心有没有界面,当中是怎样的逻辑,只需要提供结果即可

**第四层:**公共服务组件。可存放各个组件对外暴露的接口,接口实现在组件内部,可通过ARouter或者DI(依赖注入)实现跨组件服务调用;可存放路由跳转等信息和路由服务等与业务相关Base代码

**第五层:**基础组件。网络请求、图片加载、存储、utils、通用View的封装

项目组件化

1、代码解耦

代码解耦主要是从两个方面,其一是公共代码的抽取和归纳,其二是面向接口编程,接口下沉。

公共代码的抽取和归纳:
部分通用的功能性的代码抽出成utils,上层只关心结果,不关心具体的实现逻辑

面向接口编程:
当上层需要底层的某项服务时,将服务抽象成一个接口,上层持有这个接口,而不是具体的类,那么当底层发生了改变或是实现的时候,上层只需要实例化对应的新实现类即可,如果把这层实例化也作为接口去作,那么上层完全不用改变就能拥抱变化。

依赖注入:
横向的业务代码或者功能实现可以进行依赖注入的方式来达到解耦的目。

工程结构解耦:
结构的解耦其实一般针对应用的整体业务而言进行的一个"分Module"操作,根据业务类型的横向拆分,业务属性的纵向拆分以及功能SDK下沉。

2、组件module gradle管理

  • 在根目录下建立一个config.gradle文件
  • 编写对应的依赖常量代码
  • 在app module 的build.gradle中引用
  • 注意,如果想要在别的.gradle中使用声明的这些常量,一定要在抽取的xx.gradle文件中将对应的代码块用"ext"进行包裹

config.gradle

ext {
    android = [
            compileSdkVersion: 28,
            targetSdkVersion : 28,
            minSdkVersion    : 21,
    ]

    version = [
            retrofitSdkVersion      : "2.4.0",
            androidSupportSdkVersion: "28.0.0",
            butterknifeSdkVersion   : "8.8.1",
            espressoSdkVersion      : "3.0.1",
            canarySdkVersion        : "1.5.4",
            glideSdkVersion         : "4.8.0"
    ]


    dependencies = [
            //support
            "appcompat-v7"                : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
            "design"                      : "com.android.support:design:${version["androidSupportSdkVersion"]}",
            "support-v4"                  : "com.android.support:support-v4:${version["androidSupportSdkVersion"]}",
            "cardview-v7"                 : "com.android.support:cardview-v7:${version["androidSupportSdkVersion"]}",
            "annotations"                 : "com.android.support:support-annotations:${version["androidSupportSdkVersion"]}",
    ]
}

在工程根目录的build.gradle中加上如下代码:

apply from: "config.gradle"

在app module中的build.gradle中引用(其他组件module中引用类似):

apply plugin: 'com.android.application'


android {

    compileSdkVersion rootProject.ext.android["compileSdkVersion"]
    defaultConfig {
        applicationId "cn.com.xxx"
        minSdkVersion rootProject.ext.android["minSdkVersion"]
        targetSdkVersion rootProject.ext.android["targetSdkVersion"]
        versionName "1.0.0"
        versionCode getVersionCode(versionName)
    }
}


dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation rootProject.ext.dependencies["appcompat-v7"]
    implementation rootProject.ext.dependencies["cardview-v7"]
    implementation rootProject.ext.dependencies["support-v4"]
    implementation rootProject.ext.dependencies["design"]
    implementation rootProject.ext.dependencies["annotations"]
    implementation rootProject.ext.dependencies["constraint-layout"]
    implementation rootProject.ext.dependencies["arch-lifecycle"]
    implementation rootProject.ext.dependencies["FlycoTabLayout_Lib"]
    implementation rootProject.ext.dependencies["FlycoPageIndicator_Lib"]
    implementation rootProject.ext.dependencies["nineoldandroids"]
    implementation rootProject.ext.dependencies["jiecaovideoplayer"]
    implementation rootProject.ext.dependencies["SmartRefreshLayout"]
}

如此以后在查看或者更换依赖的时候也方便查看和维护,注意在进行依赖的过程中,因为依赖不同的三方,可能会出现重复依赖相同库而版本不一致的情况,这里有两种解决办法,一种是在对应依赖的三方中剔除对应的pom依赖,如:

api('com.facebook.fresco:fresco:0.10.0') {
       exclude module: 'support-v4'
}

另外一种是强制依赖的相同库的版本,如:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (requested.name.startsWith("support-") ||
                    requested.name.startsWith("animated") ||
                    requested.name.startsWith("cardview") ||
                    requested.name.startsWith("design") ||
                    requested.name.startsWith("gridlayout") ||
                    requested.name.startsWith("recyclerview") ||
                    requested.name.startsWith("transition") ||
                    requested.name.startsWith("appcompat")) {
                details.useVersion SUPPORT_LIB_VERSION
            } else if (requested.name.startsWith("multidex")) {
                details.useVersion OTHER_VERSION.multiDex
            }
        }
    }
}

3、组件路由

路由其实是组件化的核心组件,网上也有很多优秀的开源库,这里就直接使用阿里的开源库ARouter,地址如下:

https://github.com/alibaba/ARouter

ARouter配置

1.每一个模块都必须引入compiler sdk,只需在base模块中依赖 api sdk

base module

api   "com.alibaba:arouter-api:1.4.1"

其他组件 module

annotationProcessor "com.alibaba:arouter-compiler:1.2.2"

2.每一个module 的分组都必须不同,分组就是path的第一个"/“与第二个”/"之间。

3.每个module中的build.gradle中都要加入如下配置

android {
    defaultConfig {
        
        javaCompileOptions {
            annotationProcessorOptions {
                includeCompileClasspath = true
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}

ARouter使用

在common-service模块下建一个router包
Android组件化工程结构以及项目实施_第2张图片
1.provider目录下存放提供服务的接口,提供服务的具体实现类放在提供服务的模块中

@IRouterName(ServiceName.USER_INFO)
public interface IAppUserInfo extends IProvider {
    String getName();
    @Nullable
    String getMobile();
    String getIconUrl();
    String getOpenId();
    String getUnionId();
    String getPayOrderOpenId();
    long getId();
    int ROLE_TYPE_DEFAULT = 0;
    int ROLE_TYPE_SHOP_KEEPER = 1;
    int getRoleType();
    @Nullable
    String getWechatId();
    int getApiVersion();
    Disposable checkUserInfoExpire(Consumer isExpire);
}

2.IRouterName是一个注解,用来注解接口服务的路由,方便开发者明确哪个服务接口对应的ARouter的路由

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface IRouterName {
    String value();
}

3.ModulePath用来存在组件的路由地址

@StringDef({
        ModulePath.LIVE_VIDEO,ModulePath.WEB_VIEW
})
public @interface ModulePath {
    /**
     * webview模块
     */
    String WEB_VIEW = "/app/webView";
    /**
     * 直播模块路由
     */
    String LIVE_VIDEO = "/live/liveVideo";
}

4.ServiceName用来存放服务的路由地址

@StringDef({
        ServiceName.LIVE_VIDEO,
        ServiceName.USER_INFO,
        ServiceName.SHARE_DIALOG
})
public @interface ServiceName {
    /**
     * 直播服务路由
     */
    String LIVE_VIDEO = "/live/liveService";

    /**
     * 用户信息服务路由
     */
    String USER_INFO = "/app/userInfo";
    /**
     * 分享弹框
     */
    String SHARE_DIALOG = "/app/shareDialog";
}

5.RouterManager来管理具体的路由跳转

public class RouterManager {

    public static void goLiveVideo(String fromPage){
        ARouter.getInstance().build(ModulePath.LIVE_VIDEO)
                .withString(FROM_PAGE, fromPage)
                .navigation();
    }

    public static void goWebView(String url, String fromPage){
        Map params = new HashMap<>();
        params.put("url", url);
        params.put("app_name", "SSBB");
        ARouter.getInstance().build(ModulePath.WEB_VIEW)
                .withString(FROM_PAGE, fromPage)
                .withString(PARAMS,GsonUtil.toJson(params))
                .navigation();
    }
}

6.各业务模块通过RouterManager来跳转

RouterManager.goLiveVideo("manager_icon");

4、单独调试

当工程被拆分为组件化的时候,那么Module的单独调试就显得尤为重要,无论是对问题的跟踪还是业务线的并行开发,都需要工程具备单独运行调试的能力。这里单独调试同样是对gradle的操作,通过对编译脚本的编写来达到组件单独运行的目的。

1.在config.gradle对需要单独运行的Module抽取变量进行记录:

ext {

    /**
     * 分享组件
     */
    shareRunAlone = false

    /**
     * 直播组件
     */
    liveRunAlone = false

    /**
     * 登录组件
     */
    loginRunAlone = false
}

2.在对应module的编译脚本文件中添加判断module是以lib形式依赖还是以app方式进行依赖,如下代码:

def runAlone = rootProject.ext.loginLibRunAlone.toBoolean()

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

添加Application中一些必要的元素,清单文件Manifest.xml文件,但是这个xml文件是文件在单独运行的过程中所需要的,所以这里要放到一个单独的目录下:
Android组件化工程结构以及项目实施_第3张图片

同时在此基础上通过编译脚本配置单独运行时获取的Android相关文件:

android {
    sourceSets {
        main {
            if (runAlone) {
                manifest.srcFile 'src/main/runAlone/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java{
                    exclude 'src/main/runAlone/*'
                }
            }
        }
    }
}

单独调试时增加applicationId,集成调试时移除

android {
    defaultConfig {
        if (runAlone) {
            applicationId "cn.com.live"
        }
    }
}

根据以上配置,就可以使得登录模块单独运行起来,调试起来非常方便。

5、整体调试

根据上述方案,app壳其实只需要依赖业务组件即可:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':live')
}

业务组件在根据调试变量对相应的基础组件进行依赖:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')

    api project(':common-service')
    annotationProcessor OTHER_LIBRARY.arouterCompiler
    if(!shareRunAlone){
        api project(':share')
    }
    if(!loginRunAlone){
        api project(':login')
    }
    if(!liveRunAlone){
        api project(':live')
    }
}

6、资源名冲突

color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题

可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。

android 
    // 所有xml资源命名以 "live_" 开头、否则编译报红
    resourcePrefix "live_"
}

7、组件Application初始化

自定义 Application 需要声明在 AndroidManifest.xml 中。其次,每个 Module 都有该清单文件,但是最终的 APK 文件只能包含一个。因此,在构建应用时,Gradle 构建会将所有清单文件合并到一个封装到 APK 的清单文件中。

合并的优先级是:

App Module > Library Module

合并的规则:
Android组件化工程结构以及项目实施_第4张图片

结合我们的情况,是值 A 合并值 B,会产生冲突错误:

Execution failed for task ':app:processDebugManifest'.
> Manifest merger failed : Attribute application@name value=(com.baseres.BaseApplication) from AndroidManifest.xml:8:9-51
    is also present at [:carcomponent] AndroidManifest.xml:14:9-55 value=(com.carcomponent.CarApplication).
    Suggestion: add 'tools:replace="android:name"' to  element at AndroidManifest.xml:7:5-24:19 to override.

错误信息中给出了解决建议,在高优先级的 App Module 中使用 tools:replace=“android:name”,但这样做是直接用值 A 替换了值 B,并非我们想要的结果。另外再推荐给大家一个方法,打开 App Module 的 AndroidManifest.xml 文件,选择下方 Merged Manifest 选项卡,可以看到预合并结果。

解决方法:通过反射在app模块的Application的onCreate()方法中调用组件Application的初始化方法

1.在base模块中新增BaseAPP

public abstract class BaseApp extends Application {
    /**
     * Application 初始化
     */
    public abstract void initModuleApp(Application application);
    private static Context appContext;

    @Override
    public void onCreate() {
        super.onCreate();
        appContext = getApplicationContext();
    }

    public static Context getAppContext() {
        return appContext;
    }
}

2.在live模块中的LiveApplication继承BaseApp实现initModuleApp()方法,并在此方法中做初始化操作,作为library时初始化操作在initModuleApp()方法中,作为独立app时,初始化操作在onCreate()中。

public class LiveApplication extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void initModuleApp(Application application) {
        TraceManager.init(getAppContext(), Retrofit2Helper.getInstance().getRetrofit());
    }
}

3.在base模块中增加AppConfig类,用来配置需要初始化的组件Application

public class AppConfig {
    private static final String LiveApp = "cn.com.live.LiveApplication";

    public static String[] moduleApps = {
            LiveApp
    };
}

4.在app的Application的onCreate()方法中通过反射初始化在AppConfig中声明类全路径的组件Application类

public class WeBuyApp extends BaseApp {

    @Override
    public void onCreate() {
        super.onCreate();

        initModuleApp(this);
    }
    
    @Override
    public void initModuleApp(Application application) {
        for (String moduleApp : AppConfig.moduleApps) {
            try {
                Class clazz = Class.forName(moduleApp);
                SBBaseApp baseApp = (SBBaseApp) clazz.newInstance();
                baseApp.initModuleApp(this);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }
}

8、组件代码混淆

方法一:直接在app模块的proguard-rules.pro中编写所有组件的混淆规则
优点:简单无脑
缺点:若app模块依赖的组件很多,则proguard-rules.pro中混淆规则庞大不利于维护,使用app模块编写所有混淆命令是基于业务模块当中不再编写混淆命令为前提,所以在打包将业务模块上传到私有仓库时,业务模块都是不开启混淆功能的!

但是
上述结论都是建立在以implementation或者api形式依赖的前提下,开发阶段我们是以

implementation project(':live')

这种形式进行依赖的,你会发现当以这种形式进行依赖时,不管业务模块minifyEnabled是true还是false,只要app模块写上了正确的混淆规则那么程序都能正常运行!

方法二:各个业务组件单独编写混淆规则(推荐)
优点:各组件自己配置混淆文件,易于维护
在模块中的build.gradle中配置

android {
    buildTypes {
        release {
            consumerProguardFiles 'proguard-rules.pro'
        }
    }
}

使用这种配置最大的一个好处就是业务模块的是否混淆完全由app模块来决定,这种配置有一个非常重要的关键点就是不能设置minifyEnabled true,因为设置为true之后业务模块是否混淆的控制权将只能由该模块自身决定,app模块将无法控制业务模块的混淆与否而且设置为true之后还必须额外配置

proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'

9、组件库的独立发布和维护

原有拆分的本地组件彻底分离出去,采取独立发布和维护的方式迭代更新。

你可能感兴趣的:(安卓开发,架构,组件化,架构,模块化,混淆,组件通信)