Android模块化开发与ARouter框架

在App开发的初期,代码量不大,业务量比较少,一个App作为一个单独的模块进行开发,往往问题不大。但随着业务的增多,代码变的越来越复杂,每个模块之间的代码耦合变得越来越严重,结构越来越臃肿,修改一处代码要编译整个工程,导致非常耗时,这时候解耦问题急需解决。

同时,如果公司有多个终端设备的App,而且有块功能是通用的(比如说下单功能),那么通用的这一块功能被复制集成到不同App里,就显得很重复,而且维护时要修改多套代码,严重影响开发效率,因此模块化开发就很有必要。

App模块化的目标是告别结构臃肿,让各个业务变得相对独立,业务模块在组件模式下可以独立开发,而在集成模式下又可以变为依赖包集成到“app壳工程”中,组成一个完整功能的APP。

一、模块化开发的好处

  • 公用功能,不用重复开发、修改,代码复用性更强
  • 独立运行,提高编译速度,也就提高了开发效率
  • 更利于团队开发,不同的人可以独立负责不同的模块
  • 独立模块可以采用不同的技术架构,尝试新的技术方案,比如采用新的网络框架,甚至换成Kotlin来开发App

二、模块化要解决的问题

  • 模块间页面跳转(路由);
  • 模块间事件通信;
  • 模块间服务调用;
  • 模块的独立运行;
  • 模块间页面跳转路由拦截(登录)

三、ARouter路由框架

以上模块化需要要解决的问题,2017年阿里开源的路由框架ARouter都有提供解决方案。
官方对这个框架的定义是:一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦。
ARouter提供的功能有:

  • 支持直接解析标准URL进行跳转,并自动注入参数到目标页面中
  • 支持多模块工程使用
  • 支持添加多个拦截器,自定义拦截顺序
  • 支持依赖注入,可单独作为依赖注入框架使用
  • 支持InstantRun
  • 支持MultiDex(Google方案)
  • 映射关系按组分类、多级管理,按需初始化
  • 支持用户指定全局降级与局部降级策略
  • 页面、拦截器、服务等组件均自动注册到框架
  • 支持多种方式配置转场动画
  • 支持获取Fragment
  • 完全支持Kotlin以及混编(配置见文末 其他#5)
  • 支持第三方 App 加固(使用 arouter-register 实现自动注册)
  • 支持生成路由文档
  • 提供 IDE 插件便捷的关联路径和目标类

附上ARouter官网地址:ARouter

其中,关于路由方面,Google提供的原生路由主要是通过Intent,Intent可以分成显示和隐式两种。显示的方案会导致类之间的直接依赖问题,耦合严重;隐式Intent需要在配置清单中统一声明,首先有个暴露的问题,另外在多模块开发中协作也比较困难。除此之外,使用原生的路由方案会出现跳转过程无法控制的问题,因为一旦使用了startActivity()就无法插手其中任何环节了,只能交给系统管理,这就导致了在跳转失败的情况下无法降级,而是会直接抛出运营级的异常。

// Intent显式启动Activity
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
// Intent隐式启动Activity
Intent intent = new Intent("com.example.activity.ACTION_START");
startActivity(intent);

如果使用ARouter,可以在跳转过程中进行拦截,出现错误时可以实现降级策略. 比如跳转页面不存在不是直接crash而是可以跳转到一个指定的默认页面。

四、用ARouter进行模块化开发

接下来,将会用一个demo介绍如何用ARouter进行模块化开发,demo模块化的整体架构如下:

  • app:项目的宿主模块,仅仅是一个空壳,依赖于其他模块,成为项目架构的入口
  • baselibrary:项目的基类库,每个子模块都依赖共享公用的类和资源,防止公用的功能在不同的模块中有多个实现方式
  • module_route:集中管理所有模块的route
  • module_main:闪屏页,登录页,主页等
  • module_home:首页模块
  • module_mine:我的模块
  • module_video:视频模块


    module

五、依赖模式与独立运行模式切换

在项目开发中,各个模块可以同时开发,独立运行而不必依赖于宿主app,也就是每个module是一个独立的App,项目发布的时候依赖到宿主app中。各业务模块之间不允许存在相互依赖关系,但是需要依赖基类库。单一模块生成的apk体积也小,编译时间也快,开发效率会高很多,同时也可以独立测试。要实现这样的效果需要对项目做一些配置。

1、gradle.properties配置

在项目gradle.properties中需要设置一个开关,用来控制module的编译,如下:

isModule=false

当isModule为false作为依赖库,只能以宿主app启动项目,选择运行模块时其他module前都是红色的X,表示无法运行


依赖宿主模式

当isModule为true的时候作为单独的模块进行运行,选择其中一个module可以直接运行


独立运行模式

2、清单文件配置

module清单文件需要配置两个,一个作为独立项目的清单文件,一个作为库的清单文件,以module_main模块为例:


清单文件

buildApp作为依赖库的清单文件,和独立项目的清单文件buildModule区别是依赖库的清单文件Application中没有配置入口的Activity,其他都一样

3、gradle配置

gradle配置

4、宿主app配置

宿主app配置

六、ARouter功能详解

1、添加依赖和配置

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }
    }
}

