组件化

  • 前言
  • 组件化
  • 配置
  • 动态改变组件的运行模式
  • 设置AndroidManifest.xml
  • 统一版本
  • 统一第三方库依赖
  • 动态配置组件依赖
  • 引用config文件
  • ButterKnife 报错问题
  • Application 初始化
  • 资源文件冲突问题
  • 组件跳转和通信
  • 总结


前言

在上一篇文章Android开发之移动端项目架构演化之路 从模块化,组件化再到插件化中我们讲到了移动端项目架构中的组件化开发,这也是目前很火热的一种架构选择,但是也不用盲目跟风,毕竟实现组件化开发是需要额外增加一些开发成本的,如果你的开发团队就一个人或者1-3个人这种小团队,这说明这个项目规模并不大,使用单项目架构或者模块化开发也就足够了,采用组件化就没有什么性价比了

但是如果项目规模比较大或者开发人员数量上来后,组件化开发的优势就体现出来了

那怎么进行组件化开发呢?本篇文章就来一一解析

组件化

在Android Studio我们新建一个工程,那这个应用有什么呢?我这里就仿照微信主页,有四个tab,然后在工程里新建四个Module,这四个tab分别是四个组件,然后可以单独进行开发编译,最后被主应用依赖打包成一个完整的APP;项目目录如下

Android Bundle怎么用 android bundle组件_Android Bundle怎么用

  • app模块是主应用模块,这里面不做具体业务开发,只是集成各个组件,最终打包成一个完整的应用
  • baselibrary是公共类库模块,是library模式,主要功能有第三方库依赖,封装网络请求功能,数据存储,多媒体功能,各种工具类等
  • chat,contact,find,mine是四个业务组件,是application模式,分别是主页四个tab,这些组件在集成模式下作为依赖库供app模块使用,在组件开发模式下以独立APP存在;当然了随着业务不断增加,这里肯定不止这四个组件,我这里只是作为样例使用

是新建app还是library见下图

Android Bundle怎么用 android bundle组件_Android Bundle怎么用_02

所以组件化就是在开发时分成N个单独的APP进行开发,到发布时将多个app打包成一个app

配置

动态改变组件的运行模式

如果只是这样新建一个项目,然后在里面新建几个module,app模块是使用不了其它几个组件的,因为这些业务组件都是独立的应用,不是依赖库,不能被app模块依赖,所以就需要动态的修改这些业务组件的模式,怎么做呢?

还记得每个模块都有一个build.gradle文件吧,这个组件是library还是application是由该文件的第一句代码决定的,那么我们就需要动态的改变这个值

apply plugin: 'com.android.application'

我们先在工程根目录下新建一个config.gradle文件,在里面定义几个属性,如下

Android Bundle怎么用 android bundle组件_组件化_03

ext{

    /*配置各组件是否单独运行*/
    isRunAloneChat = false
    isRunAloneContact = false
    isRunAloneFind = false
    isRunAloneMine = false

}

接下来我们就需要在每个组件下的build.gradle中根据这个属性去设置三个地方

  • 设置application或者library
if (rootProject.ext.isRunAloneContact) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

也就是说如果这个值是true,说明这个组件需要单独开发测试,那就设置成application
如果是false,说明这个组件需要被app模块依赖,成为完整应用的一部分

  • 在defaultConfig配置中要注意,如果这个组件是依赖库,那么是没有applicationid的,所以也需要修改
if (rootProject.ext.isRunAloneContact) {
      applicationId "com.mango.contact"
}

设置AndroidManifest.xml

在最后打包成一个完整应用时,所有的manifest文件会合并成一个,所以只能有一个启动Activity;如果每个组件在集成模式下也使用默认的AndroidManifest.xml文件,那么就会有多个Application和程序入口,显然会发生冲突;那么就要注意作为application的AndroidManifest.xml文件跟作为library的该文件是不同的,所以需要动态改变组件所使用的AndroidManifest.xml文件

先在main目录下新建一个debug目录,在里面存放一个在application模式下该组件使用的AndroidManifest.xml文件,如图

Android Bundle怎么用 android bundle组件_Android Bundle怎么用_04

library模式下使用的文件需要修改成如下内容,application模式下无须修改该文件

```
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.mango.contact">

    <application>
        <activity android:name=".MainActivity">
        </activity>
    </application>

</manifest>
```
最后在build.gradle中做一下配置

