Android 组件化

2,195 阅读12分钟

软件开发的六大设计原则

开闭原则

对扩展开放,对修改关闭。其目的在于当我们需要拓展一个功能的时候,不能去修改原有的代码,而应该去通过其它的方法来达到这个目的,其实此处的其它方法,便是针对代码中容易发生改动的位子,利用接口或抽象类来进行拓展,在需要改动的时候,只需要根据需求重新派生一个新的实现类就可以了。

里氏代换原则

任何基类可以出现的地方,子类一定可以出现。这就要求,子类可以实现父类中的抽象方法,但不要覆盖父类已实现的方法。如果随意修改了父类已实现的方法,可能会带来未知的错误。

依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:面向接口编程

单一职责

一个对象,一个模块功能应该是单一的,不应该承担太多的职责,也就是术业有专攻。

接口隔离原则

  • 客户端不应该依赖它不需要的接口
  • 类间的依赖关系应该建立在最小的接口上

如果一个类实现一个接口,但这个接口中有它不需要的方法,那么就需要把这个接口拆分,把它需要的方法提取出来,组成一个新的接口让这个类去实现,这就是接口隔离原则。简而言之,就是说,接口中的所有方法对其实现的子类都是有用的。否则,就将接口继续细分。

迪米特法则

也叫最少知道原则(Least Knowledge Principle)一个类应该对其它的对象有最少的了解。

  • 从依赖者的角度来说,只依赖其应该依赖的对象
  • 从被依赖者的角度来讲,只暴露该暴露的方法

合成复用原则

在软件设计的时候应该优先采用组合,其次才考虑使用继承关系

软件开发的六大设计原则从一个高维度进行指导软件开发的实践,为开发可维护,高拓展性软件提供方法论。

Android组件化

很多时候,当我们提到组件化时,有人会提到模块化。那么模块化和组件化到底有什么区别和联系呢?有的文章或书上介绍到:

  • 组件是从功能的角度上去划分,如分享组件,路由组件,图片加载组件。
  • 模块是从业务的角度上去划分,如首页模块,订单模块,个人中心模块。 但是你会发现互联网上别人在谈组件化的时候,会把上面提到的路由,订单模块都称作为组件了。这里我们就不去咬文嚼字了,我这篇总结里约定组件和模块是同一个东西。

再回到我们开发的过程中来,如果是一个项目从头开始开发,可能做了没两天就会碰到这种情况,代码里面没有统一的dialog控件,然后leader 就会说:xx, 你封装一个统一的dialog组件吧。过段时间发现没有统一的上拉加载列表控件,然后leader又说:xx,你整一个列表组件吧...

两年过去了,项目中的代码越来越多了,参与的人员越来越多了,由原来的一个团队扩展成多个团队合作开发,扯皮的事也越来越多了,编译时间也从原来的20s到现在的3分钟了。于是大家开始吐槽了:

  • 别的团队的同事又改了我这个类的代码,导致我这边出错
  • 登录模块和个人中心一两年没有动过了,能不能这一部份代码抽出去,打个aar,我们编译也能提速
  • 我主要做基础UI控件支持的,改下代码还要下载你们的项目,编译,太耗时了。我就想只调试我写的UI控件。
  • ...

我们来总结以上项目迭代过程中的痛点:

  • 各业务线耦合太紧,开发过程涉及到相互依赖的功能时,会有出错风险
  • 部份稳定功能,长期没有迭代,但因为都在同一个库了,会拉长编译时间
  • 部份功能想单独运行
  • ...

所有的问题最终抛到leader这里来,于是排了一堆的技改需求:

  • 项目按首页, 商品, 订单, 个人中心, 登录几个业务模块来分module开发,module之间不相互依赖, 同时收紧代码权限,这样不同团队之间就不会存在代码冲突的问题
  • 登录这个已经稳定的模块,近期不会有改动,可直接打成aar包供其它团队引入使用
  • 单独定义一个module, 命名为ui, 整个工程中的基础UI控件,全部都放到这里来
  • 定义一个module名为common, 为项目提供一些公共的业务资源,被其它业务模块所依赖
  • 定义一个统一的内部路由,供各个组件间进行相互跳转
  • ...

