Skip to content

Latest commit

 

History

History
394 lines (292 loc) · 14.1 KB

组件化分享(一期).md

File metadata and controls

394 lines (292 loc) · 14.1 KB

[TOC]

组件化

为什么组件化

为什么组件化

随着APP业务的不断增加,版本不断迭代,APP越来越臃肿,所带来的问题也随之增加。模块化架构主要思路就是“分而治之”,把依赖整理清楚,减少代码冗余和耦合。在把代码抽取到各自模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。

现阶段遇到的问题

  • 构建速度慢(Gradle 无规则依赖),开发效率低。
  • 迭代速度慢,耦合严重,无法单独测试
  • 代码冲突多,编译慢

组件化好处

  • 加快编译速度,可以把不会经常变动的组件做成静态库,同时每个组件可以独立编译,不依赖于主工程或者其他组件
  • 业务模块解耦,模块、组件之间“各司其职”,加快业务迭代速度
  • 测试困扰,模块之间可以独立运行、测试
  • 多条业务线可以并行开发,提高开发效率
  • 控制代码权限,将代码的权限细分到更小的粒度
  • 公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量

什么是组件化

模块化组件化,其中的界限是什么,模块化和组件化的本质区别又是什么?为了解决这些问题,我们就要先了解 “模块” 和 “组件” 的区别。

组件化和模块化区别

[模块] 和 [组件] 间最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。

▍组件

组件指的是单一的功能组件,如 [视频组件]、[直播组件] 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用

▍模块

模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等

组件化实施流程

统一管理SDK版本

config.gradle主要来管理统一的SDK版本,避免版本冲突

ext {
    isApplication = false  //false:作为Lib组件存在, true:作为application存在,这个不建议改
    isLoginApplication = true  //登录模块开关,false:作为Lib组件存在, true:作为application存在
    isLiveApplication = true  //直播模块开关,false:作为Lib组件存在, true:作为application存在

    android = [
            compileSdkVersion       : 28,
            buildToolsVersion       : "28.0.3",
            minSdkVersion           : 17,
            targetSdkVersion        : 28,
            versionCode             : 22,
            versionName             : "1.8.2"    //必须是int或者float,否则影响线上升级
    ]

    version = [
            androidSupportSdkVersion: "28.0.0",
            retrofitSdkVersion      : "2.4.0",
            glideSdkVersion         : "4.8.0",
            canarySdkVersion        : "1.5.4",
            constraintVersion       : "1.0.2",
            okhttp                  : "3.8.1"
    ]

    dependencies = [
            //support
            "appcompat-v7"             : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
            //router
            "router"                      : "com.alibaba:arouter-api:1.4.1",
            "router-compiler"             : "com.alibaba:arouter-compiler:1.2.2",
    ]
}

集成模式VS组件模式

在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了两种种插件,在开发中可以通过配置不同的插件来配置不同的工程。

  • App 插件,id: com.android.application
  • Library 插件,id: com.android.libraay

区别比较简单, App 插件来配置一个 Android App 工程,项目构建后输出一个 APK 安装包,Library 插件来配置一个 Android Library 工程,构建后输出 aar 包。

通过在 module 中添加一个 gradle.properties 配置文件,在配置文件中添加一个布尔类型的变量 isAsAppRun,在 build.gradle 中通过 isAsAppRun 的值来使用不同的插件从而配置不同的工程类型,在组件调试和集成调试时直接修改 isAsAppRun 的值即可。例如,在 Login 登录组件中的配置:

▍gradle.properties 示例

# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

isAsAppRun=false

▍模块模式切换代码

if (isAsAppRun.toBoolean()) {
    //组件模式
    apply plugin: 'com.android.application'
} else {
    // 集成模式
    apply plugin: 'com.android.library'
}

动态配置组件的 ApplicationId 和 AndroidManifest 文件

一个 APP 是只有一个 ApplicationId 的,所以在单独调试和集成调试时组件的 ApplicationId 应该是不同的; 而组件模式又是可以单独打包,必然涉及到AndroidManifest 清单文件的合并的问题;所以除了通过依赖的插件来配置不同的工程,我们还要根据 isAsAppRun 的值来修改其他配置。

ApplicationId 和 AndroidManifest 文件都是可以在 build.gradle 文件中进行配置的,所以我们同样通过动态配置组件工程类型时定义的 isAsAppRun 这个变量的值来动态修改 ApplicationId 和 AndroidManifest。首先我们要新建一个 AndroidManifest.xml 文件,加上原有的 AndroidManifest 文件,在两个文件中就可以分别配置单独调试和集成调试时的不同的配置

▍清单文件配置

组件模式:src/main/manifest/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.login">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".LoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


集成模式:src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.loong.login">

    <application
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        <activity android:name=".LoginActivity" />
    </application>

</manifest>

