module解耦(二)基于路由的解耦

一、 引言

在安卓开发中,随着项目的复杂度增加,模块化(或组件化)开发方式越来越受到开发者的青睐。模块化开发可以将一个大型项目拆分成多个相对独立的模块,每个模块负责一个功能或业务场景,从而提高代码的可读性、可维护性和可复用性。但是模块化开发也带来了一些挑战,其中之一就是如何实现模块间的解耦和通信。

传统的方式是通过Intent进行页面跳转和数据传递,但这种方式存在以下缺点:

  • 需要显式地指定目标页面的类名或Action,导致强依赖关系;
  • 需要在AndroidManifest.xml中注册每个页面,并配置相应的过滤器;
  • 需要手动处理数据序列化和反序列化;
  • 需要在跨进程通信时使用Bundle或AIDL等机制;
  • 难以适应动态加载和插件化等需求。

为了解决这些问题,许多开源框架提出了基于路由(Router)的方案。路由是一种将URL映射到具体页面或服务的机制,可以用于将Intent页面跳转的强依赖关系解耦,同时减少跨团队开发的互相依赖问题。

二、 基本概念

2.1 路由

路由(Router)是一种将URL映射到具体页面或服务的机制。URL是一种统一资源定位符(Uniform Resource Locator),用于标识一个资源或目标。URL通常包含以下几个部分:

  • 协议(Scheme):表示资源访问所采用的协议类型,如http、https等;
  • 主机名(Host):表示资源所在服务器域名或IP地址;
  • 端口号(Port):表示资源所在服务器端口号,默认为80;
  • 路径(Path):表示资源在服务器上具体位置;
  • 查询参数(Query):表示请求资源时附加给服务器端程序处理数据;
  • 片段标识符(Fragment):表示请求资源时定位到某个子部分。

例如:

https://www.jianshu.com/u/1d9c468843cf/code?userId=aschnloih8ajkbkjb

这个URL包含以下部分:

  • 协议:https
  • 主机名:www.jianshu.com
  • 端口号:默认为80
  • 路径:/u/1d9c468843cf
  • 查询参数:code?userId=aschnloih8ajkbkjb
    片段标识符:无
    在基于路由的module解耦方案中,URL通常用于表示一个页面或服务,而不是一个网络资源。因此,协议部分可以自定义,例如:

router://moduleA/pageA?name=Tom&age=18
这个URL表示跳转到moduleA模块中的pageA页面,并传递name和age两个参数。

2.2 路由表

路由表(Router Table)是一种存储URL和页面或服务之间映射关系的数据结构。路由表可以是静态的或动态的,也可以是本地的或远程的。静态路由表是在编译期生成的,动态路由表是在运行期生成的。本地路由表是存储在客户端内存或文件中的,远程路由表是存储在服务器端数据库或配置文件中的。

路由表通常包含以下几个字段:

  • URL:表示一个页面或服务的唯一标识符;
  • 类名:表示对应页面或服务所属类名;
  • 类型:表示对应页面或服务所属类型,如Activity、Fragment、Service等;
  • 拦截器:表示对应页面或服务是否需要经过拦截器处理;
  • 优先级:表示对应页面或服务在多个匹配结果中的优先级;
  • 其他属性:表示对应页面或服务所需的其他属性,如进程名、启动模式等。
    例如:
URL 类名 类型 拦截器 优先级 其他属性
router://moduleA/pageA com.example.modulea.PageAActivity Activity true 1 process=“:moduleA”
router://moduleB/pageB/:id com.example.moduleb.PageBFragment Fragment false 0

这个路由表包含两条记录,分别表示moduleA模块中的pageA页面和moduleB模块中的pageB页面。其中pageB页面支持路径参数id,用于传递动态数据。

2.3 路由器

