背景

三国演义里开篇就说:天下大势,分久必合,合久必分。这话套在软件开发上,也特别贴切。我记得我刚入门时做Android应用程序开发,刚开始都是采用中心化管理的思想,将相同的资源集中进行管理,但是做着做着,发现集中管理的资源太多了,多人开发时牵一发而动全身,进而又要对原本的项目进行拆分,演变出模块化开发,以及我这里要讲的Android组件化实践。

现状

一种是项目组件化开发,一种是单一工程开发模式。

1.对工程的任意修改调试都要编译整个工程,效率十分低下;
2.不利于多人团队协同开发;
3.无法做到功能复用
4.业务模块间耦合严重

在项目的开发过程中,随着开发人员的增多及功能的增加,如果提前没有使用合理的开发架构,那么代码会越来臃肿,功能间代码耦合也会越来越严重,这时候为了保证项目代码的质量,我们就必须进行重构。

比较简单的开发架构是按照功能模块进行拆分,也就是用 Android 开发中的 module 这个概念,每个功能都是一个 module

上面说到了从普通的无架构到模块化,再由模块化到组件化,那么其中的界限是什么,模块化和组件化的本质区别又是什么?为了解决这些问题,我们就要先了解 “模块” 和 “组件” 的区别。

Android组件化需要注意什么 app组件化_Android组件化需要注意什么

组件化和模块化的区别

  • 模块
    模块指的是独立的业务模块,比如刚才提到的 [首页模块]、[圈子模块]、[直播间模块] 等
  • 组件
    组件指的是单一的功能组件,如 [视频组件]、[支付组件]、[登陆组件] 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。

最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。

问题: 当多个模块中涉及到相同功能时代码的耦合又会增加。

例子:首页模块和论坛圈子间模块中都可能涉及到了视频播放的功能,
这时候不管将播放控制的代码放到首页还是直播间,开发过程中都会发现,我们想要解决的代码耦合情况又又出现了

理想结构

要解决的问题

  1. 每个组件都是一个完整的整体,所以组件开发过程中要满足单独运行及调试的要求,这样还可以提升开发过程中项目的编译速度。
  2. 组件间的数据传递与方法调用
  3. 组件间界面跳转,在组件化开发过程中如何在不相互依赖的情况下实现互相跳转?

组件单独调试-两种方式

  1. 单独工程运行
  2. 动态配置组件的工程类型

组件单独运行

  1. 组件的工程类型
    Android Gradle 中提供了三种插件,在开发中可以通过配置不同的插件来配置不同的工程
  • App 插件,id: com.android.application
  • Library 插件,id: com.android.libraay
  • Test 插件,id: com.android.test

在 module 中添加一个 gradle.properties 配置文件,在配置文件中添加一个布尔类型的变量 isRunAlone,在 build.gradle 中通过 isRunAlone 的值来使用不同的插件从而配置不同的工程类型,在单独调试和集成调试时直接修改 isRunAlone 的值即可

组件间页面跳转

说到组件间通信,目前算是已经有了很成熟的方案,ARouter(阿里)、WMRouter(美团)等

工作原理可以分为三个部分:

  1. 编译期通过注解处理器生成相关的中间类;中间类保存路由Url和具体实现的类的路径关系, build/generated/ap_generated_source/out
  2. 程序启动时初始化路由表;
  3. 路由调用的时候通过路由表找到实现类路径,使用startActivity实现页面跳转

技术点

  1. 编译期通过注解APT
  • 动态注解和静态注解:
  • 编译时获取注解信息,并据此产生java代码文件,无性能损失

参考

  1. Java Poet生成源代码

a. 可以自动生成Java文件的第三方依赖
b. 简洁易懂的API,上手快

组件间的数据传递与方法调用

  1. 暴露服务
// 声明接口,其他组件通过接口来调用服务, 接口要放在commom底层
public interface HelloService extends IProvider {
    String sayHello(String name);
}

// 实现接口
@Route(path = "/yourservicegroupname/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
    return "hello, " + name;
    }

    @Override
    public void init(Context context) {

    }
}
  1. 发现服务
// 2. 使用依赖查找的方式发现服务,主动去发现服务并使用,下面两种方式分别是byName和byType
    helloService3 = ARouter.getInstance().navigation(HelloService.class);
    helloService3.sayHello("Vergil");

3. 问题

上面的处理可以看到 接口类HelloService必须放在底层。但是这样会导致不好扩展。有变化都需要修改底层接口。

4. 解决

底层提供一个通用的CommonService,来分发方法调用
然后根据传入的Method 找到对应的具体实现

基类

public interface BaseCommonService extends IProvider {

    /**
     * 分发方法调用
     *
     * @param method 方法path
     * @param params 方法所需的参数
     * @param args   额外参数
     * @return
     */
    Response call(String method, Bundle params, Object... args);
}

具体实现类

@Route(path = BBRouterKeys.BB_COMMON_METHOD_PATH)
public class CommonRouterService implements BaseCommonService {

    private static final String TAG = "CommonRouterService";

    private Context mContext;

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

    @Override
    public Response call(String method, Bundle bundle, Object... objects) {

        if (BBRouterKeys.BB_COMMON_INVITE_TEAM_MEMBER.equals(method)) {
            ArrayList<String> members = bundle.getStringArrayList(BBRouterKeys.BB_ENCRYPT_KEY_IS_SECRET);
            return Response.generateSuccess(inviteTeamMember(members));
        } else if (BBRouterKeys.BB_COMMON_KEY_NIM_GET_NIM_TOKEN.equals(method)) {
            String uid = bundle.getString(BBRouterKeys.BB_COMMON_KEY_NIM_UID);
            return getUserNimToken(uid);
        } else if (BBRouterKeys.BB_CHAT_UPDATE_NIM_TOKEN.equals(method)) {
            return updateNimToken();
        }

        return Response.generateFail();
    }

服务获取

/**
     * 从路由路径path对服务的方法中获取数据
     *
     * @param path
     * @param requestBundle
     * @param args
     * @return
     */
    public static Bundle getResponseBundle(String path, Bundle requestBundle, Object... args) {
        Response response = BAFRouter.call(path, requestBundle, args);
        if (response != null && response.result != null && Response.SUCCESS.equals(response.status)) {
            return response.result;
        } else {
            return null;
        }
    }

仍然存在的问题

使用commonService改造后,是可以实现发现服务,但是返回值被封装到Response的Bundle里面,还要解析去拿,而且不太友好,已经不是面向接口编程

面向接口编程-理想的情况

  1. 模块A,定义接口
@JInterface("/service")
public interface TestInterface {
     //提供的方法
     public void test(String msg);
}
  1. 模块A调用接口
Router.getInstance(). create(TestInterface.class) .test("Implement By Module B");
  1. 模块B提供服务
@JImplement("/service")
	public class TestImplement {

   public void test(String msg) {
      //方法的具体实现
       Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
   }
}

实现原理

跟上面路由查找的一样的,还是使用APT来生成中间代码,然后根据路由表,最有对应的实现类,反射出实例;在调用具体的方法

多进程架构模块通信

  • 解决方案是单独提取一个Wide Router模块,所有的Local Route与Wide Router通过进程间通信的方式建立连接
  • 请求如果在Local Router中找不到时,则通过WideRouter与其它进程建立连接,WideRouter充当局域网之间的路由。
  • 类似: 局域网- 广域网的处理

总结

  1. 提高工程编译速度
  2. 业务模块解耦,有利于多人团队协作开发
  3. 组件化是功能重用的基石