```
sourceSets{
        main {
            if (rootProject.ext.isRunAloneContact) {
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
```

统一版本

为了避免版本混乱,需要统一配置app模块和每个组件的编译版本号,在config.build中代码如下

/*全局配置gradle编译参数*/
    android = [
            compile_sdk_version : 26,
            build_tools_version : "27.0.1",
            min_sdk_version : 19,
            target_sdk_verison : 24,
            version_code : 1,
            version_name :"1.0.0"
    ]

接下来在组件的build.gradle中进行如下修改

compileSdkVersion rootProject.ext.android.compile_sdk_version
    buildToolsVersion rootProject.ext.android.build_tools_version

    defaultConfig {
        if (rootProject.ext.isRunAloneContact) {
            applicationId "com.mango.contact"
        }
        minSdkVersion rootProject.ext.android.min_sdk_version
        targetSdkVersion rootProject.ext.android.target_sdk_verison
        versionCode rootProject.ext.android.version_code
        versionName rootProject.ext.android.version_name
    }

统一第三方库依赖

为了避免每个组件重复依赖第三方库,将每个组件公用的依赖都在baselibrary模块中添加,这样所有的组件只需要依赖这个模块就行了;这样每个组件的依赖就很简单了

dependencies {
    	compile fileTree(include: ['*.jar'], dir: 'libs')
    	compile project(':baselibrary')
	}

同时为了方便依赖管理,将具体的依赖第三方库的版本在config.build中进行配置

/*全局第三方依赖统一管理*/
    dependencies = [
            "appcompat-v7"    : "com.android.support:appcompat-v7:25.3.1",
            "constraint"      : "com.android.support.constraint:constraint-layout:1.0.2",
            "junit"           : "junit:junit:4.12"
    ]

在baselibrary模块中的build.gradle文件就可以这样写了

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile rootProject.ext.dependencies["appcompat-v7"]
    compile rootProject.ext.dependencies["constraint"]
    testCompile rootProject.ext.dependencies["junit"]
}

动态配置组件依赖

默认情况下,app模块是依赖所有组件和baselibrary模块的,要注意的一点是每个组件如果是独立运行的话,那它是不能作为依赖库供app模块使用的,所以在app模块下的build.gradle文件中需要进行依赖判断

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile project(':baselibrary')
    if (!rootProject.ext.isRunAloneChat) {
        compile project(':chat')
    }
    if (!rootProject.ext.isRunAloneContact) {
        compile project(':contact')
    }
    if (!rootProject.ext.isRunAloneFind) {
        compile project(':find')
    }
    if (!rootProject.ext.isRunAloneMine) {
        compile project(':mine')
    }
}

引用config文件

所有这些配置完后,需要在项目根目录的build.gradle中引用config.gradle文件,这样所有组件才能使用这里的属性

// 直接在第一行引用就可以
apply from: "config.gradle"

最后要切记在AS的工具栏选择build的Rebuild Project,重新编译下工程,这样引用才能生效


ButterKnife 报错问题

单工程使用这个库是没有问题的,但是在组件化使用的时候就报错了,提示元素值必须是常量表达式,在主app模块里的R文件中,这些id都是常量,但是在组件中的R文件里就不是常量了,原因:

从ADT14开始Library中的R文件才从静态常量变为非常量.因为如果在多个Library中可能出现id冲突的问题.在ADT14以前则采用的是将所有的资源文件和相关的代码重新随着主项目一起编译,导致编译速度过慢.因此,从ADT14开始就变成了非常量的id了。

那么在现在业务组件是library形式越来越多的情况下,怎么解决这个错误呢?

我使用的Android Studio是3.0以下的版本,等过两天有时间了再升级吧
第一步:在项目根目录的build.gradle中添加如下代码

dependencies {
        classpath 'com.jakewharton:butterknife-gradle-plugin:8.4.0'
    }

我这里没有使用最新的10.0的版本,因为新版本要求更高的gradle版本

第二步:在你要使用的业务组件library中的build.gradle添加如下代码

if (rootProject.ext.isRunAloneChat) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'//这一句才是重点

第三步:我所有的组件library都依赖一个baselibrary,所以butterKnife的依赖就放在这里了,避免每个组件都重复引入;但是注解依赖必须要添加到每个组件自己的build.gradle中,包括主app模块

annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]

