88、组件化开发:模块拆分、路由ARouter、组件间通信、独立调试

组件化开发,说白了就是“分而治之”。

一个App越做越大,代码全堆在一个module里,那滋味我太熟了——编译等半天,改一行代码要跑全量测试,团队协作时天天解决冲突。我当年接手过一个项目,单module代码量超过50万行,每次build都能去泡杯咖啡。后来痛下决心做了组件化重构,嗯,真香。

模块拆分:怎么切才合理?

模块拆分是组件化的地基。拆不好,后面全是坑。

我个人习惯按业务领域来切,而不是按功能层来切。举个例子:

  • app壳模块:只负责组装,不写业务代码
  • common基础库:网络库、图片加载、工具类等
  • 业务组件:首页、社区、我的、商城等
  • 功能组件:分享、支付、推送、扫码等

这里有个关键点——单向依赖。业务组件只能依赖common,不能反向依赖。业务组件之间也不能互相依赖,否则就失去了组件化的意义。

核心原则:组件之间是平级的,通过路由通信,而不是直接引用。

我在项目中遇到过一种情况:产品说“社区模块要调用商城的商品详情”。如果直接写依赖,社区就强耦合了商城。正确的做法是——社区发一个路由跳转,商城自己处理。这样两个组件各自独立,互不干扰。

路由ARouter:组件间的导航员

组件之间不能直接引用类,那怎么跳转页面?

答案就是路由。ARouter是阿里开源的Android路由框架,我用它好多年了,稳定可靠。

先看怎么用:

// 在目标Activity上添加注解
@Route(path = "/mall/product_detail")
public class ProductDetailActivity extends AppCompatActivity {
    @Autowired
    String productId;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ARouter.getInstance().inject(this);
        // 直接用productId
    }
}

// 在发起方调用
ARouter.getInstance()
    .build("/mall/product_detail")
    .withString("productId", "12345")
    .navigation();

你看,发起方根本不知道目标Activity的类名,只通过一个路径字符串就能跳转。这就是解耦。

小技巧:路径命名建议用两级——模块名+功能名。比如"/mall/product_detail","/user/profile"。这样一目了然,也方便统一管理。

ARouter还支持拦截器。我曾经用它做过登录拦截——用户点击需要登录的页面时,自动跳转到登录页,登录完成再回到目标页。代码大概这样:

@Interceptor(priority = 1)
public class LoginInterceptor implements IInterceptor {
    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        if (需要登录 && 未登录) {
            // 跳转到登录页,并保存目标路径
            callback.onInterrupt(null);
        } else {
            callback.onContinue(postcard);
        }
    }
}

组件间通信:不止是跳转

路由解决了页面跳转,但组件间还需要数据通信。比如社区组件要获取用户信息,总不能每次都跳转到用户页面吧?

我常用的方案有几种:

通信方式 适用场景 缺点
接口下沉 获取数据、调用服务 需要定义公共接口
事件总线 广播通知、状态同步 调试困难,不易追踪
ARouter服务 跨组件调用方法 需要注册服务

我个人最推荐接口下沉的方式。具体做法是:

  1. 在common模块定义接口,比如IUserService
  2. 在user组件实现这个接口
  3. 通过ARouter的服务发现机制获取实现
// common模块定义接口
public interface IUserService {
    String getUserName();
    boolean isLoggedIn();
}

// user组件实现
@Route(path = "/user/service")
public class UserServiceImpl implements IUserService {
    @Override
    public String getUserName() {
        return "当前用户名";
    }
}

// 其他组件调用
IUserService userService = (IUserService) ARouter.getInstance()
    .build("/user/service").navigation();
String name = userService.getUserName();

这样做的好处是——编译期不依赖,运行期动态获取。你想想看,如果以后要替换user组件,只要新组件也实现IUserService接口就行,其他代码完全不用改。

注意:事件总线虽然用起来方便,但别滥用。我曾经在一个项目里看到几百个Event,根本分不清谁发的谁收的,调试起来想砸电脑。能用接口解决的,就别用事件。

独立调试:每个组件都能单独跑

组件化最大的痛点之一就是调试。每次改完都要跑整个App,那还组件化干嘛?

正确的做法是——每个业务组件都可以作为独立App运行。

实现方式很简单:在gradle里控制。

// 在gradle.properties里设置
isModule = false  // true表示独立运行,false表示集成到主App

// 组件build.gradle
if (isModule.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

android {
    // 独立运行时需要applicationId
    if (isModule.toBoolean()) {
        defaultConfig {
            applicationId "com.example.mall"
        }
    }
    // 独立运行时使用自己的AndroidManifest
    sourceSets {
        main {
            if (isModule.toBoolean()) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

独立运行的AndroidManifest需要单独配置:

<!-- src/main/debug/AndroidManifest.xml -->
<manifest>
    <application>
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

集成模式下的AndroidManifest不需要MAIN入口,因为由App壳统一管理。

经验之谈:我建议每个组件都保留一个DebugActivity,里面放一些测试入口。比如测试网络请求、测试UI样式、测试数据状态。这样独立调试时效率极高,不用每次都从头操作到目标页面。

组件化架构总览

说了这么多,画张图帮你理清思路:

组件化架构示意图 App壳(组装层) 首页组件 社区组件 我的组件 商城组件 ARouter 路由通信 分享组件 支付组件 推送组件 扫码组件 Common基础库(网络、图片、工具类、UI组件) 业务组件之间通过ARouter通信,不直接依赖 所有组件依赖Common基础库,但Common不依赖任何业务组件

这张图很直观地展示了组件化的分层结构。从上到下依次是:App壳 → 业务组件 → 功能组件 → 基础库。业务组件之间通过ARouter通信,不直接依赖。所有组件都依赖Common,但Common不依赖任何业务组件。

避坑指南

做了这么多年组件化,踩过的坑不少。分享几个典型的:

  • 资源冲突:不同组件的资源文件可能重名。我建议每个组件都设置resourcePrefix,比如mall_、user_,从命名上避免冲突。
  • 依赖版本统一:所有组件用的第三方库版本必须一致。我在common里用ext定义版本号,各组件统一引用。
  • Application初始化:每个组件可能需要在Application启动时做一些初始化。ARouter提供了IProvider接口,可以实现懒加载初始化。
  • 混淆配置:组件化后混淆规则要覆盖所有模块。我曾经漏掉一个组件的混淆配置,上线后崩溃率飙升,教训深刻。

曾经踩过的坑:有一次我改了common模块的一个工具类,结果所有组件都要重新编译。后来我把common拆得更细——真正稳定的基础库放一个module,经常变动的业务工具放另一个module。这样改动影响范围就小多了。

组件化不是银弹,但它确实能解决大型项目的很多痛点。关键在于——拆分要合理,通信要规范,调试要独立。做到这三点,你的组件化之路就成功了一大半。


公众号:蓝海资料掘金营,微信deep3321