android studio创建一个类继承application_Android进阶知识树——必须会的组件化技术...

作者:Alex@W  
来源:https://juejin.im/post/5d7a0d065188250802047631

1、概述

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

3835fdbadb4d5ccc7e6422c2cd47e7fa.png
1.0版本

在最初的1.0版本中只是针对一个智能设备的操控和数据交互,项目本身就很简单此时也基本单人开发,所以所有的功能代码都直接在app中开发,但随着业务的增长和对未来的规划,项目进入2.0阶段

1f8d4ce0cc4e01092d554eafe757f4f7.png
在这里插入图片描述

2.0阶段的业务比1.0增加了电商、社区、内容等业务模块,同时智能设备也由原来的单一设备变成多个设备,此时如果只在app中开发,会导致单个Module中代码急剧膨胀,代码耦合度高,而且业务增多后团队面临扩张,此时业务模块之间的耦合,在多人协作开发时也暴露出来,而且由于行业的需求有时会有临时的Demo和定制化的应用,在原来的项目上很难实现这些需求,此时必须对原来的项目代码进行组件化操作;

2、组件化基础

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

  • 组件化:单一的功能组件,要求能独立开发并且脱离业务程序,实现组件的复用,如:蓝牙组件、音乐组件

  • 模块化:模块化主要针对业务,将单独的业务功能分离开发,每个功能模块之间进行代码解耦,在编译时可以自由的添加或减少模块,如:社区模块、电商模块等

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

93c9a278e1ed5f65df5225d35799e867.png
在这里插入图片描述
上面已智能、内容两个模块为例,在项目组件化操作后的架构图,架构从下向上依次为:
  • 基础层:主要封装常用的工具类和一些封装的基础库,如数据存储、网络请求等

  • 组件层:针对单一的供分离解耦出独立的功能组件

  • 业务模块层:针对独立相近的业务模块进行分离,根据各自的需求引入相应的功能组件

  • APP层:APP层为项目的最顶层,将所有的功能模块组合在APP框架中实现真个APP编译

3、组件化

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

  • 组件化的最终目的

  1. 实现组件间、模块间的代码解耦和代码隔离,减少项目的维护成本

  2. 实现组件的复用

  3. 实现功能组件和业务模块的单独调试和整体编译,减少项目的开发编译时间

  • 组件化要解决的问题

  1. 实现组件既能单独编译也能整体编译,缩短程序的编译时间

  2. 组件和Module中如何动态配置Application

  3. 组件间的数据传递

  4. 组件和模块间的界面跳转

  5. 主项目与业务模块间的解耦,从而实现增加和删除模块

    727d3aca92a83719074d77ea9469d3de.png
    在这里插入图片描述
3.1、组件的单独调试

在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文件用于控制构建形式

1isRunAlone = false

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

1if (isRunAlone.toBoolean()) {
2    apply plugin: 'com.android.application'
3} else {
4    apply plugin: 'com.android.library'
5}

配置applicationId

1    if (isRunAlone.toBoolean()) {
2            applicationId "com.alex.kotlin.content"
3        }

配置AndroidManifest文件

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

  1. 整体编译

    f34fd7566d596a75cce37d7bd8a51515.png
    在这里插入图片描述
  2. 单独编译

    e5139eae7cc6944ecec7f0c71e990ff3.png
    在这里插入图片描述
  3. 在build.gradle中配置不同的文件路径

1sourceSets {
2            main {
3                if (isRunAlone.toBoolean()) {
4                    manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
5                } else {
6                    manifest.srcFile 'src/main/AndroidManifest.xml'
7                }
8            }
9        }

到此编译配置完成,在需要单独编译时只需要修改isRunAlone为true即可;

3.2、组件动态初始化Application

由上面配置的两个注册清单文件中可见,在App整体编译时组件使用的是全局的Application,在单独编译时使用的是AutoApplication,大家都知道一个程序中只有一个Application类,那组件中需要初始化的代码都配置在自己的AutoApplication中,那整体编译时如何初始化呢?可能有同学说整体编译时个组件和模块是可见的,直接调用AutoApplication类完成初始化,但此种情况主项目就无法实现模块的自由增减,而且当代码隔离时AutoApplication就不可见了,这里采用一种配置+反射的方式舒适化各组件的Application,具体实现如下:

在base组件中声明BaseApp抽象类,BaseApp继承Application类

1abstract class BaseApp : Application(){
2    /**3     * 初始化Module中的Application4     */
5    abstract fun initModuleApp(application: Application)
6}

在组件中实现此BaseApp类,在initModuleApp()配置整体编译时时初始化的代码

 1class AutoApplication : BaseApp() {
2    override fun onCreate() { //单独编译时初始化
3        super.onCreate()
4        MultiDex.install(this)
5        AppUtils.setContext(this)
6        initModuleApp(this)
7        ServiceFactory.getServiceFactory().loginToService = AutoLoginService()
8    }
9    override fun initModuleApp(application: Application) { //整体编译
10        ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()
11    }
12}

在Base组件中创建AppConfig类,配置初始化时要加载的BaseApp的子类

1object AppConfig {
2    private const val BASE_APPLICATION = "com.pomelos.base.BaseApplication"
3    private const val CONTENT_APPLICATION = "com.alex.kotlin.content.ContentApplication"
4    private const val AUTO_APPLICATION = "com.alex.kotlin.intelligence.AutoApplication"
5
6    val APPLICATION_ARRAY = arrayListOf(BASE_APPLICATION, CONTENT_APPLICATION, AUTO_APPLICATION)
7}

