Android进阶知识树——必须会的组件化技术

一、概述

笔者从事智能家具行业的开发工做,也是从公司创业团队工做到如今,对于公司的项目从1.0版本开始接手一直到如今,虽然说项目不是很大但麻雀虽小五脏俱全,在项目和团队的不断扩大、暴露出的问题也不段增多,组件化势在必行,本文就根据整个项目的发展,总结下组件化的实践流程;java

1.0版本
在最初的1.0版本中只是针对一个智能设备的操控和数据交互,项目自己就很简单此时也基本单人开发,因此全部的功能代码都直接在app中开发,但随着业务的增加和对将来的规划,项目进入2.0阶段
在这里插入图片描述
2.0阶段的业务比1.0增长了电商、社区、内容等业务模块,同时智能设备也由原来的单一设备变成多个设备,此时若是只在app中开发,会致使单个Module中代码急剧膨胀,代码耦合度高,并且业务增多后团队面临扩张,此时业务模块之间的耦合,在多人协做开发时也暴露出来,并且因为行业的需求有时会有临时的Demo和定制化的应用,在原来的项目上很难实现这些需求,此时必须对原来的项目代码进行组件化操做;

二、组件化基础

在进行组件化操做以前,先区分两个概念:模块化和组件化android

  • 组件化:单一的功能组件,要求能独立开发而且脱离业务程序,实现组件的复用,如:蓝牙组件、音乐组件
  • 模块化:模块化主要针对业务,将单独的业务功能分离开发,每一个功能模块之间进行代码解耦,在编译时能够自由的添加或减小模块,如:社区模块、电商模块等

由上面的介绍知道,组件化针对更细更单一的业务,功能模块粒度较大,针对某个方面的总体业务,固然业务当中可能使用不少的独立组件,按照组件化的需求项目的架构进入3.0 网络

在这里插入图片描述
上面已智能、内容两个模块为例,在项目组件化操做后的架构图,架构从下向上依次为:

  • 基础层:主要封装经常使用的工具类和一些封装的基础库,如数据存储、网络请求等
  • 组件层:针对单一的供分离解耦出独立的功能组件
  • 业务模块层:针对独立相近的业务模块进行分离,根据各自的需求引入相应的功能组件
  • APP层:APP层为项目的最顶层,将全部的功能模块组合在APP框架中实现真个APP编译

三、组件化

由上面的3.0版本架构知道,项目中包含多个功能组件和业务模块,在开发中要保证组件间不能耦合,业务木块依赖于组件,但业务模块之间也不能相互引用,不然违背了组件化的原则;架构

  • 组件化的最终目的
  1. 实现组件间、模块间的代码解耦和代码隔离,减小项目的维护成本
  2. 实现组件的复用
  3. 实现功能组件和业务模块的单独调试和总体编译,减小项目的开发编译时间
  • 组件化要解决的问题
  1. 实现组件既能单独编译也能总体编译,缩短程序的编译时间
  2. 组件和Module中如何动态配置Application
  3. 组件间的数据传递
  4. 组件和模块间的界面跳转
  5. 主项目与业务模块间的解耦,从而实现增长和删除模块
    在这里插入图片描述
3.一、组件的单独调试
  • 在Android开发中,Gradle提供三种构建形式:
  1. App 插件,id: com.android.application
  2. Library 插件,id: com.android.libray
  3. Test 插件,id: com.android.test

在咱们实际开发中app 构建形式为application,最终编译成APK文件,其他所依赖的Module编译形式为library,最终已arr形式寻在提供API调用,换句话说只要修改组件的编译形式便可实现单独编译的功能,因此在组件下建立gradle.properties文件用于控制构建形式app

isRunAlone = false
复制代码

在build.gradle中根据isRunAlone的变量修改构建形式框架