路由器(Router)是一种负责处理URL请求和跳转到相应页面或服务的组件。路由器通常包含以下几个功能:

  • 初始化:负责初始化路由表和拦截器等配置信息;
  • 解析:负责解析URL请求,并根据路由表查找匹配结果;
  • 拦截:负责根据拦截器规则判断是否需要拦截当前请求,并执行相应操作;
  • 跳转:负责根据匹配结果创建相应类型的实例,并执行跳转逻辑;
  • 回调:负责提供回调接口给调用者获取跳转结果和数据;
    例如:
// 初始化
Router.init(context);

// 解析
Postcard postcard = Router.parse("router://moduleA/pageA?name=Tom&age=18");

// 拦截
boolean intercepted = Router.intercept(postcard);

// 跳转
Router.navigate(postcard);

// 回调
Router.setCallback(new Callback() {
    @Override
    public void onSuccess(Postcard postcard) {
        // 跳转成功
    }

    @Override
    public void onFail(Postcard postcard) {
        // 跳转失败
    }
});

这段代码演示了使用Router组件进行URL请求处理和跳转到pageA页面的过程。

2.4 拦截器

拦截器(Interceptor)是一种负责对URL请求进行拦截和处理的组件。拦截器通常用于实现以下功能:

  • 权限检查:判断当前用户是否有权限访问目标页面或服务;
  • 登录检查:判断当前用户是否已经登录,如果没有则跳转到登录页面;
  • 参数校验:判断当前请求是否携带了合法的参数,如果没有则提示错误信息;
  • 数据预处理:对当前请求携带的数据进行预处理,如加密、解密、压缩、解压等;
  • 业务逻辑:根据业务需求执行一些特定的逻辑,如埋点、统计、广告等;
    例如:
public class LoginInterceptor implements Interceptor {
    @Override
    public boolean intercept(Postcard postcard) {
        // 判断当前用户是否已经登录
        if (!UserManager.isLogin()) {
            // 跳转到登录页面
            Router.navigate("router://moduleA/login");
            return true;
        }
        return false;
    }
}

这个拦截器实现了登录检查的功能,如果当前用户没有登录,则跳转到登录页面,并拦截原始请求。

三、使用实例

本节将以一个简单的示例来演示如何使用基于路由的module解耦方案。假设我们有一个电商项目,包含以下几个模块:

  • app模块:主模块,负责启动应用和管理其他模块;
  • home模块:首页模块,负责展示商品列表和轮播图等内容;
  • detail模块:详情模块,负责展示商品详情和评论等内容;
  • cart模块:购物车模块,负责展示用户已添加的商品和结算等内容;
  • user模块:用户模块,负责展示用户信息和订单等内容;

为了实现这些模块之间的解耦和通信,我们可以使用一个开源框架ARouter来实现基于路由的方案。ARouter是一个轻量级且功能强大的路由框架,支持静态路由表生成、自动参数注入、多种类型跳转、多种拦截器配置等功能。

  • ARouter框架是一个用于帮助Android App进行组件化改造的框架,支持模块间的路由、通信、解耦
  • ARouter框架使用静态注解处理,为适应多模块,使用moduleName后缀生成了一组统一规则的注册类
  • ARouter框架在GitHub上有完整的文档和示例代码,可以方便地查看和学习(https://github.com/alibaba/ARouter)
  • ARouter框架的基本使用包括初始化、注册页面和服务、跳转页面和调用服务、配置拦截器等步骤

3.1 配置依赖

首先,在项目根目录下的build.gradle文件中添加ARouter插件依赖:

buildscript {
    dependencies {
        classpath "com.alibaba:arouter-register:1.0.2"
    }
}

然后,在每个子模块下的build.gradle文件中添加ARouter库依赖,并应用ARouter插件:

apply plugin: 'com.alibaba.arouter'

dependencies {
    implementation "com.alibaba:arouter-api:1.5.2"
    annotationProcessor "com.alibaba:arouter-compiler:1.5.2"
}

最后,在app模块下的build.gradle文件中添加以下配置:

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

这样就完成了ARouter框架在项目中的配置。

3.2 初始化路由器

接下来,在app模块中创建一个Application类,并在onCreate方法中初始化路由器:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化路由器
        ARouter.init(this);
    }
}

