Android组件化开发

Android 组件化开发

模块化?组件化?插件化?

组件化和模块化

百度百科对组件化的解释:

解耦复杂系统时将多个功能模块拆分、重组的过程,有多种属性、状态反映其内部特性。

定义

组件化是一种高效的处理复杂应用系统,更好的明确功能模块作用的方式。

目的

为了解耦把复杂系统拆分成多个组件,分离组件边界和责任,便于独立升级和维护。

  • 对于模块组件,不同的人会有不同的理解,而大部门人认为是针对功能和业务不同的划分:

    • 模块:针对独立的业务模块,如首页模块,聊一聊模块,信息资讯模块,社区模块等

    • 组件:独立的功能性的组件,如视频播放组件,支付组件等


但是目的都是一样的解耦复杂的系统,明确划分各模块的职责界限。区别是一个以业务为导向,一个以功能为导向。

插件化

插件化也叫动态组件化

在Android中插件化可以理解为一项技术方案,同样使用插件化技术,将不同的组件或者模块,做成插件,动态加载,也可以满足组件化的需求,

组件化架构演进

对于早期的Eclipse项目,是没有组件化架构的概念的,只有一个Project。

Eclipse项目架构 Eclipse架构

AndroidStudio项目架构

AndroidStudio 使用了Project加多Module的方式进行项目管理,本身就是一种多模块化的开发,但是由于开发中,多模块之间产生依赖关系后 ,没有任务约束,相互的引用太多,就会变的难以维护,迭代越来越多,项目越来越复杂。

以下是一个相对较好的项目结构

  • Application层:包含业务相关的不同模块,以及一些同级的库

  • Base层:Base基类等

  • 框架层:主要统一管理依赖的框架以及封装基础库:图片框架,网络框架等,

AndroidStudio架构

使用AndroidStudio组件化架构

以微鲤看看为原型,简单对不同业务模块进行了划分,形成了一张组件化架构图

  • APP壳:没有实际的业务只作为了一个入口,同时也防止app依赖所有的组件后,没法解耦。

  • 组件层:包含项目的各个组件,组件之间相互独立,没有依赖关系,他们之间的通信调用是通过Service层的接口实现的,

  • 基础层:包含通信相关的Service,Base基类,以及一些基础功能组件

    组件化架构

组件化优缺点

优点

  • 抽取公共的功能组件,多个组件甚至项目共用,提高开发效率和节省开发成本与维护成本

  • 独立业务模块,可独立运行编译,多团队可以高效的并行开发与测试

  • 组件之间以接口对外通信,其他组件的替换,升级不会影响其他组件,也不会受其他组件的限制

缺点

  • 组件对需要的依赖度高,需求多变,可能根据业务划分的组件无法满足需求,组件做出相应的变化调整,过于复杂的业务调整会有些难度

  • 单个组件的编译速度有所提高,编译整个工程app,需要耗些时间

  • 组件化的实施对开发人员和团队管理人员提出了更高水平的要求,项目管理难度更大。组件间如何进行通信也是需要慎重考虑的。万事开头难,在对一个项目进行组件化分解时就好像庖丁解牛一般,你需要了解项目的“肌理筋骨”,才知道从何处下“刀”,才能更轻易的去分解项目,这就要求架构师对于项目的整体需求了如指掌

组件化开发

Android组件化开发其实就是解决如下问题的过程

  • 如何根据业务需求,或现有项目,进行拆分成不同模块

  • 如何隔离组件,并解决不相互依赖的组件之间的通信问题

  • 解决单个组件从可以独立编译运行的APP到依赖库的转换问题

  • 页面跳转问题,多个组件如何实现直接页面的跳转

实现方案

有3种

  • 接口编程

  • 路由框架

  • 组件化框架

一般都是混合实现,没有纯路由的组件化,这种方案会导致项目对路由框架有很大的依赖性。

Demo项目结构

以微鲤看看为原型,做了个组件化Demo项目结构如下

项目结构

  • App层 app 项目入口

  • 组件层

    • account_component 账户组件包含登录注册,账户信息

    • app_main_comp 主组件:主页HomeActivity,(这里主要考虑有的主页功能较为复杂,所以单独分出来)

    • im_component IM组件,IM相关

    • news_component 资讯组件

    • community_component 社区相关


  • base层

    • base 一些base基类

    • component_service所有组件通信的服务接口组件

    • common_library 一些三方框架,由于多个组件都会引用第三方库文件,所有用来统一管理依赖库,防止版本不统一造成一些其他问题


