命运多舛,刚加入一家做区块链的公司1月有余,今天面临公司全员降薪,裁员等等不利因素。
加入公司1月有余,重构了网络层的使用和一些IM的自定义消息问题,webview交互。本雄心壮志推动组件化的,奈何......算了,进入正题
为什么要项目组件化
当前项目整体结构图
image.png
随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,
APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个Developer都要其他模块的代码,将很难进行多人协作开发
Android项目在编译代码的时候电脑会非常卡,
单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,
单元测试根本无从下手
无法协同开发工作,代码风格控制不便
模块之间耦合严重,互相调用,不断持续下去将会无限耦合,迭代维护难度增大
所以必须要有更灵活的架构代替过去单一的工程架构。
组件化实现
image.png
整个项目被切割为了无数个app,无数个app均可单独运行
如何组件化
组件化架构图:
image.png
名称 | 含义 |
---|---|
App模式 | 将所有业务组件集合成为一个App |
组件模式 | 每个业务组件可单独作为App运行 |
App壳工程 | 融合业务组件的工程壳(可写一定业务,也可以不写) |
业务组件 | 根据对应业务的一个App |
公共组件 | 所有业务组件所需要集合使用的 |
SDK组件 | 第三SDK或者自身业务的进一步封装,将归纳到公共组件中 |
Base层 | 抽象业务层所需要的公共操作属性等 |
组件化架构的意图:
告别臃肿的项目结构
使单一业务形成app组件,便于调试
单一组件利于单元测试
协同开发,隔离组件与组件,模块与模块之间的耦合
加快开发效率,告别编译一次Xmins的时长
降低团队成员对项目某一业务的熟悉度,仅需关注自身所负责的业务
git权限控制,避免共享式开发
新业务可直接用组件形式开发,开发完成即可单元测试甚至单独给予测试人员测试,测试完毕即可整合到壳工程中
各业务研发可以互不干扰、提升协作效率,并控制产品质量;
组件化痛点(以下痛点仅介绍初级手动切换方式,后续将通过插件自动完成以下特性)
1、app与lib之间的切换完成
2、manifest的自动合并
3、组件的启动activity的自定义
4、组件的application自定义
5、组件路径的自动依赖处理
痛点 | 含义 |
---|---|
组件单独运行 | 如何将一个庞大的项目分而治之,可单独运行和集成为一个整体 |
UI跳转 | 每个业务组件之间如何跳转 |
组件通信 | 如何在业务之间进行数据传递 |
代码隔离 | 组件与组件之间如何避免直接引用 |
组件单独运行
if (isDebug) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' }
isDebug可定义于gradle.properties
中,这样在每个module的build.gralde
中都可以读取到该字段来动态定义组件作为app还是lib的存在。
组件与组件之间的Manifest合并问题
总所周知,Android的运行避免不了每个Activity的定义与权限的声明,因为我们的组件可作为组件单独运行,每一个组件都拥有自己的Application与Activity、AndroidManifest,那么我们在合并的时候必然导致冲突。
这个时候我们gradle又派上了用场,我们可以分别定义2个AndroidManifest,在组件开发阶段和集成模式下使用的是不同的AndroidManifest,并且在集成模式时将开发阶段的AndroidManifest剔除
sourceSets { main { if (rootProject.ext.isBuildApp) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { //移除debug资源 manifest.srcFile 'src/main/release/AndroidManifest.xml' java { exclude 'debug/**' } } } }
Debug模式作为App单独运行的AndroidManifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.allure.module.login"><application android:name="debug.LoginApplication" android:theme="@style/AppTheme" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true"> <activity android:name=".LoginActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <data android:scheme="allure" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity></application></manifest>
集成模式下的AndroidManifest
集成模式下,壳工程已经含有了Application与一些通用的配置(如Style的定义,Lancher等的定义),这时我们应只配置具体的Activity
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.allure.module.login"> <application android:allowBackup="true" android:label="@string/app_name" android:supportsRtl="true"> <activity android:name=".LoginActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="allure" /> </intent-filter> </activity> </application></manifest>
全局的Application处理
上面的合并Manifest中我们处理了合并问题,但实际运用中可能说某一个单一组件需要使用某一个SDK并且进行初始化。
在组件模式下我们单独定义了一个Application来处理,但实际合并过程,壳工厂已经包含了自的Applictaion,我们自定义的将会无效,这时怎么处理
处理方案一:下沉Application处理
我们可以将Application下沉到Base层或者Common层。在其中定义BaseApplication,将所有的第三方SDK初始化等操作放置于其中,组件将依赖Base层来达到目的
但是这样做个人认为在组件中依赖了Base层,依然不太优雅,这时推荐方案二
处理方案二:代理反射处理Application初始化
用过SDK的朋友都知道,SDK在早起阶段会有自己的Application,开发者使用需要继承SDK的Application,如果多个SDK都这样处理,那开发者受的苦就大了。(Java的单继承限制)
这时,我们考虑将Application的onCreat代理出去,让他人使用,然后将其反射处理初始化
接口:
public interface ApplicationImpl { void onCreate(Application baseApplication); }
在单一组件进行方法实现:
public class LoginApplication implements ApplicationImpl { private static final String TAG = "LoginApplication"; @Override public void onCreate(Application baseApplication) { Log.e(TAG, "初始化LoginApplication"); } }
public class ShopApplication implements ApplicationImpl { private static final String TAG = "ShopApplication"; @Override public void onCreate(Application baseApplication) { Log.e(TAG, "初始化ShopApplication"); } }
反射对象:
public class ModulesApplicationConfig { public static final String[] MODULES_LIST = { "com.allure.login.application.LoginApplication", "com.allure.shop.application.ShopApplication" }; }
for (String modulesImpl : ModulesApplicationConfig.MODULES_LIST) { try { Class<?> aClass = Class.forName(modulesImpl); Object object = aClass.newInstance(); if (object instanceof ApplicationImpl) { ((ApplicationImpl) object).onCreate(this); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } }
单一组件运行结果:
com.allure.modularization E/ShopApplication: 初始化ShopApplication
组件与组件之间的UI跳转
在常规开发下,因为我们的app全部处于一个工程之下,可采用显示或者隐式的跳转都可以,但如今组件作为单一模式如何跳转,这时,我们可以想到中间件来处理,这里非常感谢阿里开源的ARouter
image.png
我们所有的跳转都路由注册,统一由Arouter进行分发控制,避免了模块组件之间的直接接触
同时,路由可做的远不仅如此,还可对WebView与Native进行混合开发的App进行优雅控制,具体用法自行查看README
组件与组件之间的通信
当某一个组件需要另外一个组件的数据时怎么办?
在common层创建Provider
public interface ILoginProvider extends IProvider { LoginInfoBean getLoginInfo(); boolean isLogin(); void start2Login(); @Override void init(Context context); }
登录组件进行具体实现:
@Route(path = ARouterPathConfig.SERVICE_LOGIN)public class LoginProviderImpl implements ILoginProvider { private static final String TAG = "LoginProviderImpl"; private Context mContext; @Override public void init(Context context) { this.mContext = context; } /** * 其他快莫获取此模块组件的信息 * * @return */ @Override public LoginInfoBean getLoginInfo() { //此处可以做判断是否登录处理 LoginInfoBean loginInfoBean = new LoginInfoBean(); loginInfoBean.setAge("18"); loginInfoBean.setName("inChat"); loginInfoBean.setLogin(true); return loginInfoBean; } @Override public boolean isLogin() { return true; } @Override public void start2Login() { ARouter.getInstance().build(ARouterPathConfig.LOGIN_START) .navigation(mContext, new NavigationCallback() { @Override public void onFound(Postcard postcard) { LogUtils.d("onFound"); } @Override public void onLost(Postcard postcard) { LogUtils.d("onLost"); } @Override public void onArrival(Postcard postcard) { LogUtils.d("onArrival"); } @Override public void onInterrupt(Postcard postcard) { LogUtils.d("onInterrupt"); } }); }
获取信息的组件:
ILoginProvider iLoginProvider = (ILoginProvider) ARouter.getInstance().build(ARouterPathConfig.SERVICE_LOGIN).navigation(); //获取登录名 ToastUtils.showShort( iLoginProvider.getLoginInfo().getName() );
组件之间的AOP切割
如登录跳转的地方需要判断用户登录等...
/** * <p>描述:(拦截器AOP切面拦截登录)</p> * Created by Cherish on 2018/8/20.<br> */@Interceptor(priority = 1)public class LoginInterceptor implements IInterceptor { private static final String TAG = "LoginInterceptor"; private Context mContext; @Override public void process(Postcard postcard, InterceptorCallback callback) { Log.i(TAG, "LoginInterceptor 开始执行"); if (postcard.getExtra() == 1) {//extras=1,目标页面标记,代表需要拦截处理 boolean isLogin = BaseApplication.getInstance().isLogin(); Log.i(TAG, "是否已登录: " + isLogin); //判断用户的登录情况,可以把值保存在sp中 if (isLogin) { callback.onContinue(postcard); } else { callback.onInterrupt(null); ILoginProvider iLoginProvider = (ILoginProvider) ARouter.getInstance().build(ARouterPathConfig.SERVICE_LOGIN).navigation(); iLoginProvider.start2Login(); } } else { callback.onContinue(postcard); } } @Override public void init(Context context) { mContext = context; Log.i(TAG, "LoginInterceptor 初始化"); } }
组件之间的资源冲突
资源冲突其实很好解决,团队契约资源文件的命名,如Login登录的注册注册界面命名:login_activity_register
其他类似文件均按照login_开头。可强行控制某一module的资源命名
resourcePrefix "login_"
组件统一使用的版本控制
创建单独的config.gralde来让其他模块组件引用
config.gralde:
ext { app = [ packageName: "com.chips.client", ] defaultConfig = [ compileSdkVersion: 27, buildToolsVersion: "27.0.0", minSdkVersion : 19, targetSdkVersion : 9, versionCode : 1, versionName : '1.0.0', ] dependencies = [ appcompatV7 : 'com.android.support:appcompat-v7:27.0.1', design : 'com.android.support:design:27.0.1', constraintLayout : 'com.android.support.constraint:constraint-layout:1.0.2', quickFragment : "com.allure0:QuickFragment:1.0.2",//Fragment框架 //跳转路由Router arouter_api :'com.alibaba:arouter-api:1.3.1', arouter_compiler :'com.alibaba:arouter-compiler:1.1.4', //汉字转拼音 tinyPinyin:'com.github.promeg:tinypinyin:2.0.3', //Loading content error empty等状态页 loadsir: 'com.kingja.loadsir:loadsir:1.3.6', baseRecycleViewAdapter: 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30' ] }
组件模块引用:
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) api project(':common') api rootProject.ext.dependencies.appcompatV7 api 'com.android.support.constraint:constraint-layout:1.1.2' api 'com.blankj:utilcode:1.17.3' //ARouter implementation rootProject.ext.dependencies.arouter_api annotationProcessor rootProject.ext.dependencies.arouter_compiler }
组件化解放双手Gradle插件
对于以上的组件化难点我们解决之后,实际上都是通过每次手动的sync来完成,其实完全可以利用apt/Gradle插件等技术来完成组件之间的自动切换。
Groovy实现插件:
class AppConfigExt { boolean isDebug = false NamedDomainObjectContainer<AppExt> apps NamedDomainObjectContainer<LibraryExt> modules AppConfigExt(Project project){ apps = project.container(AppExt) modules = project.container(LibraryExt) } def isDebug(boolean isDebug){ this.isDebug = isDebug } def apps(Closure closure){ apps.configure(closure) } def modules(Closure closure){ modules.configure(closure) } @Override String toString() { return "isDebug: $debugEnable\n" + "apps: ${apps.isEmpty()? "is empty" : "$apps"}"+ "modules: ${modules.isEmpty()? "is empty" : "$modules"}" } }
在AppConfig中定义了启动模式与宿主壳App和组件的配置。
剩余代码不贴了....直接贴使用方式
插件使用方式
Step1:整个项目之下添加以下代码,以下地址为本地Maven地址,后续会上传到Jcenter/Maven
apply plugin: 'com.allure.appconfig'buildscript { repositories { maven {//本地Maven仓库地址 url uri('/Users/mac/Downloads/ttt') } } dependencies { //格式为-->group:module:version classpath 'com.allure.plugin:Component:1.0.0' } } hostAppConfig { isDebug true //宿主载体 apps { app { mainActivity "com.allure.modularization.SplashActivity" modules ':modules:login', ':modules:shop', ':modules:main' } }//组件 modules { login { isRunAlone true name ":modules:login" applicationId "com.allure.login" mainActivity ".LoginActivity" } shop { isRunAlone false name ":modules:shop" applicationId "com.allure.shop" mainActivity ".ShopActivity" } main { isRunAlone false name ":modules:main" applicationId "com.allure.main" mainActivity ".MainActivity" } } }
Step2:在每一个组件的build.gradle下定义插件的引用
apply plugin: 'com.allure.appmodules'
插件解释
hostAppConfig:
hostAppConfig | 解释 |
---|---|
isDebug | 是否开启debug模式,只有当isDebug为true时,modules的isRunAlone才能生效。 |
apps | 壳工程列表,可以有多个壳工程 |
modules | 各个组件Lib |
app:
app | 解释 |
---|---|
modules | 依赖的组件列表 |
applicationId | 启动的application,可默认为空,在测试微信支付分享时可以动态配置使用测试 |
mainActivity | 启动Activity |
modules:
app | 解释 |
---|---|
name | 依赖的组件列表 |
isRunAlone | 是否可以单独运行(必须在isDebug=true情况下可运行) |
applicationId | 启动的application,必须设置 |
mainActivity | 启动Activity |
自此,我们可以脱离手动的控制组件的applictaion和lib模式,让插件帮我们完成。
项目结构
image.png
作者:Ch3r1sh
链接:https://www.jianshu.com/p/23b0239c45aa
共同学习,写下你的评论
评论加载中...
作者其他优质文章