第四步:主app模块可以像以往那样使用butterknife,但是组件中使用ButterKnife的地方都需要将R改成R2

@OnClick(R2.id.chat_btn_intent)
    public void clickIntent(){
        Toast.makeText(getContext(),"clickIntent",Toast.LENGTH_LONG).show();
    }

这里如果onClick的绑定事件中有多个id,那么方法里不要使用switch- case 找id,而是使用if-else

Application 初始化

每个应用只有一个Application,所以只需要定义一个BaseApplication继承Application,每个组件和主app模块的自定义Application类去继承这个基类就行了,这样大家都能获取到整个应用上下文了

在baselibrary模块中写一个Application的基类,所有组件的Application都继承这个类

public abstract class BaseApplication extends Application{

    private String TAG = BaseApplication.class.getSimpleName();

    //全局唯一
    private static Application application;

    /**
     * 获取全局应用上下文
     * @return
     */
    public static Application getApplication() {
        return application;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        application = this;
        startApp();
    }

    /**
     * 每个组件单独运行时如果需要初始化资源,可以在这个方法中进行
     */
    public abstract void startApp();


}

这样每个组件定义一个自己的Application类去继承这个基类,比如

public class ChatApp extends BaseApplication {

    private String TAG = ChatApp.class.getSimpleName();

    @Override
    public void startApp() {

    }

}

资源文件冲突问题

这里的问题就是每个组件下使用的一些资源文件比如layout,icon,string等名称不能重复,要不然打包后会被覆盖,就没办法引用到正确的资源

目前也没有什么好的解决方案,基本上靠开发人员在添加资源的时候手动加上前缀名来区分,最后在合并代码的时候进行检查

同时可以在每个组件的build.gradle中添加一句代码,比如是find组件:

resourcePrefix "find_"

这只是一个软约束,它并不会自动给我们加上这个前缀,而只是会提示报错,需要自己去修改,如下

Android Bundle怎么用 android bundle组件_Android Bundle怎么用_05


Android Bundle怎么用 android bundle组件_android_06


Android Bundle怎么用 android bundle组件_android_07

这时候就需要我们手动加上前缀

Android Bundle怎么用 android bundle组件_android_08

其它的比如一些layout,anim等xml文件不加前缀也会报错;但是图片资源不加前缀是不会报错的,所以就需要开发者自己把关了,添加的时候手动加上;还有就是布局文件中的资源id也不要重复

组件跳转和通信

进行组件化开发,各个组件之间没有依赖关系,没有办法直接跳转;虽然主app模块是依赖所有组件的,但是要避免它们直接跳转,这也是为了解耦,一旦直接跳转,这样某个组件出问题时,比如拿掉了,主app模块去调用的时候就会报错,所以就需要借助第三方实现

组件跳转现成的框架有ARoute,是阿里开源的一款路由框架:ARoute

也可以参考Android 路由框架ARouter最佳实践

组件通信的时候也是同理,避免组件直接通信,可以使用EventBus框架进行通信

我这里为了降低大家的学习成本,使用接口的方式来实现跳转或者通信

现在的业务场景是主界面有四个tab,对应着四个组件,每个组件提供一个Fragment供app模块组装成首页,那么app模块在用户点击底部tab的时候需要去通知每个组件的Fragment显示,其它组件的Fragment隐藏

第一步:就是定义一个业务接口:

public interface IFragmentService {

    void showFragment(FragmentManager manager, int containerId, Bundle bundle);
    
    void hideFragment(FragmentManager manager);
}

第二步:就是每个组件需要定义一个类去实现这个接口,比如:

public class ChatFragmentService implements IFragmentService {

    private ChatFragment chatFragment;

    @Override
    public void showFragment(FragmentManager manager, int containerId, Bundle bundle) {

        if (chatFragment == null) {
            chatFragment = ChatFragment.newInstance(bundle);
            manager.beginTransaction()
                    .add(containerId, chatFragment)
                    .commit();
        } else {
            manager.beginTransaction()
                    .show(chatFragment)
                    .commit();
        }

    }

    @Override
    public void hideFragment(FragmentManager manager) {

        if (chatFragment == null) return;

        manager.beginTransaction().hide(chatFragment).commit();
    }

}

第三步:就是为了方便主app模块使用这些组件的业务接口,提供一个工厂类

public class ServiceFactory {

    private static final ServiceFactory instance = new ServiceFactory();