于是,我们可以看到,在这一堆的技改完成后,整个工程便分为了多个module, 每一个的功能更加的单一,每个人/每个团队专注于自己的工作,也少了很多扯皮的事情,同时每一个团队都可以根据自己的业务发展实际情况,选择自己期望的技术栈。以上这些工作完成了,一个组件化的架构雏形便出来了。 我们再来细看一个组件化架构要解决哪些问题:

  • 每个组件既可以是一个组件,也可以是一个application,可以单独打包调试
  • 组件间的通信
  • 组件间资源冲突的问题
  • 组件间的跳转

module和app角色的切换

manifest文件

我们期望每一个业务子module,通过配置,可以单独运行起来,此时就涉及到manifest的配置了。默认情况下,每一个module都只会有一个manifest文件,在打包阶段,打包工具会将每个module中的manifest文件合并,形成整个项目的manifest文件。比如,对于个人中心module, 它里面有一个activity名为PersonaMainActivity当它做作一个app运行起来的时候, 该activity是应用的入口,为此,在对应的manifest中会有类似如下代码:

<activity android:name=".view.PersonalMainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

但是,项目在发版的时候,个人中心这个模块一定是作为一个子module引进来的,PersonalMainActivity绝对不可能是整个app的入口。为此我们需要定义两套不同的manifest文件,一套是当该组件做为app运行的时采用,另一套是当该组件做为module时采用。

我们在项目的根目录定义一个文件config.gradle, 里面定义一个变量moduleAsApp, 当该值为false时,代表该项目中所有的业务子module都作为组件被主module依赖,当它为true时,代表业务子module可作为独立app运行。然后在每一个业务子module中都引入该配置。

config.gradle文件:

ext {
    moduleAsApp = false;
    appId = [
        "app"      : "com.fred.routerapp",
        "order"    : "com.fred.order",
        "product"  : "com.fred.product",
        "personal" : "com.fred.personal"
    ]

    packageNameForAPT = appId.app + ".apt";
    
    androidConfig = [
        compileSdkVersion: 29,
        buildToolsVersion: "29.0.3",
        minSdkVersion    : 21,
        targetSdkVersion : 29,
        versionCode      : 1,
        versionName      : "1.0"
    ]
}

再回到我们的个人中心module里面来,对这两种情况分别采用不同的manifest文件,如下:

sourceSets {
    main {
        if (moduleAsApp) {
            manifest.srcFile 'src/main/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java {
                // release 时 debug 目录下文件不需要合并到主工程
                exclude '**/debug/**'
            }
        }
    }
}

对于module配置和applicationId

if (moduleAsApp) {
    apply plugin: 'com.android.application'
} else {
     apply plugin: 'com.android.library'
}

applicationId:

if (moduleAsApp) {
    applicationId appId.personal
}

资源名冲突

在组件化开发过程中,可能会存在资源名冲突的问题,假如在product模块中有一个price_detail.xml用来显示价格相关的视图,在order模块中也有一个price_detail.xml,那便会出现资源名冲突。对于这种问题,可以统一命名方式,如加前缀,将product模块中的这些资源命名全部加上product_前缀,order模块中全部加上order_前缀,这样可以一定程序上避免。当然如果只是针对于布局xml文件可以在gradle文件中进行配置来约束

android {
    ...
    resourcePrefix "personal"
    ...
}

这种方式并不是说在编译的时候会手动将你的文件名进行修改,加personal_的前缀。只是加了这个配置,如果你的命名不合法,编译器会给一个提示。

