Android组件化实践和思考

1,710 阅读6分钟

一、为什么要组件化

随着项目需求的迭代,工程的规模越来越大,各种业务错综复杂的交织在一起,代码没有约束,边界越来越模糊,牵一发而动全身,开发和测试效率越来越低。这种情况下我们就要考虑采用组件化的架构思想对项目进行重构,拆分出基础组件和业务组件,解除业务之间的耦合。

  • 单一职责:开发人员只用关心自己负责的业务
  • 代码解耦:组件之前不直接依赖,编译器隔离
  • 复用性强:基础组件解耦,便于其它项目复用
  • 编译集成:业务组件可以独立调试,提升编译速度
  • 平台化:大型团队通常会根据业务进行拆分和平台化,组件化利于划清各自的代码边界,提高各团队配合的效率

二、组件化架构设计

组件化架构设计图

架构分层

  • 应用层:app壳工程,Application、SplashActivity等
  • 业务组件:根据业务模块进行划分,比如首页、发现等
  • 通用业务:通用基础业务,比如BaseActivity、埋点、网络库封装等
  • 基础组件:与业务不相关的基础库,比如日志库、加解密等
  • 依赖库:依赖的三方库

组件拆分

Android Studio可以将工程拆分成多个module,每个组件对应一个module,组件代码稳定之后,可以上传maven库,使用gradle方式依赖。

组件命名规范

层级 命名
应用层 app
业务组件 bussniss_home、bussniss_find
通用业务 common、service
基础组件 lib_log、lib_ui、lib_utils

依赖关系

  1. 遵循依赖倒置原则,上层依赖下层,业务层之间不互相依赖
  2. common依赖常用基础组件和三方库,建议采用api方式依赖,这样业务组件的build.gradle直接依赖common即可
  3. 业务组件对基础组件和依赖库的依赖采用implementation方式
  4. app对业务组件的依赖采用runtimeOnly方式

三、组件通信

业务组件之间完全隔离,不允许直接依赖,但有些场景下组件间需要通信,比如页面跳转和方法调用。经过综合对比,我们最终选择了ARouter作为通信的基础方案。整体是接口+实现的方案,各业务组件把需要对外暴露的接口抽象出来,抽象接口类放到统一的service服务组件,实现类放到各业务内部。

页面跳转

ARouter.getInstance().build(RouterPathConstant.PATH_FIND).navigation();

方法调用

以用户组件为例,说明下组件之间方法调用的实现方式。

service组件的IUserService,定义用户组件对外暴露的接口。

public interface IUserService extends IProvider {
    public String getUserId();
    public String getNickname();
    public String getAvatar();
}

bussiness_user组件接口实现类UserServiceImpl。

@Route(path = ServicePathConstant.SERVICE_USER)
public class UserServiceImpl implements IUserService {
    @Override
    public void init(Context context) {

    }

    public String getUserId() {
        return UserCenter.getInstance().getUserId();
    }

    public String getNickname() {
        return UserCenter.getInstance().getNickname();
    }

    public String getAvatar() {
        return UserCenter.getInstance().getAvatar();
    }
}

现在接口定义和实现OK了,怎样将接口类暴露出去,让调用方拿到IUserService对象呢?还是借助ARouter实现服务工厂类ServiceFactory,对抽象接口进行统一管理,ServiceFactory位于service组件中,所有业务组件可以直接调用。

public class ServiceFactory {

    public static IUserService getUserService() {
        return (IUserService) getService(ServicePathConstant.SERVICE_USER);
    }

    private static Object getService(String path) {
        return ARouter.getInstance().build(path).navigation();
    }
}

这里借助ARouter的注解运行时获取IUserService的实现类对象,如果不使用ARouter,也可以在ServiceFactory中定义set方式,在Application初始化的时候实例化UserServiceImpl并赋值到ServiceFactory中。

至此,我们就可以在任意业务组件内部调用用户组件的方法了。

String userId = ServiceFactory.getUserService().getUserId();

调用app方法

上面我们说到依赖倒置原则,业务组件不能依赖app层。在组件化逐步重构过程中,可能还有部分业务代码是在app module中,如果业务组件需要调用这部分代码,可以定义IUserDelegator接口,在app初始化的时候将实现类设置到IUserService。

(IXXService是业务组件对外暴露的接口,IXXDelegator是业务组件需要调用方实现的接口,两者不要弄混了~)

service组件增加IUserDelegator接口类。

public interface IUserDelegator {
    void go2Main();
    void go2Spalsh();
}

service组件IUserService增加setDelegator和getDelegator方法。

public interface IUserService extends IProvider {
    void setDelegator(IUserDelegator delegator);
    IUserDelegator getDelegator();
}

app module实现IUserDelegator并调用IUserService的setDelegator。

public class WalletApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        ServiceFactory.getUserService().setDelegator(UserDelegatorImpl());
    }

至此,business_user组件中就可以调用app的方法了。

IUserDelegator delegator = ServiceFactory.getUserService().getDelegator();
delegator.go2Main();

四、资源文件管理

资源文件当然也要拆分到各个业务module中,为了避免冲突,强烈建议资源文件加上统一的前缀,比如R.drawable.user_icon_back, R.layout.user_activity_edit, R.string.user_dialog_title。module中资源文件名重复并不影响编译,但只会有1个打包进去,为啥我的布局文件改了就是不生效,相信有不少同学遇到这类问题。

build.gradle中可以增加资源文件前缀检查,如果命名不规范,Android Studio会提示警告。

defaultConfig {
    minSdkVersion rootProject.ext.android.minSdkVersion
    targetSdkVersion rootProject.ext.android.targetSdkVersion
    resourcePrefix "user_"
}

五、组件集成与调试

某些项目会有打包多个app的需求,比如马甲包、多国家版本。这就需要有多个project和多个app module,app module中选择依赖需要的业务组件,打包生成不同的apk。

前面有提到app对业务组件的依赖使用runtimeOnly的方法,这样做的好处是能够解除app和业务组件之间的耦合。比如app中不应该直接访问UserFragment,应该在IUserService中提供获取方法,Fragment的实例化过程放到bussiness组件中,app不用关心拿到的是个什么样的Fragment。

public interface IUserService {
    public Fragment newUserFragment();
}

组件独立编译调试

目前我们工程编译2-3分钟,还算能忍,所以暂时没做组件独立编译。如果团队规模大,工程编译慢,也是可以去做的。

六、思考

  • 耦合和冗余有时候会冲突

    比如多个业务都需要调用某个api,那么api的response model是否需要提取到common组件中呢?分拆到各个业务组件中是没有耦合了,但看着一堆冗余代码又总感觉怪怪的。解耦和去冗余,就按我们怎么选择了。

  • 常量如何管理

    看过有些工程代码,喜欢定义一个全局的Constants常量类,各个模块都在引用这个类,甚至有的把常量类下沉到common模块中。这并不是个好的做法,常量应该分散到各个业务组件中去,不同模块之间尽量不要共享常量。

  • 高内聚低耦合

    代码重构一定要时刻牢记这6个字,组件化不是为了拆分module,而是要写出高内聚低耦合的代码。

  • 代码洁癖

    最后,希望大家都保持代码洁癖,做一个有洁癖的程序猿。