    private ServiceFactory(){}

    public static ServiceFactory getInstance(){
        return instance;
    }

    public Map<String,IFragmentService> FRAGMENT_SERVICE = new HashMap<>();

    public IFragmentService getFragmentService(String tag) {
        return FRAGMENT_SERVICE.get(tag);
    }

    public void setFragmentService(String tag,IFragmentService fragmentService) {
        FRAGMENT_SERVICE.put(tag,fragmentService);
    }

    public void clearFragmentService(){
        FRAGMENT_SERVICE.clear();
    }
}

第四步:在APP启动的时候将每个组件的业务接口实现类添加到工厂类,为了避免耦合,我们先在baselibrary中新建一个配置类,统一管理这些业务接口

public class AppConfig {

    public static final String CHAT_FRAGMENT_SERVICE = "com.mango.chat.ChatFragmentService";
    public static final String CONTACT_FRAGMENT_SERVICE = "com.mango.contact.ContactFragmentService";
    public static final String FIND_FRAGMENT_SERVICE = "com.mango.find.FindFragmentService";
    public static final String MINE_FRAGMENT_SERVICE = "com.mango.mine.MineFragmentService";

    /**
     * 各组件的显示tab的业务接口实现类
     */
    public static final String[]  FRAGMENT_SERVICE = {
            CHAT_FRAGMENT_SERVICE,
            CONTACT_FRAGMENT_SERVICE,
            FIND_FRAGMENT_SERVICE,
            MINE_FRAGMENT_SERVICE
    };

}

接着在主app的Application中遍历这个数组,通过反射构造出实例,然后塞到工厂类中

public class MainApp extends BaseApplication {

    private String TAG = MainApp.class.getSimpleName();

    /**
     * 保存每个接口的初始化情况
     * 因为有的组件可能会没有被主app依赖,那这个组件里的业务接口实现类就获取不到
     * 这样在后续使用的过程就能避免异常,同时添加一些提示信息
     */
    public Map<String,Boolean> initService = new HashMap<>();

    public Map<String, Boolean> getInitService() {
        return initService;
    }

    @Override
    public void startApp() {
        initService();
    }


    public void initService() {

        for (String service : AppConfig.FRAGMENT_SERVICE) {

            boolean init = false;
            try {
                Class<?> clazz = Class.forName(service);
                Object o = clazz.newInstance();
                if(o instanceof IFragmentService){
                    ServiceFactory.getInstance().setFragmentService(service,(IFragmentService)o);
                    init = true;
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }finally {
                initService.put(service,init);
            }
        }

    }
}

第五步:在主app模块的MainActivity中组装这些组件

public class MainActivity extends BaseActivity {

    @BindView(R.id.iv_chat)
    ImageView iv_chat;
    @BindView(R.id.iv_contact)
    ImageView iv_contact;
    @BindView(R.id.iv_find)
    ImageView iv_find;
    @BindView(R.id.iv_mine)
    ImageView iv_mine;

    private FragmentManager fragmentManager;
    private Map<String,ErrorFragment> unInitFrag = new HashMap<>();

    private String mLastTag;

    @Override
    public int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    public void initFragment() {
        fragmentManager = getSupportFragmentManager();
        setSelectTabIcon(0);
        setSelectFragment(AppConfig.CHAT_FRAGMENT_SERVICE);
    }

    @OnClick(R.id.chat)
    public void clickChat(){
        setSelectTabIcon(0);
        setSelectFragment(AppConfig.CHAT_FRAGMENT_SERVICE);
    }

    @OnClick(R.id.contact)
    public void clickContact(){
        setSelectTabIcon(1);
        setSelectFragment(AppConfig.CONTACT_FRAGMENT_SERVICE);
    }

    @OnClick(R.id.find)
    public void clickFind(){
        setSelectTabIcon(2);
        setSelectFragment(AppConfig.FIND_FRAGMENT_SERVICE);
    }

    @OnClick(R.id.mine)
    public void clickMine(){
        setSelectTabIcon(3);
        setSelectFragment(AppConfig.MINE_FRAGMENT_SERVICE);
    }

    private void setSelectTabIcon(int position){
        iv_chat.setSelected(position == 0 ? true : false);
        iv_contact.setSelected(position == 1 ? true : false);
        iv_find.setSelected(position == 2 ? true : false);
        iv_mine.setSelected(position == 3 ? true : false);
    }

