Android架构Android组件化开发框架【库】

Android模块化开发与ARouter框架

2019-01-24  本文已影响172人  AC编程

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

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

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

一、模块化开发的好处

二、模块化要解决的问题

三、ARouter路由框架

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

附上ARouter官网地址:https://github.com/alibaba/ARouter/blob/master/README_CN.md

其中,关于路由方面,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,也就是每个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";

}

//声明的地方
@Route(path = "/test/activity")

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

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

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

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、解决步骤

classpath 'com.jakewharton:butterknife-gradle-plugin:8.2.1'
build.gradle
apply plugin: 'com.jakewharton.butterknife'
library build.gradle
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地址

上一篇下一篇

猜你喜欢

热点阅读