image.png 对于资源命名冲突这一块我们不仅需要关注布局文件,还有类似于color,anim, dimen等命名

组件间的通信

此处组件间的通信主要包括两方面的内容:一种是业务之间的通知消息,比如订单模块中,一个订单提交了,需要通知购物车刷新一个购物车中的商品列表,对于这种类型的消息通知,我们可以采用EventBus,RxBus这种消息总线来做。另一个通信则是组件间基础数据的打通。比如在订单模块中,用户下单时需要判断是否登录,从单一职责的原则上讲用户的登录信息是在个人中心模块中才有。那么个人中心模块如何向其它模块提供用户相关的数据呢?

还记得我们前面提到的commonmodule吗?它可以被其它的所有业务子模块依赖,于是我们在commonmodule中定义一个接口IAccountService

public interface IAccountService {
    public boolean isLogin();
    ...
}

在个人中心模块有它的实现类

public class AccountServiceImpl implements IAccountService {

    @Override
    public boolean isLogin() {
        return false;
    }
}

common模块中有一个ServiceManager类,这个类是一个服务的管理者,它会持有一个AccountService, 如下:

public class ServiceManager {
    private ServiceManager() {}
    private IAccountService accountService;
 
    private static class AppConfigurationHolder {
        private static final ServiceManager instance = new ServiceManager();
    }

    public static ServiceManager getInstance() {
        return AppConfigurationHolder.instance;
    }

    public void setAccountService(IAccountService as) {
        this.accountService = as;
    }

    public IAccountService getAccountService() {
        return this.accountService;
    }
}

我们期望其它模块通过调用ServiceManager.getInstance().getAccountService()便可以拿到用户信息相关的服务。如果要达到此目的,那这个AcccountService在什么时候注入呢?我们在再看app的架构依赖

image.png AccountService的实现在个人中心module,我们不确定用户在什么场景会调用这个服务。对于这种类型的服务,需要在app启动的时候便注入到app中。而AccountService只能在个人中心模块实例化,为此个人中心模块必须要能监听到应用的初始化时机,也就是Application的onCreate方法

监听Application的状态

common组件中定义一个接口, 其它的业务module都会实现这个接口。

public interface AppStateListener {
    void onCreate();
    void onLowMemory();
}

在个人中心模块会有一个类, 在onCreate中会new一个AccountService,并且注入到ServiceManager

public class PersonalAppStatusListener implements AppStateListener {
    @Override
    public void onCreate() {
        ServiceManager.getInstance().setAccountService(new AccountServiceImpl(AppStateManager.getInstance().getApplication()));
    }

    @Override
    public void onLowMemory() {

    }
}

common模块中,会有一个类来维护所有的子业务module对Application状态的监听

public class AppStateManager {
    private List<AppStateListener> lifeCycleListenerList;
    private AppStateManager(){}
    private Application application;

    public void init(Context context) {
        this.application = (Application) context;
        initAppLifeCycleListener();
    }

    public void initAppLifeCycleListener() {
        //定义所有模块的listener的类名
        String [] names = Constants.moduleLifeCycleListener;
        if (names != null && names.length > 0) {
            lifeCycleListenerList = new ArrayList<>();
        }
        for (int i = 0; i < names.length; i ++) {
            try {
               Class clazz =  Class.forName(names[i]);
               if (AppStateListener.class.isAssignableFrom(clazz)) {
                   lifeCycleListenerList.add((AppStateListener) clazz.newInstance());
               }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            }
        }
    }

    public Application getApplication() {
        return this.application;
    }
    public void onCreate() {
        if (lifeCycleListenerList != null) {
            for (AppStateListener listener : lifeCycleListenerList) {
                listener.onCreate();
            }
        }
    }

    public void onLowMemory() {
        if (lifeCycleListenerList != null) {
            for (AppStateListener listener : lifeCycleListenerList) {
                listener.onLowMemory();
            }
        }
    }


