个人 Android 组件化之路

结构图

PandaMvp 组件化结构图

其中路由数据组件为上层业务组件必需要依赖的库,独立功能组件和公共 UI 组件能够根据需求选择是否依赖。公共 UI 组件为应用总体 UI 风格上的公共配置和封装,通常业务组件也都会依赖。基础SDK 为最底层的 SDK 库,全部的业务组件都基于它。顶层的业务 APP 通常按功能模块进行划分譬如:邮件 AppIM App视频 Appjava

为何要作组件化

1、作组件化主要是随着软件的版本迭代,暴露出一个巨大的问题。同一个 module 下,各类数据跳转之间高度的耦合了,虽然开发要求要注意代码的耦合度,但团队中每一个人的经验水平和编码风格都不同,对这个耦合程度的理解和标准也不同,随着时间推移模块间的代码会越写相互依赖程度越大。毕竟有时候明明能直接拿过来用,就不会太多的去考虑设计模式。作组件化将相对独立的模块独立出去,达到硬性代码隔离,强制下降模块耦合度的目的。 2、项目随着开发需求的不断迭代会变得愈来愈庞大,开发过程当中项目整编是个很费时的事,组件化以后能够灵活配置选择须要的组件编译,缩短期 3、多个项目中有的组件是能够共用的,像我经历过的两个项目的网盘模块和邮件模块。未采用组件化方案,移代码移资源太费时费力了。采用组件化方案,直接将 module 导入新的项目,增长对应的路由和 路由Service 方法就能用(前提是项目都采用组件化方案)android

组件化过程当中的几个问题

多个组件 module 怎样共用 Application

Application 代理类生命周期接口

一、在 BaseApplication 中建立 AppProxy 类,(这个类是 IAppLifeCycle 的一个实现类)。在 BaseApplication 的生命周期方法中调用 AppProxy 的生命周期方法 二、AppProxy 构造函数中扫描 Manifest 文件,扫描类中经过反射拿到每一个组件中的实现类。将这些实现类添加到 AppProxy 中的列表中。 三、在生命周期方法中循环第二步中的列表调用列表内各个 module 注入的生命周期代理对象的对应方法 核心处理即在 AppProxy 类中:git

AppProxy 生命周期

各个 module 的代理实现类必定要注册到 manifest 中,不然会扫描不到github

<!--配置 Application-->
        <meta-data
            android:name="com.pandaq.pandamvp.app.lifecycle.LifeCycleInjector"
            android:value="AppInjector"/>
复制代码

这样配置以后咱们是没办法手动控制 module 生命周期方法的调用顺序的,所以在 LifeCycleInjector 中增长了优先级选项,默认为 0,数字越大越延后加载shell

/** * priority for lifeCycle methods inject * * @return priority 0 for first */
    int priority();
复制代码

Activity 及 Fragment 生命周期

  • Activity:如上图中,与 Application 生命周期注入对应,在 AppProxy 的 onCreate() 方法中将 Activity 生命周期回调注册到 application 中。经过 Application 来管理 Activity 生命周期。
@Override
    public void onCreate(@NonNull Application application) {
        for (IAppLifeCycle appLifeCycle : mAppLifeCycles) {
            appLifeCycle.onCreate(application);
        }
		// 注册各个 module activity 生命周期回调方法 
        for (Application.ActivityLifecycleCallbacks callbacks : mActivityLifeCycles) {
            application.registerActivityLifecycleCallbacks(callbacks);
        }

    }
	
	@Override
    public void onTerminate(@NonNull Application application) {
        if (mAppLifeCycles != null) {
            for (IAppLifeCycle appLifeCycle : mAppLifeCycles) {
                appLifeCycle.onTerminate(application);
            }
        }
		// app 生命周期结束时注销 activity 生命周期回调 
        if (mActivityLifeCycles != null) {
            for (Application.ActivityLifecycleCallbacks callbacks : mActivityLifeCycles) {
                application.unregisterActivityLifecycleCallbacks(callbacks);
            }
        }
        mAppLifeCycles = null;
        mActivityLifeCycles = null;
        mFragmentLifecycleCallbacks = null;
        AppUtils.release();
    }