同时,在AndroidManifest.xml文件中注册该Application类,并添加meta-data标签指定路由表生成路径:

<application
    android:name=".MyApplication">
    
    <meta-data
        android:name="ROUTER_MODULE_APP"
        android:value="app/src/main/java/com/example/app/router" /> </application>

这样就完成了路由器在app模块中的初始化。

3.3 注册页面和服务

然后,在每个子模块中,使用@Route注解来注册页面和服务,并指定对应的URL和其他属性:

// home模块中的HomePageActivity
@Route(path = "router://home/homePage")
public class HomePageActivity extends AppCompatActivity {
    // ...
}

// detail模块中的DetailPageActivity
@Route(path = "router://detail/detailPage", extras = 1)
public class DetailPageActivity extends AppCompatActivity {
    // ...
}

// cart模块中的CartService实现类
@Route(path = "router://cart/cartService")
public class CartServiceImpl implements CartService {
    // ...
}

// user模块中的UserService实现类
@Route(path = "router://user/userService")
public class UserServiceImpl implements UserService {
    // ...
}

这样就完成了页面和服务在各个子模块中的注册。

3.4 跳转页面和调用服务

接下来,在任意一个子模块中,可以使用ARouter.getInstance()方法获取路由器实例,并通过build方法构建Postcard对象,然后通过navigation方法进行跳转或调用:

// 从home模块跳转到detail模块的DetailPageActivity,并传递商品id参数
ARouter.getInstance().build("router://detail/detailPage").withString("id", "123456").navigation();

// 从detail模块调用cart模块的CartService接口,添加商品到购物车
CartService cartService = ARouter.getInstance().build("router://cart/cartService").navigation();
cartService.addProductToCart(product);

// 从cart模块调用user模块的UserService接口,获取用户信息
UserService userService = ARouter.getInstance().build("router://user/userService").navigation();
User user = userService.getUserInfo();

这样就完成了页面和服务在各个子模块之间的跳转和调用。

3.5 配置拦截器

最后,在任意一个子模块中,可以使用@Interceptor注解来配置拦截器,并指定优先级和名称:

// 配置一个登录拦截器,优先级为8,名称为loginInterceptor
@Interceptor(priority = 8, name = "loginInterceptor")
public class LoginInterceptor implements IInterceptor {

    @Override
    public void process(Postcard postcard) {
        // 判断当前请求是否需要登录检查(extras为1表示需要)
        if (postcard.getExtras() == 1) {
            // 判断当前用户是否已经登录
            if (!UserManager.isLogin()) {
                // 跳转到登录页面,并传递原始请求信息(requestCode为100表示是从拦截器跳转过来)
                ARouter.getInstance().build("router://user/login").with(postcard).withInt("requestCode", 100).navigation();
                // 拦截当前请求,并设置结果码为失败(RESULT_FAILED表示失败)
                postcard.setGreenChannel();
                postcard.withInt("resultCode", RESULT_FAILED);
            }
        }
        // 继续执行下一个拦截器或目标请求(onContinue方法表示继续)
        callback.onContinue(postcard);
    }

    @Override
    public void init(Context context) {
        // 初始化拦截器相关资源(可选)
    }
}

这样就完成了拦截器在某个子模块中的配置。

四、优缺点分析

1.基于路由的module解耦方案有以下优点:

  • 实现了模块之间的完全解耦,不需要依赖任何接口或实现类;
  • 支持多种类型的跳转和调用,包括Activity、Fragment、Service等;
  • 支持多种方式的参数传递和注入,包括基本类型、对象类型、Bundle等;
  • 支持多种拦截器配置和处理,可以实现权限检查、登录检查等功能;
  • 支持静态路由表生成和动态路由注册,提高了性能和灵活性;

2.基于路由的module解耦方案也有以下缺点:

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

推荐阅读更多精彩内容