美文网首页Android TechMobDevGroup组件化模块化相关
android组件化方案,让团队开发更有效率

android组件化方案,让团队开发更有效率

作者: Rainbow冰糖葫芦娃 | 来源:发表于2017-02-24 20:17 被阅读2313次

刚接到Leader组件化任务的时候,内心是有疑惑的。目前项目中,各种业务交杂在一起,互相跳转、互相请求数据。分模块的过程必然是痛苦的,需要增加模块之间通的信协议。对于一个5,6人的团队来说,全都放在一个大模块中似乎也没啥不好,可以随心所欲地调用,节约思考框架合理性的时间。

但最终让我解除疑惑是团队扩张的长远考虑,未来团队扩张到8~10人时,在同一个模块中开发不同的业务就会很乱。代码量2倍,掌握的难度就会超过4倍,按业务分模块一定是未来的方向。

顾名思义,组件化能够让开发者只需专注自己开发的组件,独立运行自己的模块,节省编译时间,减少因别人的问题导致工作被打断的可能。

组件化的实现围绕下面几个点

1、子模块单独编译
2、sdk和第三方库的版本一致性
3、资源重复定义
4、模块之间页面跳转
5、模块之间数据传递
6、模块初始化处理

1、子模块如何单独编译

我们希望在开发模式下,能够单独调试自己的模块,编译成独立的apk。而在主程序发布时,成为一个library嵌入主工程。

首先在子模块build.gradle中定义常量,来标示模块目前是否处于开发模式

def isDebug = true

在子模块的build.gradle中进行模式配置。debug模式下编译成独立app,release模式下编译成library。

if (isDebug.toBoolean()) {
  apply plugin: 'com.android.application'
} else {
  apply plugin: 'com.android.library'
}

两种模式下模块AndroidManifest.xml文件是有差别的。作为独立运行的app,有自己的Application,要加Launcher的入口intent,作为library不需要。这个问题很好解决,写两个不同的AndroidManifest.xml即可,并在gradle中进行配置。

  sourceSets {
    main {
      if (isDebug.toBoolean()) {
        manifest.srcFile 'src/main/debug/AndroidManifest.xml'
      } else {
        manifest.srcFile 'src/main/AndroidManifest.xml'
      }
    }
  }

2、sdk和第三方库的版本一致性

不同module依赖sdk版本不一致,会因兼容性问题导致编译问题。
不同module引用了同一个第三方库的不同版本,并且这个库没有做到向前兼容,就有可能出现方法找不到、参数不对应等问题。
所以有必要统一整个project的依赖版本。

在最外层build.gradle中定义的常量能被整个projectbuild.gradle文件引用,统一的版本定义可以放在这里。

ext {
    android_compileSdkVersion = 25
    android_buildToolsVersion = '25.0.2'
    android_minSdkVersion = 21
    android_targetSdkVersion = 25

    lib_appcompat = 'com.android.support:appcompat-v7:25.1.1'
    lib_picasso = 'com.squareup.picasso:picasso:2.5.2'
    lib_gson = 'com.google.code.gson:gson:2.6.1'
}

3、资源的重复定义

说到资源的重复定义,笔者趟过坑,如果主工程和子模块中重复定义了同名的资源。

主工程中

<string name="daddy">爸爸</string>

子工程中

<string name="daddy">干爹</string>

虽然编译不会出错,但是最后子模块中用到daddy的地方都会显示爸爸
编译时子模块的资源会和主工程合并到同一个类中,所以资源重名会有问题。

但是资源也要模块化呀,总不能在底层找个统一的地方都扔在里面,gradle提供了一个解决方案来避免重复定义的问题。

resourcePrefix "a_"

强制模块中的资源名称带有a_前缀,否则编译不过。

聊到这里,我们知道了如何使用gradle独立编译子模块,以及如何处理分模块导致的一些问题。但是除了主工程统一调度外,模块与模块之间也需要互相调起和访问,所以需要协议去统一,这个协议是模块间共同定义与使用的,所以写在底层。

Paste_Image.png

4、模块之间页面跳转

首先想到的就是配置uri去匹配模块AndroidManifest.xml中的intentFilter来启动相应Activity,这种方式是解耦的,但有缺点,要跳转其它模块,得先去看别的模块的AndroidManifest.xml进行入口适配,还得研究具体Activity中的传参设置,虽然代码依赖上解耦了,但是实现逻辑上没有解耦,忍不了。需要在底层创建一个路由协议,让使用者通过协议方便地调用。