搭建流程

统一配置文件

  • 因为每个组件或者库文件,都会有依赖一些好的框架,这里为了方便统一管理新建config.build,来全局管理build.gradle的配置。

ext {  
    version = [
            'compileVersion'  : 28,
            'targetSdkVersion': 25,
            'minSdkVersion'   : 19,
            'versionCode'     : 1,
            'versionName'     : "1.0.0"
    ]
    dependencies = [
            'appcompat_v7'          : 'com.android.support:appcompat-v7:28.0.0',
            'butterKnife'           : "com.jakewharton:butterknife:$butterKnifeVersion",
            'butterKnife_annotation': "com.jakewharton:butterknife-   compiler:$butterKnifeVersion",
            'arouter_api'           : "com.alibaba:arouter-api:$arouter_api",
            "arouter_annotation"    : "com.alibaba:arouter-compiler:$arouter_annotation"
    ]
  // 该字段标识当前是集成模式(运行APP),还是调试模式(单个组件运行编译)
  isRunApp = true;
}

  • 为每个组件创建module.gradle配置文件,该文件是完全复制组件下build.gradle文件,每个组件的build.gradle都需引入该配置文件,方便统一管理

    module.gradle

    //引入config.gradle配置  
    apply from: '../config.gradle'
    
    def VERSION = project.ext.version  
    def libs = project.ext.dependencies
    
    android {  
        compileSdkVersion VERSION.compileVersion
        defaultConfig {
            minSdkVersion VERSION.minSdkVersion
            targetSdkVersion VERSION.targetSdkVersion
            versionCode VERSION.versionCode
            versionName VERSION.versionName
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
        }
    }
    // 每个组件都是需要依赖:component_service组件进行通信的  
    dependencies {  
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        annotationProcessor libs['butterKnife_annotation']
        api project(':component_service')
        annotationProcessor libs['arouter_annotation']
        api libs['arouter_api']
    }

解决独立组件编译运行问题

在组件的配置gradle文件module.gradle中加入Boolean类型字段:isRunApptrue表示当前运行整个app,false表示单个组件可以运行编译,

module.gradle

apply from: '../config.gradle'

def isRunApp = project.ext.isRunApp  
// 这样只需要修改配置文件的,然后同步一下,就可以切换编译模式  
if (isRunApp) {  
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}
android {}  
dependencies {}

主app下的build.gradle文件也需要加入

   if (isRunApp) {  
        implementation project(':account_component')
        implementation project(':app_main_comp')
        implementation project(':news_component')
        implementation project(':community_component')
        implementation project(':im_component')
    }

Manifest合并问题

项目整体运行时,每个组件的manifest最终会合并,主module的manifest中会定义项目的themenamelabel,单独编译时同样也是需要的,这样AndroidManifest.xml的Application标签下有属性相同,会产生冲突。如:themenamelabel等,

解决办法有2种

  • 解决冲突时,需要加入tools:replace="android:name" 表明name属性是可以背替代的,这样合并时就会自动合并,最终冲突的会编入主app的属性,如果主app不存在,会编入最后编译的module的manifest的属性

  • 以主App为准,其他组件的manifest,只定义<application></application>,不定义任何属性,避免冲突,为独立运行的组件创建组件对应的Androidmanifest.xml文件,并配置目录

    在组件的默认gradle文件中配置manifest路径

    module.gradle

    apply from: '../config.gradle'  
    //...  
    android {  
      //...
        sourceSets {
            main {
                if (isRunApp) {
                   // 不做处理,使用默认的manifest路径
                } else {
                  // 指定 组件运行时,AndroidManifest.xml的路径
                    manifest.srcFile 'src/main/debug/manifest/AndroidManifest.xml'
                }
            }
        }
    }
    dependencies {}

组件独立运行

组件独立运行也是需要Activity的入口,所有我们会为每个独立的组件创建相应入口类,入口类只是我们运行组件,调试组件的辅助类,包括一些运行调试时的一些资源文件,这些在运行时都不需要写入到项目中。

根据分析,我们在module.gradle中配置组件运行时的资源目录和java主目录,在开发时调试时,我们就可以使用指定的目录去测试我们的功能

module.gradle

