Andorid极速组件化

Android 极速组件化

Demo地址(壳工程合并组件后打包的apk如果更改了组件在次运行,请先清除缓存在安装,或者卸载掉原有的)

组件化插件地址,渐进式体验,当你使用插件的那一刻就已经完成了组件化

命运多舛,刚加入一家做区块链的公司1月有余,今天面临公司全员降薪,裁员等等不利因素。
加入公司1月有余,重构了网络层的使用和一些IM的自定义消息问题,webview交互。本雄心壮志推动组件化的,奈何......算了,进入正题

为什么要项目组件化

当前项目整体结构图

image.png
  • 随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,
  • APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个Developer都要其他模块的代码,将很难进行多人协作开发
  • Android项目在编译代码的时候电脑会非常卡,
  • 单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,
  • 单元测试根本无从下手
  • 无法协同开发工作,代码风格控制不便
  • 模块之间耦合严重,互相调用,不断持续下去将会无限耦合,迭代维护难度增大

所以必须要有更灵活的架构代替过去单一的工程架构。

组件化实现

image.png

整个项目被切割为了无数个app,无数个app均可单独运行

如何组件化

组件化架构图:


image.png
名称 含义
App模式 将所有业务组件集合成为一个App
组件模式 每个业务组件可单独作为App运行
App壳工程 融合业务组件的工程壳(可写一定业务,也可以不写)
业务组件 根据对应业务的一个App
公共组件 所有业务组件所需要集合使用的
SDK组件 第三SDK或者自身业务的进一步封装,将归纳到公共组件中
Base层 抽象业务层所需要的公共操作属性等

组件化架构的意图:

  • 告别臃肿的项目结构
  • 使单一业务形成app组件,便于调试
  • 单一组件利于单元测试
  • 协同开发,隔离组件与组件,模块与模块之间的耦合
  • 加快开发效率,告别编译一次Xmins的时长
  • 降低团队成员对项目某一业务的熟悉度,仅需关注自身所负责的业务
  • git权限控制,避免共享式开发
  • 新业务可直接用组件形式开发,开发完成即可单元测试甚至单独给予测试人员测试,测试完毕即可整合到壳工程中
  • 各业务研发可以互不干扰、提升协作效率,并控制产品质量;

组件化痛点(以下痛点仅介绍初级手动切换方式,后续将通过插件自动完成以下特性)

1、app与lib之间的切换完成
2、manifest的自动合并
3、组件的启动activity的自定义
4、组件的application自定义
5、组件路径的自动依赖处理

痛点 含义
组件单独运行 如何将一个庞大的项目分而治之,可单独运行和集成为一个整体
UI跳转 每个业务组件之间如何跳转
组件通信 如何在业务之间进行数据传递
代码隔离 组件与组件之间如何避免直接引用

组件单独运行

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

isDebug可定义于gradle.properties中,这样在每个module的build.gralde中都可以读取到该字段来动态定义组件作为app还是lib的存在。

组件与组件之间的Manifest合并问题

总所周知,Android的运行避免不了每个Activity的定义与权限的声明,因为我们的组件可作为组件单独运行,每一个组件都拥有自己的Application与Activity、AndroidManifest,那么我们在合并的时候必然导致冲突。

这个时候我们gradle又派上了用场,我们可以分别定义2个AndroidManifest,在组件开发阶段和集成模式下使用的是不同的AndroidManifest,并且在集成模式时将开发阶段的AndroidManifest剔除

 sourceSets {
        main {
            if (rootProject.ext.isBuildApp) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                //移除debug资源
                manifest.srcFile 'src/main/release/AndroidManifest.xml'
                java {
                    exclude 'debug/**'
                }
            }
        }
    }
  • Debug模式作为App单独运行的AndroidManifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.allure.module.login">

<application
    android:name="debug.LoginApplication"
    android:theme="@style/AppTheme"
    android:icon="@mipmap/ic_launcher"
    android:allowBackup="true"
    android:label="@string/app_name"
    android:supportsRtl="true">

    <activity android:name=".LoginActivity"
        android:exported="true">

        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <data android:scheme="allure" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