用注解把需要的参数写在路由协议的接口中。下面是moduleA提供给其它模块跳转moduleA中页面的接口:

public interface RouterA {

  @RouterUri("test://host_a")
  public Intent getIntentActivityA(@RounterParam("name") String name, @RounterParam("age") int age,
      @RounterParam("phones") Phones phones);//Phones是一个自定义类

}

其中@RouterUri表示跳转改页面需要匹配的uri,这个uri最终会拿去和moduleA中的AndroidManifest.xml中对应activityintentFilter去匹配。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RouterUri {
  String value() default "";
}

@RounterParam用来表示目标activity需要的参数,最终会在目标activity中进行解析。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RounterParam {
  String value() default "";
}

为什么用注解的方式写接口而不是直接定义跳转方法呢?
用注解的方式,可以把参数更直观地展现在最醒目的方法声明中。而写成实现的方法,参数会被写在方法内部,定义起来不方便,而且要带上少量逻辑,不够简洁。参考retrofit框架,也是用注解方式去实现,简洁、方便。

**为什么接口返回的是Intent,而不是直接进行页面跳转呢? **
因为我们的项目中,实现这个跳转可能是activity,可能是fragment,也可能startActivityForResult需要带入一个自定义的requestCode。所以为了灵活性,直接返回Intent

写好了接口,还需要将接口中的参数组装成一个可进行跳转的Intent。使用Proxy生成类动态代理这个接口。

public class RounterBus {
  //静态map存储代理接口的实例
  private static HashMap<Class, Object> sRounterMap = new HashMap<Class, Object>();

  /**
   * 得到动态代理路由接口的实例
   *
   * @param c 接口类
   * @param <T>
   * @return
   */
  public static<T> T getRounter(Class<T> c) {
    T rounter = (T) sRounterMap.get(c);
    if (rounter == null) {
      rounter = (T) Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, new InvocationHandler() {
        @Override public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
          //从方法注解的获取uri
          RouterUri routerUri = method.getAnnotation(RouterUri.class);
          if (routerUri == null || TextUtils.isEmpty(routerUri.value())) {
            throw new IllegalArgumentException(
                "invoke a rounter method, bug not assign a rounterUri");
          }
          Uri.Builder uriBuilder = Uri.parse(routerUri.value()).buildUpon();

          //从参数值和参数注解,获取信息,拼入uri的query
          Annotation[][] annotations = method.getParameterAnnotations();
          if (annotations != null && annotations.length > 0) {
            for (int i = 0, n = annotations.length; i < n; i++) {
              Annotation[] typeAnnotation = annotations[i];
              if (typeAnnotation == null || typeAnnotation.length == 0) {
                throw new IllegalArgumentException("method " + method.getName() + ", args at " + i + " lack of annotion RouterUri");
              }
              boolean findAnnotaion = false;
              for (Annotation a : typeAnnotation) {
                if (a != null && (a.annotationType() == RounterParam.class)) {
                  uriBuilder.appendQueryParameter(((RounterParam) a).value(), GsonInstance.getInstance().toJson(args[i]));
                  findAnnotaion = true;
                  break;
                }
              }
              if (!findAnnotaion) {
                throw new IllegalArgumentException("method " + method.getName() + " args at " + i + ", lack of annotion RouterUri");
              }
            }
          }

          Context context = AppContext.get();
          PackageManager pm = context.getPackageManager();
          Uri uri = uriBuilder.build();
          Intent intent = new Intent(Intent.ACTION_VIEW, uri);
          //查询这个intent是否能被接收用来进行跳转
          List<ResolveInfo> activities = pm.queryIntentActivities(intent, 0);
          if (activities != null && !activities.isEmpty()) {
            return intent;
          } else {
            if (BuildConfig.IS_DEBUG) {
              Toast.makeText(context, "子模块作为独立程序启动时,跳不到其他模块哟", Toast.LENGTH_SHORT).show();
            } else {
              throw new IllegalArgumentException("can't resolve uri with " + uri.toString());
            }
          }
          return null;
        }
      });
      sRounterMap.put(c, rounter);
    }
    return rounter;
  }
}

上面代码包装了一个路由总线,来获取并缓存路由接口的实例。
例如:moduleB需要调起 moduleA中的ActivityA

Intent intent = RounterBus.getRounter(RouterA.class).getIntentActivityA("xixi", 1, new Phones());
if (intent != null) {
    startActivityForResult(intent, 0);
}

5、模块之间的数据传递