复制代码
  • Fragment : 各module 内与 Activity 的生命周期注入同样,经过 ILifecycleInjector 的实现类,将 Fragment 生命周期实现类添加到注入列表中,但在 AppProxy 中处理再也不是经过注册到 Application 来管理,而是经过一个默认的 Activity生命周期实现类,将这些 fragment 生命周期回调类统一注册到 FragmentManager 中
private void registerFragmentCallbacks(Activity activity) {
        //注册框架外部, 开发者扩展的 BaseFragment 生命周期逻辑
        for (FragmentManager.FragmentLifecycleCallbacks fragmentLifecycle : mFragmentLifeCycles) {
            if (activity instanceof FragmentActivity) {
                ((FragmentActivity) activity).getSupportFragmentManager().registerFragmentLifecycleCallbacks(fragmentLifecycle, true);
            }
        }
    }
复制代码

描述可能不太容易看懂,具体的代码能够参考 PandaMvp数据库

组件间的通讯

组件中的通讯这里采用了 ARouter,具体使用这里不展开,直接去看 ARouter 的文档,几个关键点: 1、页面跳转:.设计模式

@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}
复制代码
ARouter.getInstance().build("/test/activity")
            .withLong("key1", 666L)
            .withString("key3", "888")
            .withObject("key4", new Test("Jack", "Rose"))
            .navigation();
复制代码

2、页面值的回传:缓存

// 构建标准的路由请求,startActivityForResult
// navigation的第一个参数必须是Activity,第二个参数则是RequestCode
ARouter.getInstance().build("/home/main").navigation(this, 5);
复制代码

3、Fragment 发现:app

// 获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
复制代码

4、跨组件方法调用:框架

// 声明接口,其余组件经过接口来调用服务, router 组件中定义
public interface EmailService extends IProvider {
    EmailAccount getAccount();
}

// 实现接口对应的业务组件中实现
@Route(path = "/email/emailservice")
public class EmailServiceImpl implements EmailService {

    @Override
    public EmailAccount getAccount() {
    return new EmailAccount();
    }

    @Override
    public void init(Context context) {

    }
}
复制代码
// 调用组件中发现服务再调用方法
public class Test {
    @Autowired(name = "/email/emailservice")
    EmailService emailService;

    public Test() {
    	ARouter.getInstance().inject(this);
		EmailAccount account = emailService.getAccount();
    }

}
复制代码

为避免书写错误等问题,最好定义常量类统一管理路由的 path 并为每一个组件使用不一样的父路径分组

组件间数据实体共享

独立组件间的数据获取传递都经过 Arouter 的服务来完成:

router 组件文件夹结构

如图所示,每个独立的业务 module 在 router module 下都有一个本身的文件夹。以 email 组件为例将,其余的业务组件须要获取邮件组件中用户邮件的帐号信息和邮件签名,则 email 将本身的帐号信息和签名信息类 MailAccountMailSign 注册到 router module 中,这样其余组件就能经过对 router module 的依赖识别这两个数据类。A 组件须要从 Email 组件获取 MailAccount。首先 Email 组件要在 router 中的服务 EmailService 接口中注册对外暴露 getAccount() 方法,若是获取为异步的,则还须要再 callbacks 中注册一个回调方法。A 中经过接口回调异步拿到 EmailService 给他的数据。

ORM 数据库的增删改查

ORM 数据库,项目采用的是 GreenDao3.0。由于 GreenDao 的初始化和 Tab生成不能跨 module,因此在存储数据时有两种方案: 一、每一个业务 module 本身维护数据库。业务组件间须要通讯的数据再单首创建类下沉到一个公共库中去。这种方式能保证业务数据的彻底独立,但须要多写数据实体类 二、直接把数据实体类都放在一个公共库中,GreenDao 的初始化也放在这个库中。我在项目的实际操做中是将要存入数据库的实体类放入 Router module 中按文件夹分开存放的。 数据库的操做工具类定义在对应的组件内,如 Email 组件,其中的缓存表操做工具类叫 EmailTb,经过 EmailTb 的方法对数据库进行增删改查。Email 组件内部增删改查没有任何的阻碍隔离,若是 A组件须要对 Email 表进行增删改查,则须要经过 EmailService 中注册暴露的方法间接的增删改查。若是 Email 未注册暴露对应方法则其余组件不能对 Email 数据库操做