dependencies {
    // 替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    compile 'com.alibaba:arouter-api:x.x.x'
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
    ...
}

注意,arouter-api:1.3.1、arouter-compiler:1.1.4配置是

arguments = [moduleName: project.getName()]

arouter-api:1.4.1、arouter-compiler:1.2.2配置是

arguments = [AROUTER_MODULE_NAME: project.getName()]

2、添加注解

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/activity")
public class YourActivity extend Activity {
    ...
}

在我的demo中,各模块的route地址都统一放在module_route中集中管理,其他module都需要依赖module_route,同时不同模块的route有单独的RoutePath类。如图:


module_route
@Route(path = MainRoutePath.LOGIN_ACTIVITY)
public class LoginActivity extends BaseActivity {
    ...
}
public class MainRoutePath {

    private static final String PREFIX = "/main/";

    public static final String MAIN_ACTIVITY = PREFIX+"MainActivity";

    public static final String LOGIN_ACTIVITY = PREFIX+"LoginActivity";

}

  • 问题1:为什么要把route地址写在一个常量类里?
//声明的地方
@Route(path = "/test/activity")

//使用的地方
ARouter.getInstance().build("/test/activity").navigation();

声明的地方和使用的地方可以是处于不同的module,这种写法各module不需要相互依赖,貌似很好,耦合度很低。但这只是表面上看代码没有了耦合度,但他们的耦合关系还在,试想一下,声明的地方哪天把route地址改了,使用的地方完全“无感”,只有等到真正运行时才能发现出错了,这种写法风险很大,而且不容易提前发现。如果声明的地方和使用的地方route都用一个常量来表示,就能很好的避免这种风险。

  • 问题2:为什么不同模块的route有单独的RoutePath类
    因为一个App一般页面都比较多,如果所有route都用一个RoutePath类来装,那这个类将会很大,且不同模块开发人员都需要去改这个类,容易产生混乱。如果不同模块的route有单独的RoutePath类,不同模块的开发人员只去改对应的类,代码会更好管理。
  • 问题3:为什么要单独建一个module_route,仅仅只配置route,为什么不把RoutePath类放在baselibrary里?
    baselibrary一般是一些通用的基础功能或通用配置,正常情况下应只能让少数的有架构层次的开发人员去改动,所以应该做权限保护,如果把RoutePath放在baselibrary里,相当于baselibrary对所有开发人员都是开发的。
  • 问题4:配置route地址时,有什么讲究?
    如上MainRoutePath中,我的route地址配置规则采用的是: 前缀 + Activity类名,前缀一般用module名字。根据官方文档说明
    1、SDK中针对所有的路径(/test/1 /test/2)进行分组,分组只有在分组中的某一个路径第一次被访问的时候,该分组才会被初始化;
    2、可以通过 @Route 注解主动指定分组,否则使用路径中第一段字符串(/*/)作为分组。
    意思是用分组可以按需加载,提高性能,当没有主动分组时,ARouter用第一段字符串作为分组。所以我的前缀就是分组名,不用再去主动指定分组。

3、初始化SDK

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

4、发起路由操作

// 1. 简单的跳转
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation();

// 2. 跳转并携带参数
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY)
                            .withString("name", name)
                            .withInt("age", 28)
                            .navigation();

5、目标页面接收参数

@Route(path = MainRoutePath.MAIN_ACTIVITY)
public class MainActivity extends BaseActivity {

    /**
     * 接收参数
     */
    @Autowired(name = "name")
    public String name;
    @Autowired(name = "age")
    public int age;
    ...
}

6、声明拦截器(拦截跳转过程,面向切面编程)

拦截都是全局性的,因此一般写在baselibrary里,如权限校验的拦截器。拦截器会在跳转之间执行,多个拦截器会按优先级顺序依次执行


AuthInterceptor