每个module在底层library中用接口定义自己公开出来的方法

public interface FunctionA {
  public String getData(String key);
}

由总线进行管理

public class FunctionBus {
  /**
   * 方法总线,缓存的map
   */
  public static Map<Class, Object> sFunctionClassMap = new HashMap<>();

  /**
   * 设置接口的实现类
   * @param o
   */
  public static void setFunction(Object o) {
    Class[] interfaces = o.getClass().getInterfaces();
    for (Class c : interfaces) {
      if (sFunctionClassMap.containsKey(c)) {
        throw new IllegalStateException("duplicate set function:" + c.getName());
      }
      sFunctionClassMap.put(c, o);
    }
  }

  /**
   * 获取接口的实现类
   * @param c
   * @param <T>
   * @return
   */
  public static<T> T getFunction(Class<T> c) {
    T f = (T) sFunctionClassMap.get(c);
    if (f == null) {
      Toast.makeText(AppContext.get(), "you have not register function:" + c.getName(), Toast.LENGTH_SHORT).show();
      return null;
    }
    return f;
  }
}

每个module需要提前在方法总线中传入接口的实例(建议在Application.onCreate时),别的模块才能访问到该方法。

moduleB访问moduleA提供的getData方法:

FunctionBus.getFunction(FunctionA.class).getData("hi")

6、application初始化

子模块作为application时,有一些初始化的工作需要在Application.onCreate时进行。而作为library时,调不到这个onCreate。所以自己写一个静态方法,供主工程的Application调用。

public class ApplicationA extends Application {

  @Override public void onCreate() {
    super.onCreate();
    //给底层library设置context
    AppContext.init(getApplicationContext());
  }

  /**
   * 作为library时需要初始化的内容
   */
  public static void onCreateAsLibrary() {
    //给FunctionBus传入接口的实例
    FunctionBus.setFunction(new FunctionA() {
      @Override public String getData(String key) {
        return "xixi";
      }
    });
  }
}

主工程的Application onCreate时记得初始化子模块。

public class MainApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    AppContext.init(getApplicationContext());
    ApplicationA.onCreateAsLibrary();
    ApplicationB.onCreateAsLibrary();
  }
}

想调试A模块,but某些功能需要依赖B

这时只需要把B模块作为library引入A。并且记得在B模块Application.onCreate时初始化一下A模块。是不是很轻量级?常用的话在gradle中设置一个开关就更方便了。

def isDebugWithB = true
if (isDebugWithB.toBoolean()) {
  compile project(':moduleB')
}
ApplicationB.onCreateAsLibrary();

总结

大学毕业刚开始工作时,当时的Leader就表达过一个观点,随着团队人越来越多,产出会越来越多,但是人均产出明显会减少。随着代码量的增多,代码的复杂性会呈指数上升,增加了新同学融入工作的难度。读完这篇文章我们知道,通过组件化,新同学只需关注少量的代码就能快速融入工作,开发中也能更专注于自己的功能,编译更快,bug更少。爽歪歪!

文中代码 github地址:https://github.com/RainbleNi/ModuleDivider

参考文章:http://www.cnblogs.com/whoislcj/p/5860138.html

相关文章

  • android组件化方案,让团队开发更有效率

    刚接到Leader组件化任务的时候,内心是有疑惑的。目前项目中,各种业务交杂在一起,互相跳转、互相请求数据。分模块...

  • android组件化精彩文章

    Android组件化方案_移动开发_不学习傻了吧-CSDN博客

  • Android组件化方案

    Android组件化方案Android彻底组件化demo发布

  • 组件化开发

    不怕跌倒,所以飞翔 组件化开发 参考资源 Android组件化方案 为什么要组件化开发 解决问题 实际业务变化非常...

  • Router方案说明

    前言 该方案是阐述Android平台组件化方案设计中Router 组件的的设计思路。针对猫窝科技组件化开发工作,进...

  • Android组件化

    Android 得到app 彻底组件化方案实践 CC:可关联生命周期的android组件化开发框架 美团猫眼电影a...

  • 使用JIMU组件化方案

    开源组件化方案JIMU 写在前面 公司为提高开发效率,降低模块之间的耦合,寻求一种高效、稳定的组件开发方案。有幸看...

  • Android 组件化架构概要

    组件化相信大家都很清楚他是什么,Android的一种开发架构,并且备受团队推崇,确实组件化在我看来对团队协作方面是...

  • android:组件化方案

    得到Android组件化方案已经开源,参见Android组件化方案开源。方案的解读文章是一个小的系列,这是系列的第...

  • Android彻底组件化—代码和资源隔离

    得到Android组件化方案已经开源,参见Android组件化方案开源。方案的解读文章是一个小的系列,这是系列的第...