</manifest>
  • 集成模式下的AndroidManifest

集成模式下,壳工程已经含有了Application与一些通用的配置(如Style的定义,Lancher等的定义),这时我们应只配置具体的Activity

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.allure.module.login">

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:supportsRtl="true">

        <activity android:name=".LoginActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="allure" />
            </intent-filter>
        </activity>
    </application>

</manifest>

全局的Application处理

上面的合并Manifest中我们处理了合并问题,但实际运用中可能说某一个单一组件需要使用某一个SDK并且进行初始化。
在组件模式下我们单独定义了一个Application来处理,但实际合并过程,壳工厂已经包含了自的Applictaion,我们自定义的将会无效,这时怎么处理

处理方案一:下沉Application处理

我们可以将Application下沉到Base层或者Common层。在其中定义BaseApplication,将所有的第三方SDK初始化等操作放置于其中,组件将依赖Base层来达到目的

但是这样做个人认为在组件中依赖了Base层,依然不太优雅,这时推荐方案二

处理方案二:代理反射处理Application初始化

用过SDK的朋友都知道,SDK在早起阶段会有自己的Application,开发者使用需要继承SDK的Application,如果多个SDK都这样处理,那开发者受的苦就大了。(Java的单继承限制)

这时,我们考虑将Application的onCreat代理出去,让他人使用,然后将其反射处理初始化

接口:

public interface ApplicationImpl {

    void onCreate(Application baseApplication);
    
}

在单一组件进行方法实现:

public class LoginApplication implements ApplicationImpl {

    private static final String TAG = "LoginApplication";

    @Override
    public void onCreate(Application baseApplication) {
        Log.e(TAG, "初始化LoginApplication");

    }

}
public class ShopApplication implements ApplicationImpl {
    
    private static final String TAG = "ShopApplication";

    @Override
    public void onCreate(Application baseApplication) {
        Log.e(TAG, "初始化ShopApplication");
    }
}

反射对象:

public class ModulesApplicationConfig {
    public static final String[] MODULES_LIST = {
            "com.allure.login.application.LoginApplication",
            "com.allure.shop.application.ShopApplication"
    };
}
  for (String modulesImpl : ModulesApplicationConfig.MODULES_LIST) {
            try {
                Class<?> aClass = Class.forName(modulesImpl);
                Object object = aClass.newInstance();

                if (object instanceof ApplicationImpl) {
                    ((ApplicationImpl) object).onCreate(this);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }

单一组件运行结果:

com.allure.modularization E/ShopApplication: 初始化ShopApplication

组件与组件之间的UI跳转

在常规开发下,因为我们的app全部处于一个工程之下,可采用显示或者隐式的跳转都可以,但如今组件作为单一模式如何跳转,这时,我们可以想到中间件来处理,这里非常感谢阿里开源的ARouter

image.png

我们所有的跳转都路由注册,统一由Arouter进行分发控制,避免了模块组件之间的直接接触

同时,路由可做的远不仅如此,还可对WebView与Native进行混合开发的App进行优雅控制,具体用法自行查看README

组件与组件之间的通信

当某一个组件需要另外一个组件的数据时怎么办?

在common层创建Provider

public interface ILoginProvider extends IProvider {

    LoginInfoBean getLoginInfo();
    boolean isLogin();
    void start2Login();

    @Override
    void init(Context context);
}

登录组件进行具体实现:

@Route(path = ARouterPathConfig.SERVICE_LOGIN)
public class LoginProviderImpl implements ILoginProvider {
    private static final String TAG = "LoginProviderImpl";
    private Context mContext;

    @Override
    public void init(Context context) {
        this.mContext = context;
    }


    /**
     * 其他快莫获取此模块组件的信息
     *
     * @return
     */
    @Override
    public LoginInfoBean getLoginInfo() {
        //此处可以做判断是否登录处理
        LoginInfoBean loginInfoBean = new LoginInfoBean();
        loginInfoBean.setAge("18");
        loginInfoBean.setName("inChat");
        loginInfoBean.setLogin(true);
        return loginInfoBean;
    }

    @Override
    public boolean isLogin() {
        return true;
    }

    @Override
    public void start2Login() {
        ARouter.getInstance().build(ARouterPathConfig.LOGIN_START)
                .navigation(mContext, new NavigationCallback() {
                    @Override
                    public void onFound(Postcard postcard) {
                        LogUtils.d("onFound");
                    }

                    @Override
                    public void onLost(Postcard postcard) {
                        LogUtils.d("onLost");
                    }

                    @Override
                    public void onArrival(Postcard postcard) {
                        LogUtils.d("onArrival");
                    }

                    @Override
                    public void onInterrupt(Postcard postcard) {
                        LogUtils.d("onInterrupt");

                    }
                });
    }



获取信息的组件:


  ILoginProvider iLoginProvider = (ILoginProvider) 
   ARouter.getInstance().build(ARouterPathConfig.SERVICE_LOGIN).navigation();
   //获取登录名
   ToastUtils.showShort(
                        iLoginProvider.getLoginInfo().getName()
                );

组件之间的AOP切割

如登录跳转的地方需要判断用户登录等...

/**
 * <p>描述:(拦截器AOP切面拦截登录)</p>
 * Created by Cherish on 2018/8/20.<br>
 */
@Interceptor(priority = 1)
public class LoginInterceptor implements IInterceptor {

    private static final String TAG = "LoginInterceptor";

    private Context mContext;

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {

        Log.i(TAG, "LoginInterceptor 开始执行");

        if (postcard.getExtra() == 1) {//extras=1,目标页面标记,代表需要拦截处理

            boolean isLogin = BaseApplication.getInstance().isLogin();
            Log.i(TAG, "是否已登录: " + isLogin);
            //判断用户的登录情况,可以把值保存在sp中
            if (isLogin) {
                callback.onContinue(postcard);
            } else {
                callback.onInterrupt(null);
                ILoginProvider iLoginProvider = (ILoginProvider) ARouter.getInstance().build(ARouterPathConfig.SERVICE_LOGIN).navigation();
                iLoginProvider.start2Login();
            }
        } else {
            callback.onContinue(postcard);
        }

    }

    @Override
    public void init(Context context) {

        mContext = context;
        Log.i(TAG, "LoginInterceptor 初始化");

    }
}

组件之间的资源冲突

资源冲突其实很好解决,团队契约资源文件的命名,如Login登录的注册注册界面命名:login_activity_register

其他类似文件均按照login_开头。可强行控制某一module的资源命名

resourcePrefix "login_"

组件统一使用的版本控制

创建单独的config.gralde来让其他模块组件引用

config.gralde:

ext {

    app = [
            packageName: "com.chips.client",
    ]
    defaultConfig = [
            compileSdkVersion: 27,
            buildToolsVersion: "27.0.0",
            minSdkVersion    : 19,
            targetSdkVersion : 9,
            versionCode      : 1,
            versionName      : '1.0.0',
    ]


    dependencies = [
            appcompatV7                      : 'com.android.support:appcompat-v7:27.0.1',
            design                           : 'com.android.support:design:27.0.1',
            constraintLayout                 : 'com.android.support.constraint:constraint-layout:1.0.2',
            quickFragment                    : "com.allure0:QuickFragment:1.0.2",//Fragment框架
            //跳转路由Router
            arouter_api :'com.alibaba:arouter-api:1.3.1',
            arouter_compiler :'com.alibaba:arouter-compiler:1.1.4',
            //汉字转拼音
            tinyPinyin:'com.github.promeg:tinypinyin:2.0.3',
            //Loading content error empty等状态页
            loadsir: 'com.kingja.loadsir:loadsir:1.3.6',
            baseRecycleViewAdapter:  'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
    ]
}

组件模块引用:

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

    api project(':common')

    api rootProject.ext.dependencies.appcompatV7
    api 'com.android.support.constraint:constraint-layout:1.1.2'
    api 'com.blankj:utilcode:1.17.3'
    //ARouter
    implementation rootProject.ext.dependencies.arouter_api
    annotationProcessor rootProject.ext.dependencies.arouter_compiler
}

组件化解放双手Gradle插件

对于以上的组件化难点我们解决之后,实际上都是通过每次手动的sync来完成,其实完全可以利用apt/Gradle插件等技术来完成组件之间的自动切换。

Groovy实现插件:


class AppConfigExt {

    boolean isDebug = false
    NamedDomainObjectContainer<AppExt> apps
    NamedDomainObjectContainer<LibraryExt> modules

    AppConfigExt(Project project){
        apps = project.container(AppExt)
        modules = project.container(LibraryExt)
    }

    def isDebug(boolean isDebug){
        this.isDebug = isDebug
    }

    def apps(Closure closure){
        apps.configure(closure)
    }


    def modules(Closure closure){
        modules.configure(closure)
    }

    @Override
    String toString() {
        return "isDebug: $debugEnable\n" +
                "apps: ${apps.isEmpty()? "is empty" : "$apps"}"+
                "modules: ${modules.isEmpty()? "is empty" : "$modules"}"
    }
}

在AppConfig中定义了启动模式与宿主壳App和组件的配置。

剩余代码不贴了....直接贴使用方式

插件使用方式

Step1:整个项目之下添加以下代码,以下地址为本地Maven地址,后续会上传到Jcenter/Maven
apply plugin: 'com.allure.appconfig'

buildscript {
    repositories {
        maven {//本地Maven仓库地址
            url uri('/Users/mac/Downloads/ttt')
        }
    }
    dependencies {
        //格式为-->group:module:version
        classpath 'com.allure.plugin:Component:1.0.0'
    }
}

hostAppConfig {
    isDebug true

    //宿主载体
    apps {
        app {

            mainActivity "com.allure.modularization.SplashActivity"
            modules ':modules:login',
                    ':modules:shop',
                    ':modules:main'
        }
    }

//组件
    modules {
        login {
            isRunAlone true
            name ":modules:login"
            applicationId "com.allure.login"
            mainActivity ".LoginActivity"

        }
        shop {
            isRunAlone false
            name ":modules:shop"
            applicationId "com.allure.shop"
            mainActivity ".ShopActivity"

        }
        main {
            isRunAlone false
            name ":modules:main"
            applicationId "com.allure.main"
            mainActivity ".MainActivity"

        }
    }
}
Step2:在每一个组件的build.gradle下定义插件的引用
apply plugin: 'com.allure.appmodules'

插件解释

hostAppConfig:
hostAppConfig 解释
isDebug 是否开启debug模式,只有当isDebug为true时,modules的isRunAlone才能生效。
apps 壳工程列表,可以有多个壳工程
modules 各个组件Lib
app:
app 解释
modules 依赖的组件列表
applicationId 启动的application,可默认为空,在测试微信支付分享时可以动态配置使用测试
mainActivity 启动Activity
modules:
app 解释
name 依赖的组件列表
isRunAlone 是否可以单独运行(必须在isDebug=true情况下可运行)
applicationId 启动的application,必须设置
mainActivity 启动Activity

自此,我们可以脱离手动的控制组件的applictaion和lib模式,让插件帮我们完成。

项目结构

image.png

组件化总结

  • 减小业务与业务之间的耦合
  • 组件模式下可以加快编译速度,提高开发效率;
  • 自由选择开发框架(MVC /MVP / MVVM /);
  • 方便做单元测试;
  • 代码架构更加清晰,降低项目的维护难度;
  • 适合于团队开发;
  • 更契合面向对象思想
  • 利于整体项目进度推动,减少团队部门交叉出错问题
  • 职责清晰,定位更准
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,499评论 25 707
  • 这个城很小,出生后14年这个女孩都没有闯进她的世界,明明住得那么近
    野熙xi阅读 245评论 0 0
  • 开始喜欢油画 开始喜欢油画里的 氛围 生机勃勃的 生活表达 可是跟大自然 比起来 它只是一簇花 一枝叶 一条街道 ...
    粒粒蓝雪阅读 215评论 0 0
  • 房梁皓月白雪寒 雁门浮诛夜深寒 西湖舟亭楼阁寒 醉卧闺榻床被寒 一思一见一情终 几生几世几浮图 落梅静开,浅枝香来...
    莘九阅读 206评论 0 0