apply from: '../config.gradle'  
//...  
android {  
  //...
    sourceSets {
        main {
            if (isRunApp) {
               // 不做处理,使用默认的manifest路径
               //manifest.srcFile...
                resources {
                  // 运行项目时,排除debug文件下的资源
                    exclude 'src/main/debug/*'
                }
            } else {
                //指定 组件运行时,AndroidManifest.xml的路径
                manifest.srcFile 'src/main/debug/manifest/AndroidManifest.xml'
                // 指定资源和java文件位置
                java.srcDirs = ['src/main/debug/java/','src/main/java']
                res.srcDirs = ['src/main/debug/res/','src/main/res/']
            }
        }
    }
}
dependencies {}

这样我们在debug文件下。调试我们的组件,就完全可以跑起来了。

组件初始化

组件在作为Library依赖时,是没有application生命周期的,如果某个组件需要在项目启动时,做一些初始化的操作,是无法完成的,所以在主APP的application执行oncreate时,我们要对每个组件进行初始化。

组件的初始化方式有多种,可根据项目架构来对应做初始化处理

思路:在每个需要组件中定义一个初始化组件的类,在主App的applicationonCreate()方法中调用,由于代码隔离,主app是拿不到组件的初始化类的,这里采用反射的方式实现

  • 在Comoponent_service基础层的服务组件中,定义BaseModuleInit.java,需要初始化的组件继承该类,做初始化的操作

    public abstract class BaseModuleInit {
    
        /**
         * 每个组件有需要首次启动初始化时使用
         * @param application 主application对象
         */
        public abstract void applicationInit(Application application);
    }
  • 通过ModuleAppInits.java记录需要初始化的类路径

    public class ModuleAppInits {  
        //需要加入避免混淆的类,初始化组件时的初始化类全路径;
        public static final List<String> sModulesAppInits = Arrays.asList(
                "com.zgq.account.AccountModuleInit",
                "com.zgq.app_main_comp.MainModuleInit",
                "com.zgq.news_component.NewsModuleInit",
                "com.zgq.community_component.CommunityModuleInit",
                "com.zgq.im_component.ImModuleInit");
    }
  • 在Application的onCreate()方法中调用初始化

    Application

        /**  
         * 初始化各组件;
         * 各组件没有Application,所有通过主Application的初始话,对各组件进行初始化;
         */
        public void initModule() {
            for (String sModulesAppInit : ModuleAppInits.sModulesAppInits) {
                try {
                    Class<?> clazz = Class.forName(sModulesAppInit);
                    BaseModuleInit baseModuleInit = (BaseModuleInit) clazz.newInstance();
                    baseModuleInit.applicationInit(this);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
    


使用框架Arouter初始化

可以使用Arouter的依赖查找的功能来对组件初始化

// 1.component_service基础组件的 初始化组件的方法需要implement Arouter的Iprovider接口  
public class BaseModuleInit implement Iprovider{}  
// 2.组件的初始化方法添加路由表
@Route(path = "/xx/xx/init")
public class ComponentModuleInit extends BaseModuleInit{}

// 3.在application的OnCreate()中使用路由查找该初始化类,
BaseModuleInit init =(BaseModuleInit)Arouter.getInstance().build("xx/xx/init").nagigation();  
init.application(this);  

组件之间的通信

组件直接的交互都是通过Component_service来实现的,component_serivce为每个组件提供了数据交互的接口,每个组件只需要实现该接口,并在初始化组件的时候设置给component_service即可

举例

  • 在component_service组件中为账户组件创建数据输出Service;AccountService.java

  • 账户组件实现AccountService,创建AccountServiceImp类对外输出数据

public interface AccountService {  
    String getUid();
    String getIsLogin();
    UserInfo getUserInfo();
    BaseFragment getMyFragment(BaseActivity activity);
}
  • 在component_service创建serviceProvider为各组件提供交互service,

//组件交互service输出类  
public class ServiceProvider {  
    //账户组件service
    private AccountService mAccountService;
    // 主组件service
    private MainComponentService mMainComponentService;
    // 资讯组件service
    private NewsService mNewsService;
    // 社区组件service
    private CommunityService mCommunityService;
    // IM组件service
    private IMService mIMService;
    private ServiceProvider() {
    }
    private static final ServiceProvider instance = new ServiceProvider();
    public static ServiceProvider getInstance() {
        return instance;
    }
  // 省略 get()set()方法
}

  • 在初始化组件时,使用serviceProvider.set()方法设置相应的service;

// 账户组件初始化  
public class AccountModuleInit extends BaseModuleInit {  
    @Override
    public void applicationInit(Application application) {
        ServiceProvider.getInstance().setAccountService(new AccountServiceImp());
    }
}

同样的serviceProvider相关的功能,也是可以通过Arouter的依赖查找来实现

页面跳转

页面的跳转统一放在component_service组件Router类中

具体的实现可以采用路由,或者面向接口实现的传统Intent的跳转,

目前对随着项目复杂度,还有逻辑越来越多,包括push跳转,webview拦截跳转等,使用Intent的方式,我们需要写太多的if else,后期维护成本高,所以页面的跳转建议还是路由

具体的使用参考:

Arouter:一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦

这里是使用的阿里的Arouter:

public class Router {  
    // 跳转首页
    public static void startToMain(Activity mActivity) {
        ARouter.getInstance().build(RouterAPI.MAIN_ACT).navigation(mActivity);
    }
}

@Route(path = RouterAPI.MAIN_ACT)
public class HomeActivity extends BaseActivity {  
  public void onCreate(Bundle savedInstanceState){}
}

遇到的问题

资源冲突

  • 多团队协作,多组件开发时,在资源命名时,难免会有相同的时候,这时候,在合并项目的时候就会产生冲突。

    解决的办法是在每个组件的build.gradle中配置命名约束,

// 配置资源命名约束,一般会以组件名开头  
android {  
    resourcePrefix "xxx_"
}

配置命名约束后, 会限制.xml结尾的资源,包括values下的文件,如果我们不以xxx_开头的话,会报红提示的,但是一些图片资源等,还是需要手动解决冲突,多组件开发时,这就要求我们在开发过程中,形成好的命名习惯,都是xxx的组件名开头,防止最终合并项目时,需要手动解决冲突。

  • ButterKnife

    使用ButterKnife的时候,我们在主APP使用是没有任何问题的,在Library中使用会出现一些问题

    在library中的BindView(R.id.xx)时,会发现R文件不是一个final常量,在编译library为aar的时候,R.java文件还不是一个具体的常量值,最终编译classes的时候,会使用常量值替换R文件的变量,

    所以由于R文件不是具体的常量,使用butterKnife的时候,是无法使用注解和swtichcase的。

    butterKnife给出了具体的解决方案

    • build.gradle加入插件依赖

    buildscript {  
      dependencies {
        classpath 'com.jakewharton:butterknife-gradle-plugin:10.1.0'
      }
    }
    
    • 在组件的build.gradle中引入

    apply plugin: 'com.jakewharton.butterknife'  
    
    • 引入之后我们就可以在组件的开发中 使用R2文件来使用注解了

    class ExampleActivity extends Activity {  
      @BindView(R2.id.user) EditText username;
      @BindView(R2.id.pass) EditText password;
    }
    


  • swtich-case

    swtich-case: R2只能使用在注解中,当我们注册多个view的点击事件的时候,没法使用R2,

    只能使用if else来处理逻辑了


其他遇到的坑

待优化的地方

  • 给每个组件设置独立的isRunApp的变量值,分开控制,这样在调试的时候,是可以运行主app调试,但是组件只依赖两个相关的,减少运行调试等待的时间;

总结

  • 对于组件化重点是对业务模块的划分,对于搭建流程没什么复杂。组件化开发并不是为了组件化而去组件化,主要我们需要根据项目的开发形式,和对业务模块的理解来决定是否组件化。引用一句针对组件化文字的评论:

组件化难的是怎么拆分基础模块,需求在不断变化,有时候以前觉得很好的方法,到最后反成限制了

  • 组件化实战中,会有很多问题需要解决,这里在写demo,可能会有很多的问题没有暴露出来。

  • 对于单个组件的调试,不能作为最终项目的调试结果,实际开发中,组件自测没问题后,我们最终一定要以app运行,去测试我们的项目,环境不一致,可能会引发想不到的问题。

文档汇总

组件化框架
  • 得到DDComponentForAndroid

得到框架分析

得到框架:DDComponentForAndroid

  • MvpArms

MVPArms 是一个整合了大量主流开源项目的 Android MVP 快速搭建框架, 其中包含 Dagger2RetrofitRxJava 以及 RxLifecycleRxCacheRx 系三方库,

ArmsComponent

组件化相关文章

从零搭建组件化框架

组件化路由最佳实践

网易严选组件化

路由详细分析

作者介绍

  • 冀旭晖 Android 高级开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。