网友评论

  • 西北狼神:公共module commonlibrary的gradle中定义的buildConfigField "IS_DEBUG" 可以通过buildTypes {
    release {
    buildConfigField "boolean", "LOG_DEBUG", "false"
    }
    debug{
    buildConfigField ....
    }

    这种方式类添加BuildConfig的字段,而不需要你手动去使用idDebug变量的值来控制。:joy:
    西北狼神:@Jon_Snow09 :smile: 之前我好想是看明白了他为啥这样写,有原因的,你多看看。太久了我也就不得这个逻辑了。
    Jon_Snow09:你好,我也觉得可以添加一个自定义的BuildConfig字段,就不需要手动更改debug变量的值了,这里为什么要这么写呢?
    西北狼神:已看明白你这里为什么要这样写了哈。

    PS:下一个问题
    被依赖的业务module如何能反向跳转回去?

    比如单独调试moduleA的时候,moduleA依赖moduleB, 在moduleA中通过URI跳转到moduleB了,,在moduleB中通过URI跳转回moduleA中不成功, RounterBus中queryIntentActivities处查询出来为0个。

    :unamused: :unamused:
  • passerbywhu:模块之间的数据传递那部分。如果使用contentProvider去提供数据的话,就可以和上面模块之间页面跳转部分使用统一的URI格式了。
    另外最后一部分主工程Application onCreate中去初始化子模块直接调用了
    ApplicationA.onCreateAsLibrary();
    ApplicationB.onCreateAsLibrary();
    这样子岂不是有直接依赖?那在其他module单独编译为application的时候这部分代码是注释掉的?
    另外还需要解决的问题是主工程中生命周期如何通知到其余各模块的问题。
    Rainbow冰糖葫芦娃:嗯 contentProvider只能返回cursor所以解析起来比较麻烦,所以在耦合很强的业务模块之间通信不如FunctionBus
    可以换成注册的方式,用接口。不直接依赖。
    生命周期,也用注册的方式可解决
  • 安新小子:楼主请问这句依赖的作用是什么,
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
    exclude group: 'com.android.support', module: 'support-annotations'
    })
    Rainbow冰糖葫芦娃: @staticzh 看注解的定义在哪里,我没用过apt呢
    安新小子:@Rainbow冰糖葫芦娃 恩恩,我就是想问 使用注解的时候不是都用apt插件吗,看了好多关于apt的讲解,具体还不太懂。使用注解是不是都要导入apt插件呢
    Rainbow冰糖葫芦娃: @staticzh 这句是创建工程的时候默认加上的,应该是测试是才会编译的内容
  • 安新小子:楼主,可以拿你上面的代码添加自己的理解继续讲解发表文章吗?已注明参考文章地址
    644b57ffe605:在不在?我想问下你们组件化用的什么数据库,我这边刚想用模块化来开发,可是用greendao却不能用
    安新小子:@Rainbow冰糖葫芦娃 好的,谢谢啊
    Rainbow冰糖葫芦娃: @staticzh 可以呀,没问题
  • 安新小子:既然组件间的参数传递有单独的总线维护FunctionBus ,那为什么跳转的接口getIntentActivityA中还要参数有什么意义呢
    安新小子:@Rainbow冰糖葫芦娃 恩恩,明白了,谢谢啊
    Rainbow冰糖葫芦娃: @staticzh 这个问题问得很好,其实我觉得用注解进行解析参数进行页面跳转要比自己写方法来得方便直观些。当然用function也可以达到目的,就是少许麻烦些。
  • 安新小子:楼主请问我设置了resourcePrefix 以后,没有修改前缀编译还能通过是怎么回事啊
    安新小子:@Rainbow冰糖葫芦娃 谢谢
    Rainbow冰糖葫芦娃: @staticzh ,lint检查会有报红,编译确实还能过
  • shangwfa:很不错
    Rainbow冰糖葫芦娃: @shangwfa 谢谢
  • Merbng:谢谢:+1:
  • 姜康:讲得很清晰
    Rainbow冰糖葫芦娃: @姜康 谢谢夸奖

本文标题:android组件化方案,让团队开发更有效率

本文链接:https://www.haomeiwen.com/subject/rnovwttx.html