    private void setSelectFragment(String selectTag){

        String newTag = null;
        for (int i=0; i<AppConfig.FRAGMENT_SERVICE.length; i++) {
            if (TextUtils.equals(AppConfig.FRAGMENT_SERVICE[i] , selectTag)) {
                newTag = selectTag;
                break;
            }
        }

        if (!TextUtils.equals(mLastTag,newTag)) {

            if (!TextUtils.isEmpty(mLastTag)) {
                if ((((MainApp) MainApp.getApplication()).getInitService().get(mLastTag))) {
                    ServiceFactory.getInstance().getFragmentService(mLastTag).hideFragment(fragmentManager);
                } else {
                    fragmentManager.beginTransaction().hide(unInitFrag.get(mLastTag)).commit();
                }
            }

            if ((((MainApp) MainApp.getApplication()).getInitService().get(newTag))) {
                ServiceFactory.getInstance().getFragmentService(newTag).showFragment(fragmentManager, R.id.main_container, null);
            } else {
                if (unInitFrag.get(newTag) == null) {
                    ErrorFragment errorFragment = new ErrorFragment();
                    fragmentManager.beginTransaction().add(R.id.main_container, errorFragment).commit();
                    unInitFrag.put(newTag,errorFragment);
                } else {
                    fragmentManager.beginTransaction().show(unInitFrag.get(newTag)).commit();
                }
            }

            mLastTag = newTag;
        }

    }

}

效果如图

Android Bundle怎么用 android bundle组件_Android_09

第六步:假如我们不要其中第二个组件(即第二个tab),代码不需要动,只需要将config.build文件修改下,看结果

/*配置各组件是否单独运行*/
    isRunAloneChat = false
    isRunAloneContact = true
    isRunAloneFind = false
    isRunAloneMine = false

Android Bundle怎么用 android bundle组件_组件化_10

第七步:这里就涉及到通信的问题了,比如我们在第一个tab中的一个按钮进行点击,然后要跳转到第二个tab,同时携带一些数据过去在第二个tab进行Toast

  • 先在baselibrary里定义好业务接口
public interface IGoContact {

    boolean goContact(Activity activity,String msg);
}
  • 这个业务是第一个tab,也就是chat组件要告诉主app模块现在去第二个tab,所以该接口由主app模块实现
public class AccessContactService implements IGoContact {


    @Override
    public boolean goContact(Activity activity , String msg) {

        if (activity == null ) return false;
        if (! (activity instanceof MainActivity)) return false;

        MainActivity mainActivity = (MainActivity) activity;
        mainActivity.setSelectTabIcon(1);
        mainActivity.setSelectFragment(AppConfig.CONTACT_FRAGMENT_SERVICE,msg);
        return true;
    }

}
  • 接下来就是chat组件要调用这个实现类了,这里在ChatFragment通过反射获取它的实例
private IGoContact goContact;
    @Override
    public void initData(View view) {
        super.initData(view);

        try {

            Class<?> clazz = Class.forName("com.mango.compontent.AccessContactService");
            Object o = clazz.newInstance();
            if (o instanceof IGoContact) {
                goContact = (IGoContact) o;
            }
        } catch (Exception e) {

        }
    }
  • 最后就是在点击事件调用这个实例的方法
@OnClick(R2.id.chat_btn_intent )
    public void clickIntent(){
        if (goContact != null) {
            goContact.goContact(getActivity(),ACCESS_CONTACT_FRAGMENT);
        }
    }
  • 还有一点就是在第二个tab接收消息
public void getMsg(Bundle bundle){
        String msg = bundle.getString("msg");
        if (TextUtils.isEmpty(msg)) return;
        Toast.makeText(getContext(),msg,Toast.LENGTH_LONG).show();
    }

最后效果如图

Android Bundle怎么用 android bundle组件_Android Bundle怎么用_11

整个demo可以访问MangoCompontent

总结

组件化的开发随着越来越深入,你碰到的坑将会越来越多,但是它所带来的优点还是值得你这么做的,它的第一个麻烦的地方就是配置,当你功能越来越复杂,组件越来越多,配置也会随着增多;第二麻烦点就是组件通信和跳转的问题,当然了如果你使用第三方框架,这些将会变得简单,但是你要是全都自己写,那将是很大的一个工作量;后续碰到的坑再来补上