资源文件重名问题

resourcePrefix "a_"
复制代码

经过在 gradle 中配置 resourcePrefix 统一为资源文件添加前缀限制,在编译时命名不符合规范编译器将会提示错误。进行组件化改造时这是个体力活,说多了都是泪

module 组合运行

module 自由组合运行,则须要 module 既要有成为 application 的能力又要有做为 library 的能力。咱们经过 gradle.propertiesbuild.gradle 文件配置,经过脚本在编译时决定打包哪些业务组件 App 组成应用。

# 一、整编模式
# launchApp = app
# buildAll = true
#
# 二、单组件调试
# launchApp = xx (组件module名字)
# buildAll = false
#
# 二、多组件调试
# launchApp = app (组件 module 名字)
# buildAll = false
# loadComponents = xx1,xx2 (须要联调的组件 module 名字)
#
# 打包容器 App module 名字
shellApp = app
# 被启动的业务组件的名字,打包发布时必定为外壳 APP
launchApp = app_bmodule
#是否整编 App,true 的时候会壳 App 打包会依赖 allComponents。false 打包会依赖 loadComponents
buildAll = false
# 全部业务组件 App 的 module 名字
allComponents = app_amodule,app_bmodule
# 多业务组件放入 shellApp 联合调试启用的 module 名字
loadComponents =
复制代码

公共的 build.gradle 中配置

// 根据配置是否为 launchApp 决定业务组件 module 是做为 library 仍是独立 App
boolean isShellApp = project.getName() == shellApp
boolean isLaunchApp = project.getName() == launchApp
if (isLaunchApp) { // 壳 APP 始终以 application 模式运行,其余业务组件以依赖库模式根据配置拔插
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
·
·
·
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
            if (isShellApp){ // 容器 App 只会以 App模式运行或者不运行
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }else {
			// application 模式和 library 模式的清单文件是不同的,这里根据 isLaunchApp 肯定使用哪个
                if (isLaunchApp) {
                    manifest.srcFile 'src/main/debug/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/release/AndroidManifest.xml'
                }
            }
        }
    }

复制代码

各业务组件 module 的,build.gradle:

·
·
·
    defaultConfig { //根据是否为 launchApp 决定添加 applicationId 和版本号 
        if (isLaunchApp) {
            applicationId "com.pandaq.app_amodule"
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
·
·
·
复制代码

容器 App module(主 module),相较于通常业务组件 module 的 build.gradle,还须要配置多组件打包和整编打包时动态依赖业务组件库:

·
·
·
    defaultConfig { //根据是否为 launchApp 决定添加 applicationId 和版本号 
        if (isLaunchApp) {
            applicationId "com.pandaq.app_amodule"
            versionCode 1
            versionName "1.0"
            testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        }
    }
·
·
·
dependencies{
		·
		·
		·
	// 按需加载依赖业务 APP
    if (buildAll.toBoolean()) { //整编时将业务组件所有添加依赖
        for (String name : allComponents.split(",")) {
            implementation(project(":$name")) {
                exclude group: 'com.android.support'
                exclude module: 'appcompat-v7'
                exclude module: 'support-v4'
            }
        }
    } else { //非整编时能够选择业务组价加入容器 App
        for (String name : loadComponents.split(",")) {
            if (!name.isEmpty()) {
                implementation(project(":$name")) {
                    exclude group: 'com.android.support'
                    exclude module: 'appcompat-v7'
                    exclude module: 'support-v4'
                }
            }
        }
    }
}
复制代码

其余思考

组件化有风险,推动需谨慎。一个非组件化的大型项目要对其进行组件化改造这个过程是漫长而艰巨的,项目中各个模块不可避免的会有各类耦合关系,每每牵一发而动全身,要对它进行组件化改造。首先要对项目进行封装解耦,独立的功能该下沉的下沉,该重写的重写。有时候代码的复用对组件化改造简直是灾难,尤为是原本不属于一个功能模块的界面进行了复用这种。

相关文章
相关标签/搜索