if (isRunAlone.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
复制代码
  • 配置applicationId
if (isRunAlone.toBoolean()) {
            applicationId "com.alex.kotlin.content"
        }
复制代码
  • 配置AndroidManifest文件

在组件化单独编译和总体编译时,注册清单中所须要的内容不一样,如单独编译须要额外的启动页,且单独编译时也休要配置不一样的Application,此时在main文件加下建立manifest/AndroidMenifest.xml文件,根据单独编译的须要设置内容。ide

  1. 总体编译
    在这里插入图片描述
  2. 单独编译
    在这里插入图片描述
  3. 在build.gradle中配置不一样的文件路径
sourceSets {
        main {
            if (isRunAlone.toBoolean()) {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
复制代码

到此编译配置完成,在须要单独编译时只须要修改isRunAlone为true便可;模块化

3.二、组件动态初始化Application

由上面配置的两个注册清单文件中可见,在App总体编译时组件使用的是全局的Application,在单独编译时使用的是AutoApplication,你们都知道一个程序中只有一个Application类,那组件中须要初始化的代码都配置在本身的AutoApplication中,那总体编译时如何初始化呢?可能有同窗说总体编译时个组件和模块是可见的,直接调用AutoApplication类完成初始化,但此种状况主项目就没法实现模块的自由增减,并且当代码隔离时AutoApplication就不可见了,这里采用一种配置+反射的方式温馨化各组件的Application,具体实现以下:工具

  • 在base组件中声明BaseApp抽象类,BaseApp继承Application类
abstract class BaseApp : Application(){
    /**
     * 初始化Module中的Application
     */
    abstract fun initModuleApp(application: Application)
}
复制代码
  • 在组件中实现此BaseApp类,在initModuleApp()配置总体编译时时初始化的代码
class AutoApplication : BaseApp() {
    override fun onCreate() { //单独编译时初始化
        super.onCreate()
        MultiDex.install(this)
        AppUtils.setContext(this)
        initModuleApp(this)
        ServiceFactory.getServiceFactory().loginToService = AutoLoginService()
    }
    override fun initModuleApp(application: Application) { //总体编译
        ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
    }
}
复制代码
  • 在Base组件中建立AppConfig类,配置初始化时要加载的BaseApp的子类
object AppConfig {
    private const val BASE_APPLICATION = "com.pomelos.base.BaseApplication"
    private const val CONTENT_APPLICATION = "com.alex.kotlin.content.ContentApplication"
    private const val AUTO_APPLICATION = "com.alex.kotlin.intelligence.AutoApplication"

    val APPLICATION_ARRAY = arrayListOf(BASE_APPLICATION, CONTENT_APPLICATION, AUTO_APPLICATION)
}
复制代码
  • 在主Application中反射调用全部的Application
public class GlobalApplication extends BaseApp {
	@SuppressLint("StaticFieldLeak")
	private static GlobalApplication instance;
	public GlobalApplication() {}
	@SuppressWarnings("AlibabaAvoidManuallyCreateThread")
	@Override
	public void onCreate() {
		super.onCreate();
		MultiDex.install(this);
		AppUtils.setContext(this);
		if (BuildConfig.DEBUG) {
			//开启Debug
			ARouter.openDebug();
			//开启打印日志
			ARouter.openLog();
		}
		//初始化ARouter
		ARouter.init(this);
		ServiceFactory.Companion.getServiceFactory().setLoginToService(new AppLoginService());
		//初始化组件的Application
		initModuleApp(this);
	}
	@Override
	public void initModuleApp(@NotNull Application application) {
		for (String applicationName : AppConfig.INSTANCE.getAPPLICATION_ARRAY()) { //遍历全部配置的Application
			try {
				Class clazz = Class.forName(applicationName); //反射执行
				BaseApp baseApp = (BaseApp) clazz.newInstance(); //建立实例
				baseApp.initModuleApp(application); // 执行初始化
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} 
		}
	}
}
复制代码

以上经过在AppConfig中配置全部的Application的路径,在主Application执行时反射建立每一个实例,调用对应的initModuleApp()完成全部的配置,不知有没有注意到在AutoApplication中一样在onCreate()中初始化了内容,此处是为了在单独编译时调用;源码分析

3.三、组件间的数据传递

在项目中由于有时须要打包不一样需求的APK,因此我将login单独分离出成组件同一登陆行为,那么在特务模块依赖Login以后便可实现登陆功能,但每一个单独的业务独立编译时会产生多个APK,这些APK都须要获取登陆状态及跳转相应的首界面,那么在保证程序解耦的状况下如何实现呢?答案及时使用注册接口实现;

  1. 在Base组件中声明LoginToService接口
interface LoginToService {
    /** * 实现登陆后的去向 */
    fun goToSuccess()
}
复制代码
  1. 在base中建立ServiceFactory,同时单例对外提供调用
class ServiceFactory private constructor() {
    companion object {
        fun getServiceFactory(): ServiceFactory {
            return Inner.serviceFactory
        }
    }
    private object Inner {
        val serviceFactory = ServiceFactory()
    }
}
复制代码
  1. 在ServiceFactory中声明LoginToService对象,同时提供LoginToService的空实现
var loginToService: LoginToService? = null
        get() {
            if (field == null) {
                field = EmptyLoginService()
            }
            return field
        }
复制代码
  1. 在对应的业务模块中实现LoginToService,重写方法设置须要跳转的界面
class AppLoginService : LoginToService { //App模块
    override fun goToSuccess() {
        val intent = Intent(AppUtils.getContext(), MainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}

class AutoLoginService : LoginToService { // 智能模块
    override fun goToSuccess() {
        val intent = Intent(AppUtils.getContext(), AutoMainActivity::class.java)
        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
        AppUtils.getContext().startActivity(intent)
    }
}
复制代码
  1. 在初始化Application中向ServiceFactory注册各自的实例
ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
复制代码
  1. 在login组件中完成登陆后便可调用ServiceFactory中注册对象的方法实现跳转
override fun loadSuccess(loginBean: LoginEntity) {
        ServiceFactory.getServiceFactory().loginToService?.goToSuccess()
    }
复制代码

各组件经过向base组件中的ServiceFactory注册的方式,对外提供执行的功能,由于ServiceFactory单例调用,因此在其余组件中经过ServiceFactory获取注册的实例后便可执行方法,为了在减去组件或模块时防止报错,在base中一样提供了服务的空实现;

3.四、组件间的界面跳转

关于页面跳转推荐使用阿里的ARoute框架,详情见另外一篇文章:Android框架源码分析——以Arouter为例谈谈学习开源框架的最佳姿式

3.五、主项目与业务模块间的解耦

在通常项目中,主app的首界面都来自不一样的业务模块组成,最多见的就是使用不一样组件的Fragment和ViewPager组合,但此时主App须要获取组件中的Fragment实例,按照组件化的思想不能直接使用,不然主APP和组件、模块间又会耦合在一块儿,此处也是采用接口模式处理,过程和数据交互大体相同;

  • 在base组件中声明接口,在对应的模块中实现接口
interface ContentService {
    /** * 返回实例化的Fragment */
    fun newInstanceFragment(): BaseCompatFragment?
}
// 内容模块实现
class ContentServiceImpl : ContentService {
    override fun newInstanceFragment(): BaseCompatFragment? {
        return ContentBaseFragment.newInstance() //提供Fragment对象
    }
}
复制代码
  • 在初始化Application过程当中注册服务
ServiceFactory.getServiceFactory().serviceContent = ContentServiceImpl()
复制代码
  • 在主App中经过ServiceFactory获取
mFragments[SECOND] = ServiceFactory.getServiceFactory().serviceContent?.newInstanceFragment()
复制代码
3.六、其余问题
  • 代码隔离

虽然经历组件化将代码解耦,但在开发中若是依赖的组件或模块中的方法老是可见,万一在开发中使用了其中的代码,那程序程序又会耦合在一块儿,如何能让组件和模块中的方法不可见呢?答案就在runtimeOnly依赖,他能够在开发过程当中隔离代码,在编译时代码可见

runtimeOnly project(':content') runtimeOnly project(':intelligence') 复制代码
  • 资源隔离

runtimeOnly依赖实现了代码隔离,但对资源并无效果,使用中仍是可能会直接引用资源,为了防止这种现象,为每一个组件的资源加上特有的前缀

resourcePrefix "auto_"
复制代码

此时该Module下的资源都必须以auto_开头不然会警告;

在这里插入图片描述

  • ContentProvider

因为项目中使用到了ContentProvider,(不了解的点击Android进阶知识树——ContentProvider使用和工做过程详解)在总体编译安装在手机后能够正常运行,此时要单独编译时老是提示安装失败,最终缘由就是两个Apk中的ContentProvider和权限一致致使,那如何保证单独编译和总体编译时权限不一样,从而安装成功呢?咱们首先在上面的连个Menifest文件中配置Provider

  • 单独编译
<provider
            android:name=".database.MyContentProvider"
            android:authorities="com.alex.kotlin.intelligence.database.MyContentProvider"
            android:exported="false" />
复制代码
  • 总体编译
<provider
            android:name=".database.MyContentProvider"
            android:authorities="com.findtech.threePomelos.database.MyContentProvider"
            android:exported="false" />
复制代码

这样两个权限不一样的Provider便可安装成功,在使用时须要根据权限执行ContentProvider,那么如何在代码中根据不一样编译类型,拼接对应的执行权限呢?此处使用在build.gradle中配置BuildConfig来处理,将权限直接配置在BuildConfig中,在使用时直接获取便可

if (isRunAlone.toBoolean()) {
            buildConfigField 'String','AUTHORITY','"com.alex.kotlin.intelligence.database.MyContentProvider"'
        }else {
            buildConfigField 'String','AUTHORITY','"com.findtech.threePomelos.database.MyContentProvider"'
        }
        
   const val AUTHORITY = BuildConfig.AUTHORITY //使用
复制代码

四、总结

解决上面的全部问题后,项目的组件化基本能够实现,但具体的划分粒度和细节,须要自身结合业务和经验去处理,可能有些须要直接分离组件,也可能小的功能须要放在base组件中共享,并且每一个人针对每一个项目的处理方式也不一样,只要理解组件化的思想和方式实现最终的需求便可;

相关文章
相关标签/搜索