然后在 build.gradle 中通过判断 isAsAppRun 的值,来配置不同的 ApplicationId 和 AndroidManifest.xml 文件的路径:

▍applicationId、清单文件动态配置


android {
    compileSdkVersion compile_sdk_version.toInteger()

    defaultConfig {
        if (isAsAppRun.toBoolean()) {
            applicationId "com.loong.login"
        }
        ...
    }

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

组件之间调用和通信

在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候就需要引入“路由”的概念了。 在介绍路由之前,我们先看下传统架构模式和组件化之后的架构模式有什么不同。

传统结构

▍传统APP架构图

摘自网络 image

▍存在的问题

往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。

以下图片摘自网络 image

组件化结构

image

组件间通信方式

组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系。在这个演示项目中,使用的阿里开源的路由框架。

▍示意图

image

▍项目配置

componentbase 模块下的build.gradle 配置 Base 模块下build.gradle 文件中配置路由

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    api(rootProject.ext.dependencies["appcompat-v7"]) {
        exclude module: "support-v4"
        exclude module: "support-annotations"
    }
    // 配置路由
    api 'com.alibaba:arouter-api:1.3.1'
}

其余模块下gradle 配置

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

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation project (':base')
    
    //// arouter-compiler 的注解依赖需要所有使用 ARouter 的 model 都添加依赖
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}

▍使用方式

ARouter统一入口封装工具类

/**
  * 简单的跳转页面
  * @param string                string目标界面对应的路径
  * @param callback              监听路由过程
  */
public static void navigation(String string , Context context , NavigationCallback callback){
    if (string==null){
        return;
    }
    ARouter.getInstance()
            .build(string)
            .navigation(context,callback);
}

// 监听
public static NavigationCallback getCallback(){
    NavigationCallback callback = new NavCallback() {
        @Override
        public void onArrival(Postcard postcard) {
            Log.i(TAG,"ARouterUtils"+"---跳转完了");
        }

        @Override
        public void onFound(Postcard postcard) {
            super.onFound(postcard);
            Log.i(TAG,"ARouterUtils"+"---找到了");
        }

        @Override
        public void onInterrupt(Postcard postcard) {
            super.onInterrupt(postcard);
            Log.i(TAG,"ARouterUtils"+"---被拦截了");
        }

        @Override
        public void onLost(Postcard postcard) {
            super.onLost(postcard);
            Log.i(TAG,"ARouterUtils"+"---找不到了");
            //无法找到路径,作替换处理
            PathReplaceServiceImpl pathReplaceService = new PathReplaceServiceImpl();
//                pathReplaceService.replacePath(ARouterConstant.ACTIVITY_ANDROID_ACTIVITY,ARouterConstant.ACTIVITY_DOU_MUSIC_ACTIVITY);
        }
    };
    return callback;
}

定义路由跳转路径

public class ARouterConstant {

    //跳转到登录页面
    public static final String ACTIVITY_LOGIN = "/account/login/LoginActivity";
    //跳转到直播页面
    public static final String ACTIVITY_LIVE = "/share/share/LiveActivity";

    public static final String ACTIVITY_ANDROID_ACTIVITY = "/degrade/web/DegradeWebActivity";

}

调转LoginActivity 方式

/* 跳登录界面
 *
 * @param view
 */
public void login(View view) {
    ARouterUtils.navigation(ARouterConstant.ACTIVITY_LOGIN,this,ARouterUtils.getCallback());
}

目标Activity 定义@Router

@Route(path = ARouterConstant.ACTIVITY_LOGIN)
public class LoginActivity extends AppCompatActivity {
    // 代码省略
}

使用该方式即可实现组件之间的通信。以及数据传递。该文档不再过多的介绍ARouter原理以及执行流程。具体相关的ARouter 分析。详见下一篇ARouter流程分析

组件化中Fragment通信难点

除了 Activity 的跳转,我们在开发过程中也会经常使用 Fragment,一种很常见的样式就是应用主页 Activity 中包含了多个隶属不同组件的 Fragment。一般情况下,我们都是直接通过访问具体 Fragment 类的方式实现 Fragment 的实例化,但是现在为了实现模块与组件间的解耦,在移除组件时不会由于引用的 Fragment 不存在而编译失败,我们就不能模块中直接访问组件的 Fragment 类。

▍解决方案:

① 通过反射来解决,通过来初始化 Fragment 对象并返回给 Activity,在 Actiivty 中将 Fragment 添加到特定位置即可。

② 通过ARouter 路由方式获取

/**
  *
  * @param path path目标界面对应的路径
  * @return
  */
public static Fragment getTargetFragment(String path){
    Fragment fragment;
    if (TextUtils.isEmpty(path)){
        return null;
    }
    fragment = (Fragment) ARouter.getInstance()
            .build(path)
            .navigation();
    return fragment;

}