在主Application中反射调用所有的Application

 1public class GlobalApplication extends BaseApp {
2    @SuppressLint("StaticFieldLeak")
3    private static GlobalApplication instance;
4    public GlobalApplication() {}
5    @SuppressWarnings("AlibabaAvoidManuallyCreateThread")
6    @Override
7    public void onCreate() {
8        super.onCreate();
9        MultiDex.install(this);
10        AppUtils.setContext(this);
11        if (BuildConfig.DEBUG) {
12            //开启Debug
13            ARouter.openDebug();
14            //开启打印日志
15            ARouter.openLog();
16        }
17        //初始化ARouter
18        ARouter.init(this);
19        ServiceFactory.Companion.getServiceFactory().setLoginToService(new AppLoginService());
20        //初始化组件的Application
21        initModuleApp(this);
22    }
23    @Override
24    public void initModuleApp(@NotNull Application application) {
25        for (String applicationName : AppConfig.INSTANCE.getAPPLICATION_ARRAY()) { //遍历所有配置的Application
26            try {
27                Class clazz = Class.forName(applicationName); //反射执行
28                BaseApp baseApp = (BaseApp) clazz.newInstance(); //创建实例
29                baseApp.initModuleApp(application); // 执行初始化
30            } catch (ClassNotFoundException e) {
31                e.printStackTrace();
32            } 
33        }
34    }
35}

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

3.3、组件间的数据传递

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

1)在Base组件中声明LoginToService接口

1interface LoginToService {
2    /**3     * 实现登录后的去向4     */
5    fun goToSuccess()
6}

2)在base中创建ServiceFactory,同时单例对外提供调用

 1class ServiceFactory private constructor() {
2    companion object {
3        fun getServiceFactory(): ServiceFactory {
4            return Inner.serviceFactory
5        }
6    }
7    private object Inner {
8        val serviceFactory = ServiceFactory()
9    }
10}

3)在ServiceFactory中声明LoginToService对象,同时提供LoginToService的空实现

1var loginToService: LoginToService? = null
2        get() {
3            if (field == null) {
4                field = EmptyLoginService()
5            }
6            return field
7        }

4)在对应的业务模块中实现LoginToService,重写方法设置需要跳转的界面

 1class AppLoginService : LoginToService { //App模块
2    override fun goToSuccess() {
3        val intent = Intent(AppUtils.getContext(), MainActivity::class.java)
4        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
5        AppUtils.getContext().startActivity(intent)
6    }
7}
8
9class AutoLoginService : LoginToService { // 智能模块
10    override fun goToSuccess() {
11        val intent = Intent(AppUtils.getContext(), AutoMainActivity::class.java)
12        intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
13        AppUtils.getContext().startActivity(intent)
14    }
15}

5)在初始化Application中向ServiceFactory注册各自的实例

1ServiceFactory.getServiceFactory().serviceIntelligence = AutoIntelligenceService()

6)在login组件中完成登录后即可调用ServiceFactory中注册对象的方法实现跳转

1override fun loadSuccess(loginBean: LoginEntity) {
2        ServiceFactory.getServiceFactory().loginToService?.goToSuccess()
3    }

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

3.4、组件间的界面跳转

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

3.5、主项目与业务模块间的解耦

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

在base组件中声明接口,在对应的模块中实现接口

 1interface ContentService {
2        /** 3         * 返回实例化的Fragment 4         */
5        fun newInstanceFragment(): BaseCompatFragment?
6    }
7    // 内容模块实现
8    class ContentServiceImpl : ContentService {
9        override fun newInstanceFragment(): BaseCompatFragment? {
10            return ContentBaseFragment.newInstance() //提供Fragment对象
11        }
12    }

在初始化Application过程中注册服务

1   ServiceFactory.getServiceFactory().serviceContent = ContentServiceImpl()

在主App中通过ServiceFactory获取

1  mFragments[SECOND] = ServiceFactory.getServiceFactory().serviceContent?.newInstanceFragment()
3.6、其他问题
  • 代码隔离

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

1    runtimeOnly project(':content')
2    runtimeOnly project(':intelligence')
  • 资源隔离

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

1  resourcePrefix "auto_"

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

f9ca05be00d6fa9654cd32703b3ea073.png
在这里插入图片描述
  • ContentProvider

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

  • 单独编译

  • 整体编译

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

1 if (isRunAlone.toBoolean()) {
2            buildConfigField 'String','AUTHORITY','"com.alex.kotlin.intelligence.database.MyContentProvider"'
3        }else {
4            buildConfigField 'String','AUTHORITY','"com.findtech.threePomelos.database.MyContentProvider"'
5        }
6
7   const val AUTHORITY = BuildConfig.AUTHORITY //使用

4、总结

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

推荐阅读

干起来,你就超过了 50% 的人

自定义 behavior - 完美仿 QQ 浏览器首页,美团商家详情页

责任链模式以及在 Android 中的应用

一步步带你读懂 Okhttp 源码

Android 点九图机制讲解及在聊天气泡中的应用

扫一扫,欢迎关注我的微信公众号  stormjun94(徐公码字), 目前是一名程序员,不仅分享 Android开发相关知识,同时还分享技术人成长历程,包括个人总结,职场经验,面试经验等,希望能让你少走一点弯路。
46419f5006c3741f53e1ceb87a6884d7.png
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值