    private static class Instance{
        public static AppStateManager INSTANCE = new AppStateManager();
    }

    public static AppStateManager getInstance() {
        return AppStateManager.Instance.INSTANCE;
    }
}

在Application中会在适当的时机调用AppStateManager方法,将Application的状态分发给各子业务模块

public class App extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        AppStateManager.getInstance().onCreate();
    }

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        AppStateManager.getInstance().init(this);
    }

    @Override
    public void onLowMemory() {
        super.onLowMemory();
        AppStateManager.getInstance().onLowMemory();
    }
}

再来从时间顺序上梳理一遍整个流程:

  1. 用户启动app, 首先执行AppattachBaseContext方法
  2. attachBaseContext方法中会调用AppStateManagerinit方法,该方法会把所有业务module的AppStateListener都加载进来
  3. ApponCreate方法中会调用AppStateManager类中的onCreate方法,最终会执行所有业务module里面AppStateListeneronCreate方法,也包括个人中心模块, 个人中心模块的AppStateListener中的onCreate方法会创建AccountService, 并且将AccountService注入到Servicemanager中.

以上步骤完成后,各模块都可以使用ServiceManager.getInstance().getAccountService()来拿到用户信息相关的数据。

路由

由于业务模块之间是不相互依赖的。所以路由的配置只能加到common模块中,在common模块里面维护着一个大的Map,来管理特定的url与具体的Activity之间的映射。在app启动的时候,去初始化这个Map。具体思路可能是下面这个样子:

定义一个RouterManager

public class RouterManager {
    private Map<String, String> map = new HashMap<>();

    private RouterManager instance;

    private RouterManager() {}

    public static RouterManager getInstance() {
        return InstanceHolder.instance;
    }

    public void put(String url, String className) {
            map.put(url, className);

    }

