关于什么是组件化、为什么要进行组件化以及实施组件化的基本流程网上一搜一大把,这里不做过多说明,不了解的话可以Google一下。这里主要记录一下组件化开发的一些心得和踩的一些坑。
。
在gradle.properties文件里定义一个常量 IsBuildApp = false ,表示是否把组件module作为单独的app运行。定义好了这个常量后,在项目的任何一个gradle文件里都可以读取到这个值,那么就用这个值来作为module组件是否需要单独运行的开关。
// 在module组件的gradle里配置如下,gradle.properties 中的数据类型都是String类型,这里需要做一下转换 if (IsBuildApp.toBoolean()){ apply plugin: 'com.android.application' }else { apply plugin: 'com.android.library' }
我们知道android的四大组件、权限等都是需要注册的,当module单独运行的时候,肯定需要一个清单文件注册组件和申请权限,但是当module作为app的一个子组件存在的时候,清单文件是要合并到app的壳工程中的,这个时候如果每个module都有自己的启动页面和自定义application的话,就会引起冲突。
为了解决这个问题,那就需要根据module是否需要单独运行来配置不同的清单文件。在java同级目录新建independent目录,在此目录下创建项目module需要单独运行的清单文件和application。然后在module的gradle文件里指定清单文件路径,代码如下:
// 在android领域里指定清单文件的路径 sourceSets { main { if (IsBuildApp.toBoolean()) { // 单独作为app运行的清单文件,这里可以添加启动页面、自定义application等。 manifest.srcFile 'src/main/independent/AndroidManifest.xml' } else { // 作为组件的清单文件 manifest.srcFile 'src/main/AndroidManifest.xml' //release模式下排除independent文件夹中的所有Java文件 java { exclude 'independent/**' } } } }
这样配置完成以后,作为组件的清单文件是不能有自己的启动页面、application、appname等属性的,下面看一下完整的配置:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.article.demos.vue"> <application android:theme="@style/AppTheme"> <activity android:name=".ui.VueActivity" /> </application> </manifest>
下面看一下独立运行模式下的清单文件:
// 作为独立app运行的清单文件,注意这里我设置了主题,不然的话会报错。 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.article.demos.main"> <application android:theme="@style/AppTheme"> <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>
独立运行的话,就和正常的app清单文件一样,要有启动页面,application标签可以添加label、icon、自定义application等,就不多说啦。
在commonlibrary中创建自定义application,因为其他的module都依赖这个module,所以其他的module都可以获取到这个全局的application。另外,组件在独立运行模式下的application,继承我们自定义这个BaseApplication就可以了。因为我们在release模式下,排除了所有independent文件夹下的java文件,所以作为组件运行时,并不会产生application的冲突,配置如下:
sourceSets { main { if (IsBuildApp.toBoolean()) { manifest.srcFile 'src/main/independent/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' //release模式下排除independent文件夹中的所有Java文件 java { exclude 'independent/**' } } } }
为了避免重复依赖三方库的问题,我们的三方库依赖统一放在commonlibrary的module中,这样既可以避免重复依赖,又方便管理。然后我们在app的module里,如下引用即可:
dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') if (IsBuildApp.toBoolean()) { implementation project(':commonlibrary') } else { implementation project(':androidmodule') implementation project(':vuemodule') implementation project(':kotlinmodule') implementation project(':javamodule') } }
资源冲突主要是指各个module里的资源文件名冲突的问题,如果命名一样,合并的时候便会产生冲突。
解决冲突主要有两个解决方案,一个是约定规则,比如资源名约定都以module名开头。
方案二是通过gradle脚本来设置,在各个组件的gradle文件里添加如下代码:
resourcePrefix "module名称_"
但是这种配置有限制,比如只能限定xml里的资源,所以并不推荐这种方式。
因为组件是相互隔离的,我们并不能显式跳转,这里我们选用阿里巴巴的Arouter路由跳转,项目的地址 github.com/alibaba/ARo… 。
这里需要特别说明一下,需要跳转的目标module需要引入arouter的注解处理器,否则无法处理router注解会出现路径不匹配的问题:
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
同时,改module的defaultconfig里也别忘记配置moduleName
javaCompileOptions { annotationProcessorOptions { arguments = [ moduleName : project.getName() ] } }
跨moduel交互一般是指module间通信和module间的相互调用。module间通信这里选用eventbus,很简单,就不过多说明了。
下面说一下同级module直接的通信,比如我在任何一个页面要调用loginModule里的微信登录方法,因为各个module是互相独立的,互不依赖,想要直接调用基本不可能。目前网上发现有两种解决方案,一个是写一个反射工具类,通过反射获取到要调用的类,然后调用相应的方法。另一个是通过commonModule做一下桥接, 了解更多可以参考这里。 不过感觉用Arouter能更优雅的实现,下面具体讲一下利用arouter来实现。
首先,在公共module里创建一个接口IService
public interface IService extends IProvider{ String wxLogin(); }
接口里定义一个微信登录的伪代码,然后在我们的登录组件里,实现该接口并添加route注解
@Route(path = Constant.WX_LOGIN) public class WxTest implements IService{ @Override public void init(Context context) { } @Override public String wxLogin() { return "wxlogin"; } }
其中 Constant.WX_LOGIN是我定义的一个字符串常量
public static final String WX_LOGIN = "/wx/login";
以上两步就把工作做完了,下面只需要在需要调用的页面调用登录就行了。首先,我们获取到IService
/** * 推荐使用方式二来获取IService */ // IService iService = (IService) ARouter.getInstance().build(Constant.WX_LOGIN).navigation(); IService iService = ARouter.getInstance().navigation(IService.class);
拿到IService后,就可以放心大胆的调用登录方法就行了。
mBinding.btLogin.setOnClickListener(v -> { String s = iService.wxLogin(); Toast.makeText(getContext(), s, Toast.LENGTH_SHORT).show(); });
一般的项目首页都是一个activity和多个fragment组成。由于组件间的隔离,我们在首页里怎么获取到其他组件里的fragment呢?开篇的两个参考文章分别使用了两种不同的方式,有兴趣的朋友可以看看。各有利弊吧,一个是查询所有,太耗时。一个是直接反射获取,但是好像有点违背组件隔离,需要知道fragment的全路径。
这里我参考了《Android组件化架构》一书,使用arouter来获取。其实三种方式获取的原理一样,都是通过反射。我们看一下arouter的注解的源码就知道:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.CLASS) public @interface Route {……}
可以看到Route注解的retention是CLASS,也是通过反射来获取。
(1)使用dataBinding的话,每个module的gradle文件里都要加上dataBinding的支持,否则无法生成相应的binding类
// 每个module都加上dataBinding的支持,否则无法生成相应的binding类 dataBinding { enabled = true }
(2)java8的支持一样要每个module都要单独配置
compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }
(3)升级到as 3.1.2后,出现无法访问TaskStackBuilder的问题
检查一下你的support包,将你的support包更新到27或以上即可。
(4)如果使用有自定义注解annotation的话,如果编译报错 Annotation processors must be explicitly declared now...,那么在commonlibrary的gradle文件的defaultConfig里添加如下代码:
// Annotation processors must be explicitly declared now javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
(5)如果你组件化开发,子module中无法使用butterknife的话,网上自行搜解决方案吧( ♀️)
关于为何出现这个问题,推荐一篇博文 R.java、R2.java是时候懂了
(6)其他问题本篇博客会持续更新……
最后 附上完整的demo地址 ,如果对你有帮助麻烦start鼓励一下,你的鼓励是我前进的动力。