概述
从 2016 年开始,模块化在 Android 社区越来越多的被提及。随着移动平台的不断发展,移动平台上的软件慢慢走向复杂化,体积也变得臃肿庞大,为了降低大型软件复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等等需求,模块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架 Atlas 就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。
那么什么是模块化呢,和我们常说的组件化又有什么联系和区别呢?根据《 Java 应用架构设计:模块化模式与 OSGi 》一书中对模块化的定义:模块化是一种处理复杂系统分解为更好的可管理模块的方式。对于这种概念性的解释,太过生涩难懂,不够直观。
那么究竟何为模块化呢?举个例子,相信随着业务的不断迭代,APK项目已经无限大了,以我们公司的电商项目为例,在迭代了5年后,apk的体积已经40M ,如果使用传统的ant打包大概差不多要近10分钟,如果用增量打包时间也要3-5分钟。但是可以发现,很多老的代码其实我们在最新的版本是不需要的,当然我们可以手动的将这些代码删除,但是又还怕啥时候用到。此时,最好的方法就是将这些模块独立成一个独立的工程,当需要的时候再引入进来,这就是模块化的一个背景。
所以,此处,我们对模块化和组件化做一个简单的定义: 模块化:指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,如订单模块(OrderModule)、特卖模块(SPecialModule)、即时通讯模块(InstantMessagingModule)等等。
组件化:组件是指通用的功能或者UI库可以做成一个功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等;
插件化:和模块化差不多,只是它是可以把模块打包成独立的apk,可以动态的加载,删除,独立的插件apk可以在线下载安装,有利于减少apk的体积和实现模块的热修复。目前热门的插件化方案有:阿里的atlas,360公司的RePlugin,滴滴的VirtualAPK等等;
例如,下面是模块化之前和模块化之后的项目的目录结构:
模块化的示意图可以用下面的模型表示:
模块化要解决的问题
要使用模块化开发Android项目,有以下几点需要注意:
- 模块间页面跳转(路由);
- 模块间事件通信;
- 模块间服务调用;
- 模块的独立运行;
- 其他注意事项; 为了方便讲解,我们以下面的项目为例:
这是一个常见的首页画面,该页面主要有首页、微聊、推荐和我的组成。我们将该4个Tab单独成4个独立的模块。
其中:
- app模块:主模块,主要进行搭载各个模块的功能;
- lib_base:对ARouter进行初始化,和放置一些各个模块公用的封装类;
- module_home,module_caht,module_recom,module_me:分别对应“首页”、“微聊”、“推荐”、“我的”模块。
ARouter模块化开发
ARouter各个模块的gradle配置
app模块是程序的容器,起到程序入口的作用,lib_base作为基础模块,用来将一些公共的库和对ARouter的初始化操作放在这一模块中。因此每个子模块都会用到它里面的内容,所以我们在 lib_base中添加如下内容。
代码语言:javascript复制 compile 'com.alibaba:arouter-api:1.2.4'
annotationProcessor "com.alibaba:arouter-compiler:1.1.4"
compile 'com.android.support:design:27.1.1'
compile 'org.simple:androideventbus:1.0.5.1'
compile 'com.alibaba:fastjson:1.2.31'
因为我们把拦截器等公用类放在base注册,在编译期间生成路径映射。所以还需要在build中加入如下配置:
代码语言:javascript复制defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
由于每个子模块都会用到lib_base里面的东西,所以需要在各子模块的build文件中导入(即module_home,module_caht,module_recom,module_me等模块中)如下配置:
代码语言:javascript复制//注意,此处也需要引入了com.alibaba:arouter-compiler:1.1.4
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
compile project(':lib_base')
同样,也需要在各子模块的build中加入如下配置。
代码语言:javascript复制defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
}
然后在app模块(也即是主模块)对各个子模块进行依赖。
代码语言:javascript复制compile project(':module_home')
compile project(':module_chat')
compile project(':module_recom')
compile project(':module_me')
子模块依赖规则配置
对于模块化项目,每个单独的Module 都可以单独编译成 APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个 Module 来整体编译打包。简单的说就是开发时是 Application,发布时是 Library。所以,需要在子模块中做如下的配置:
代码语言:javascript复制if(isBuildModule.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
同理,Manifest.xml 也需要有两套:
代码语言:javascript复制if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
同时,每个子模块的defaultConfig还需要增加如下配置:
代码语言:javascript复制defaultConfig {
if (!isNeedMeModule.toBoolean()) {
applicationId "com.xzh.module_me"
}
}
而上面的isBuildModule.toBoolean()判断条件,读取的是项目根目录下的gradle.properties配置文件。
代码语言:javascript复制# 是否需要单独编译 true表示需要,false表示不需要
isNeedHomeModule=false
#isNeedHomeModule=true
isNeedChatModule=false
#isNeedChatModule=false
isNeedRecomModule=false
#isNeedRecomModule=false
isNeedMeModule=false
#isNeedMeModule=false
然后根据上面的编译配置在app模块中添加如下依赖:
代码语言:javascript复制if (!isNeedHomeModule.toBoolean()) {
compile project(':module_home')
}
if (!isNeedChatModule.toBoolean()) {
compile project(':module_chat')
}
if (!isNeedRecomModule.toBoolean()) {
compile project(':module_recom')
}
if (!isNeedMeModule.toBoolean()) {
compile project(':module_me')
}
如果需要单独运行某个模块时,只需要修改gradle.properties对应的配置即可。例如,需要单独运行module_home模块时,只需要开启对于的配置即可isNeedHomeModule=true。
配置注意
由于配置后项目只有一个入口和启动文件(即app模块的MainActvity),所以其他子模块的MainActivity的intent-filter拦截要去掉,不然会有多个桌面入口。
代码语言:javascript复制<activity android:name=".MainActivity">
<!--<intent-filter>-->
<!--<action android:name="android.intent.action.MAIN" />-->
<!--<category android:name="android.intent.category.LAUNCHER" />-->
<!--</intent-filter>-->
</activity>
ARouter使用
以上面的效果实现为例,在MainActivity中使用TabLayout Adapter的形式搭建4个Tab页面。代码如下:
代码语言:javascript复制public class MainActivity extends AppCompatActivity {
private ViewPager mMViewPager;
private TabLayout mToolbarTab;
private int[] tabIcons = {
R.drawable.tab_home,
R.drawable.tab_weichat,
R.drawable.tab_recommend,
R.drawable.tab_user
};
private String[] tab_array;
private DemandAdapter mDemandAdapter;
private List<Fragment> fragments = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
}
private void init() {
initData();
initView();
setViewPagerAdapter();
setTabBindViewPager();
setItem();
}
private void initData() {
tab_array = getResources().getStringArray(R.array.tab_main);
fragments.clear();
fragments.add(FragmentUtils.getHomeFragment());
fragments.add(FragmentUtils.getChatFragment());
fragments.add(FragmentUtils.getRecomFragment());
fragments.add(FragmentUtils.getMeFragment());
}
private void initView() {
mMViewPager = (ViewPager) findViewById(R.id.mViewPager);
mToolbarTab = (TabLayout) findViewById(R.id.toolbar_tab);
}
private void setViewPagerAdapter() {
mDemandAdapter = new DemandAdapter(getSupportFragmentManager(),fragments);
mMViewPager.setAdapter(mDemandAdapter);
}
private void setTabBindViewPager() {
mToolbarTab.setupWithViewPager(mMViewPager);
}
private void setItem() {
for (int i = 0; i < mToolbarTab.getTabCount(); i ) {
mToolbarTab.getTabAt(i).setCustomView(getTabView(i));
}
}
public View getTabView(int position) {
View view = LayoutInflater.from(this).inflate(R.layout.item_tab, null);
ImageView tab_image = view.findViewById(R.id.tab_image);
TextView tab_text = view.findViewById(R.id.tab_text);
tab_image.setImageResource(tabIcons[position]);
tab_text.setText(tab_array[position]);
return view;
}
}
然后,使用ARouter来获取到各个模块的Fragment。
代码语言:javascript复制public class FragmentUtils {
public static Fragment getHomeFragment() {
Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Home_Fragment_Main).navigation();
return fragment;
}
public static Fragment getChatFragment() {
Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Chat_Fragment_Main).navigation();
return fragment;
}
public static Fragment getRecomFragment() {
Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Recom_Fragment_Main).navigation();
return fragment;
}
public static Fragment getMeFragment() {
Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Me_Fragment_Main).navigation();
return fragment;
}
}
而FragmentUtils使用了RouteUtils来定义具体的跳转协议。
代码语言:javascript复制public class RouteUtils {
public static final String Home_Fragment_Main = "/home/main";
public static final String Chat_Fragment_Main = "/chat/main";
public static final String Recom_Fragment_Main = "/recom/main";
public static final String Me_Fragment_Main = "/me/main";
}
上面的子模块使用的是Fragment,所以,在子模块中要使用Route说明。例如:
代码语言:javascript复制@Route(path = RouteUtils.Chat_Fragment_Main)
public class MainFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_weichat, null);
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
}
}
跨模块跳转
假如要实现跨模块跳转,首先在RouteUtils定义
代码语言:javascript复制public static final String Me_Login = "/me/main/login";
ARouter要跳转Activity,就在这个Activity上加入注解。
代码语言:javascript复制@Route(path = RouteUtils.Me_Login)
public class LoginActivity extends AppCompatActivity{
}
然后在需要跳转的地方添加如下代码:
代码语言:javascript复制ARouter.getInstance().build(RouteUtils.Me_Login).navigation();
实现ForResult返回数据
如果跨模块跳转需要返回数据,即Activity的StartActivityForResult,则可以使用下面的方式。
代码语言:javascript复制ARouter.getInstance().build(RouteUtils.Chat_ForResult).navigation(this, 666); //666即为Code
接收数据数据:
代码语言:javascript复制@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 666:
String name = data.getStringExtra("name");
UIUtils.showToast(name ",resultCode===>" resultCode);
break;
default:
break;
}
}
然后,接受返回数据:
代码语言:javascript复制Intent intent = new Intent();
intent.putExtra("name", "ForResult返回的数据");
setResult(999, intent);
finish();
使用Eventbus跨模块通信
使用Eventbus进行跨模块通信,首先在需要接受的地方定义一个订阅者。
代码语言:javascript复制@Subscriber(tag = EvenBusTag.GOTO_EVENTBUS)
public void onEvent(String s) {
UIUtils.showToast(s);
}
然后在发送方使用EventBus发送消息。例如:
代码语言:javascript复制@Route(path = RouteUtils.Me_EventBus)
public class EventBusActivity extends AppCompatActivity implements View.OnClickListener {
/**
* eventBus数据接收页面
*/
private TextView mTextView;
/**
* eventBus返回数据
*/
private Button mBtnBackData;
private String name;
private long age;
private EventBusBean eventbus;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_event_bus);
ARouter.getInstance().inject(this);
initData();
initView();
}
private void initData() {
name = getIntent().getStringExtra("name");
age = getIntent().getLongExtra("age", 0);
eventbus = getIntent().getParcelableExtra("eventbus");
}
private void initView() {
mTextView = (TextView) findViewById(R.id.textView);
mBtnBackData = (Button) findViewById(R.id.btn_back_data);
mBtnBackData.setOnClickListener(this);
mTextView.setText("name=" name ",tage=" age ",tproject=" eventbus.getProject()
",tnum=" eventbus.getNum());
}
@Override
public void onClick(View v) {
int i = v.getId();
if (i == R.id.btn_back_data) {
EventBus.getDefault().post(name, EvenBusTag.GOTO_EVENTBUS);
finish();
} else {
}
}
}
其实,ARouter的功能远不止于此,后面将为大家一一讲解,并最终自己实现一个模块间的路由。