    public void startActivity(Activity activity, String url, Intent intentData) {
        try {
            Intent intent = new Intent(activity, Class.forName(map.get(url)));
            intent.putExtras(intentData);
            activity.startActivity(intent);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    private static class InstanceHolder {
        public static RouterManager instance = new RouterManager();
    }

}

添加映射关系

在上面的例子中我们有看到每个子业务模块会有一个监听器用来监听Application的onCreate方法,我们可以在那个里面加个各自业务模块的页面与url之间的映射关系

public class PersonalAppStatusListener implements AppStateListener {
    @Override
    public void onCreate() {
       ...
       RouterManager.getInstance().put("personal/login", "com.fred.personal.view.LoginActivity");
       RouterManager.getInstance().put("personal/main", "com.fred.personal.view.PersonalMainActivity");
    }

    @Override
    public void onLowMemory() {

    }
}

使用

使用时我们可以这样调起个人中心的主页面

RouterManager.getInstance().startActivity(activity, "personal/main", null);

以上只是简单说明了实现一个router的思路,当前这种方案的弊端显而易见,每次新增一个页面,还需要在自己手动添加到总的Map里面,这个总的Map需要自己来维护,很容易出,同时这种使用方式也不是很友好。这就是为什么需要一个类似于ARoute这样的一个路由组件,后面我们讲一下如何自己实现一个路由组件。

混淆

混淆的配置在主module中,每个业务组件中都保留一份混淆配置文件不便于修改和管理。

开发环境的设置

虽然在做组件化,但是我们期望各个子module在开发环境的配置上能保持统一,比如compileSdk, targetSdk等参数以及第三方包的版本统一。于是我们在项目的根目录下定义一个config.gradle文件,里面对这些信息进行配置

ext {
    moduleAsApp = false;
    appId = [
            "app"      : "com.fred.routerapp",
            "order"    : "com.fred.order",
            "product"  : "com.fred.product",
            "personal" : "com.fred.personal"
    ]
    packageNameForAPT = appId.app + ".apt";
    android = [
            compileSdkVersion: 28,
            buildToolsVersion:'29.0.2',
            minSdkVersion    : 14,
            targetSdkVersion : 26,
            versionName : "2.0",
            versionCode : 20210510,
    ]
    dependVersion = [
            rxJava              : "2.1.0",
            rxAndroid           : '2.0.2',
            retrofitSdkVersion  : '2.3.0',
            glideTrans          : '4.1.0',
            glide               : '4.9.0',
            room                : '2.0.0',
    ]
    retrofitDeps = [
            "retrofit"                : "com.squareup.retrofit2:retrofit:${dependVersion['retrofitSdkVersion']}",
            "retrofitConverterGson"   : "com.squareup.retrofit2:converter-gson:${dependVersion.retrofitSdkVersion}",
            "retrofitAdapterRxjava2"  : "com.squareup.retrofit2:adapter-rxjava2:${dependVersion.retrofitSdkVersion}"
    ]
    rxJavaDeps = [
            "rxJava"   : "io.reactivex.rxjava2:rxjava:${dependVersion.rxJava}",
            "rxAndroid": "io.reactivex.rxjava2:rxandroid:${dependVersion.rxAndroid}",
    ]
    glideDeps = [
            "glide"         : "com.github.bumptech.glide:glide:${dependVersion.glide}",
            'glideOKhttp'   : "com.github.bumptech.glide:okhttp3-integration:${dependVersion.glide}",
            "glideTrans"    : "jp.wasabeef:glide-transformations:${dependVersion.glideTrans}",
    ]
    roomDeps = [
            'room-runtime': "androidx.room:room-runtime:${dependVersion.room}",
            'room-rxjava': "androidx.room:room-rxjava2:${dependVersion.room}",
            "room-compiler": "androidx.room:room-compiler:${dependVersion.room}"
    ]
    kotlinDeps = [
            "kotlin"            :  "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version",
            "kotlin-coroutines" : "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2"
    ]

    compactDeps = [
            "recyclerview"      : 'androidx.recyclerview:recyclerview:1.0.0',
            'constraintlayout'  : 'androidx.constraintlayout:constraintlayout:1.1.3',
            'cardview'          :'androidx.cardview:cardview:1.0.0',
            'compact'           : 'androidx.appcompat:appcompat:1.0.0'
    ]
    commonDeps = [
            'multidex'              : 'androidx.multidex:multidex:2.0.0',
            "okHttp"                : 'com.squareup.okhttp3:okhttp:3.12.0',
            'eventBus'              : 'org.greenrobot:eventbus:3.0.0',
            'statusBar'             : 'com.jaeger.statusbarutil:library:1.5.0',
            'gson'                  : 'com.google.code.gson:gson:2.3.1',
            'wechatShare'           : 'com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.6.4',
            'bugly'                 : 'com.tencent.bugly:crashreport:3.2.3'

    ]
    retrofitLibs = retrofitDeps.values()
    rxJavaLibs = rxJavaDeps.values()
    glideLibs = glideDeps.values()
    roomLibs = roomDeps.values()
    kotlinLibs = kotlinDeps.values()
    compactLibs = compactDeps.values()
    commonLibs = commonDeps.values()
}

在项目的build.gradle中引入apply from "config.gradle", 然后在需要引入的各子业务module的build.gradle中加入对应的需要个入的依赖如 api rxJavaLibs,这样便能做到全局依赖版本的统一。

以上简单总结了一下组件化的相关架构思路,我一直觉得组件化并不是一门新的技术,它更多的是一种项目的架构方法,采用这种方法可以更加方便的管理代码,使我们代码以一种按模块的方式整合起来,满足特定场景的需求。

  • 采用面向接口编程去掉组件间的相互依赖
  • 当某一个模块在开发阶段能够单独运行起来,能大大减少编译的时间
  • 当模块粒度足够小,它就具体更好的适用性,如抽出取来一个分享的模块,日志的模块,可以适用于多个app

参考