但是需要注意的是,每次所有的跳转都会执行拦截器操作,ARouter提供了greenChannel()方法进行跳转过去一切拦截器,在不需要拦截器的地方跳转的时候加上即可。

 //greenChannel表示跳过拦截器验证
 ARouter.getInstance().build(MainRoutePath.LOGIN_ACTIVITY).greenChannel().navigation();

7、降级策略

ARouter提供的降级策略主要有两种方式,一种是通过回调的方式;一种是提供服务接口的方式。我们分别来看看两种方式的使用方法:

  • 一、单独降级-回调的方式
    这种方式在跳转失败的时候会回调NavCallback接口的onLost方法。
ARouter.getInstance().build(MainRoutePath.MAIN_ACTIVITY).navigation(this, new NavCallback() {
     @Override
     public void onFound(Postcard postcard) {
          Log.d("ARouter", "找到了");
     }

     @Override
     public void onLost(Postcard postcard) {
          Log.d("ARouter", "找不到了");
     }

     @Override
     public void onArrival(Postcard postcard) {
          Log.d("ARouter", "跳转完了");
     }

     @Override
      public void onInterrupt(Postcard postcard) {
           Log.d("ARouter", "被拦截了");
      }
});

回调接口,对于降级策略主要实现感兴趣的onLost方法即可。

  • 二、全局降级-服务接口的方式
    这种方式很简单,主要处理逻辑在内部,暴露的接口很友好。
//跳转目标页面不存在,触发降级策略 避免crash
ARouter.getInstance().build("/test/test").navigation();

这种降级策略主要是实现服务接口DegradeService,就一个方法就是onLost,和上面的类似。

//要用ARouter跳转才能拦截到,用Intent隐式或显示跳转无法拦截,出错还是会crash
@Route(path = RoutePath.DEGRADE)
public class DegradeServiceImpl implements DegradeService {
    @Override
    public void onLost(Context context, Postcard postcard) {
        ARouter.getInstance().build(RoutePath.DEGRADE_TIP).greenChannel().navigation();
    }

    @Override
    public void init(Context context) {

    }
}

全局降级-服务接口也应该写在baselibrary里

8、使用 IDE 插件导航到目标类

在 Android Studio 插件市场中搜索 ARouter Helper, 或者直接下载文档上方 最新版本 中列出的 arouter-idea-plugin zip 安装包手动安装,安装后 插件无任何设置,可以在跳转代码的行首找到一个图标 点击该图标,即可跳转到标识了代码中路径的目标类,如图:

ARouter Helper安装一

ARouter Helper安装二

跳转快捷 图标

9、生成路由文档

// 更新 build.gradle, 添加参数 AROUTER_GENERATE_DOC = enable
// 生成的文档路径 : build/generated/source/apt/(debug or release)/com/alibaba/android/arouter/docs/arouter-map-of-${moduleName}.json

arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
路由文档

七、Android Butterknife在library组件化模块中的使用问题

1、问题

当项目中有多module时,在使用Butterknife的时候会发现在library模块中使用会出问题。当library模块中的页面通过butterknife找id的时候,就会报错,提示@BindView的属性必须是一个常数,也就是说library module编译的时候,R文件中所有的数据并没有被加上final,也就是R文件中的数据并非常量。

2、解决步骤

  • I 首先在项目的总build.gradle中添加classpath
classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.1'
build.gradle
  • II 在library中build.gradle中引入插件
apply plugin: 'com.jakewharton.butterknife'
library build.gradle
  • III 在library中build.gradle中dependencies添加依赖
compile "com.jakewharton:butterknife:8.5.1"
annotationProcessor "com.jakewharton:butterknife-compiler:8.5.1"

3、butterknife在library activity中的使用和注意事项

1、用R2代替R findviewid

   @BindView(R2.id.textView)
    TextView textView;
    @BindView(R2.id.button1)
    Button button1;
    @BindView(R2.id.image)
    ImageView image;

2、在click方法中同样使用R2,但是找id的时候使用R

 @OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.textView:
                break;
            case R.id.button1:
                break;
            case R.id.image:
                break;
        }
    }

3、特别注意library中switch-case的使用,在library中是不能使用switch- case 找id的,解决方法就是用if-else代替

@OnClick({R2.id.textView, R2.id.button1, R2.id.button2, R2.id.button3, R2.id.image})
    public void onViewClicked(View view) {
        int i = view.getId();
        if (i == R.id.textView) {

        } else if (i == R.id.button1) {

        } else if (i == R.id.image) {

        }
    }

八、 Demo地址

  • ARouter的其他详细功能,可阅读官方文档:ARouter
  • 最后附上我的Demo地址:ARouter Demo

最后给大家送波福利

阿里云折扣快速入口

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335