安卓组件化开发是老生常谈的问题,基础的模块化开发教程很多,本系列教程展现从零开始,到整个系统搭建的过程,设计项目组件化结构、MVP设计模式、组件间通信路由框架、WebSocket网络交互基础库的设计、推送服务基础库的设计、UI统一风格基础库的设计、数据库交互基础库的设计、以及业务相关的实际应用场景问题。
系列文章
Android组件化-基础框架搭建
Android组件化-组件间通信BRouter
Android组件化-风格统一&主题变色
Android组件化-MVP设计模式
一、模块化开发的几个问题
项目由一个空壳模块app、一个主界面app-main模块、若干app-xxx业务模块组成、一个公共库lib-common、若干基础库组成,借本结构如下
使用Android Studio作为IDE开发,项目模块基本文件结构如下:
二、项目搭建 项目搭建主要分三步,搭建基础库模块lib-xxx、全局公共库模块lib-common,搭建业务应用模块app-xxx,整合业务模块到app空壳模块。
2.1 基础库 新建一个模块作为lib,在AndroidStudio中 File->New->New Module…->Android Library,LibraryName为db,ModuleName为lib-db,如图:
lib-db模块中build.gradle修改如下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt' ), 'proguard-rules.pro' } } }dependencies { implementation fileTree (dir: 'libs' , include : ['*.jar' ]) implementation 'com.android.support:appcompat-v7:26.1.0' testImplementation 'junit:junit:4.12' }
此时lib-db 未引入任何第三方库,rootProject.ext.compileSdkVersion为项目根目录build.gradle中定义的统一配置,方便做全局修改。根目录build.gradle如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 ext { minSdkVersion = 21 targetSdkVersion = 26 compileSdkVersion = 26 buildToolsVersion = '25.0.3' versionCode = 1 versionName = "1.0" javaVersion = JavaVersion.VERSION_1_8 supportVersion = "26.1.0" butterknifeVersion = "8.8.1" gsonVersion = "2.8.5" rxJavaVersion = "2.2.2" rxAndroidVersion = "2.1.0" rxBindingVersion = "1.0.0" rxPermissionVersion = "0.10.2" retrofitVersion = "2.4.0" okHttpVersion = "3.11.0" eventBusVersion = "3.1.1" greenDaoVersion = "3.2.2" lottieVersion = "2.6.1" }buildscript { repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } }allprojects { repositories { google() jcenter() mavenCentral() maven { url 'https://jitpack.io' } } }task clean(type: Delete ) { delete rootProject.buildDir }subprojects { project .configurations .all { resolutionStrategy.eachDependency { details -> if (details.requested.group == 'com.android.support' && !details.requested.name.contains('multidex' )) { details.useVersion "26.1.0" } } } }
** lib库模块中引入第三方库需注意,假如此库中 方法/View控件 需要在应用模块中使用,则此依赖需要使用api引入,如 **
1 2 api "com.thirdpart.app:app:1.0"
类似地新建其他几个lib库模块lib-log、lib-push、lib-apptool、lib-socket。
2.2 common库 lib-common和基础库同样是lib库模块,搭建的方法相同,不同点在于lib-common模块通过 ‘compile project’ 依赖其它所有的基础库模块,并直接被其它应用模块所依赖,实现向应用模块提供服务。
新建lib-common后,build.gradle配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 apply plugin: 'com.android.library' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { debug { } release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt' ), 'proguard-rules.pro' } } }dependencies { implementation fileTree (dir: 'libs' , include : ['*.jar' ]) compile "com.android.support:design:$rootProject.supportVersion" compile "com.android.support:cardview-v7:$rootProject.supportVersion" compile "com.android.support:appcompat-v7:$rootProject.supportVersion" compile "com.android.support:recyclerview-v7:$rootProject.supportVersion" compile "com.android.support.constraint:constraint-layout:1.1.3" testImplementation 'junit:junit:4.12' compile project (':lib-ui' ) compile project (':lib-log' ) compile project (':lib-push' ) compile project (':lib-router' ) compile project (':lib-socket' ) compile project (':lib-apptool' ) api "com.google.code.gson:gson:$rootProject.gsonVersion" api "io.reactivex.rxjava2:rxjava:$rootProject.rxJavaVersion" api "io.reactivex.rxjava2:rxandroid:$rootProject.rxAndroidVersion" api "com.jakewharton.rxbinding:rxbinding:$rootProject.rxBindingVersion" api "com.jakewharton.rxbinding:rxbinding-appcompat-v7:$rootProject.rxBindingVersion" api "com.jakewharton.rxbinding:rxbinding-recyclerview-v7:$rootProject.rxBindingVersion" api "com.jakewharton.rxbinding:rxbinding-design:$rootProject.rxBindingVersion" api "com.github.tbruyelle:rxpermissions:$rootProject.rxPermissionVersion" api "com.squareup.okhttp3:okhttp:$rootProject.okHttpVersion" api "com.squareup.okhttp3:logging-interceptor:$rootProject.okHttpVersion" api "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion" api "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion" api "com.squareup.retrofit2:adapter-rxjava:$rootProject.retrofitVersion" api "org.greenrobot:eventbus:$rootProject.eventBusVersion" api 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0-alpha-14' api 'com.scwang.smartrefresh:SmartRefreshHeader:1.0.3' api("com.airbnb.android:lottie:$rootProject.lottieVersion" ) { exclude group : 'com.android.support' } }
2.3 业务模块 业务模块如app-mine(个人中心)、app-message(消息中心)等既可作为应用模块独立运行,也可作为库模块被空壳app依赖,新建业务模块 File->New Module->Phone & Tablet Module,
app-message模块中build.gradle配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 if (moduling.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt' ), 'proguard-rules.pro' } } resourcePrefix 'message_' sourceSets { main { if (moduling.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' println '[Module-Message]: Appling Application...' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { exclude 'debug/**' } println '[Module-Message]: Appling Library...' } } } }dependencies { implementation fileTree (dir: 'libs' , include : ['*.jar' ]) implementation 'com.android.support:support-v4:26.1.0' compile project (':lib-common' ) }
如上的gradle配置涉及到单组件运行调试、组件间资源文件冲突。
粘贴代码时 manifest.srcFile 容易变成 Manifest.srcFile
2.4 空壳app模块 空壳app是作为应用模块的“组合车间”,通过依赖不同的/所有的应用模块,将各个功能模块的功能聚合起来,实现app应有的功能,app模块中包含:
build.gradle
app的名称、图标
全局的Application文件
build.gradle如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 apply plugin: 'com.android.application' android { compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode rootProject.ext.versionCode versionName rootProject.ext.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" ndk { abiFilters 'armeabi' , 'armeabi-v7a' , 'arm64-v8a' } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt' ), 'proguard-rules.pro' } } resourcePrefix 'app_' }dependencies { implementation fileTree (dir: 'libs' , include : ['*.jar' ]) if (moduling.toBoolean()) { compile project (':lib-common' ) } else { compile project (':app-main' ) compile project (':app-mine' ) compile project (':app-message' ) } }
app名称、图标配置在空壳中方便全局修改;
app还包含Application文件,在此做一些初始化操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class AppApplication extends CommonApplication { @Override public void onCreate () { super .onCreate(); BLog.d("[App]: Application Starting..." ); initRouter(); if (shouldInit()) { PushClient.getInstance().init(this )..setListener(new PushHandler ()); } } }
除此之外,空壳app不包含任何逻辑、业务代码,登录注册页、欢迎页、主界面包含在app-main中,也是作为应用模块被app依赖。app模块是真正意义上的空壳。
三、单模块调试 3.1 模块动态切换 app-message模块里builld.gradle开头的moduling配置在gradle.properties中,为boolean类型,
通过配置moduling的值,实现app-message作为独立应用模块或库模块的切换。
当build.gradle的apply plugin配置为’com.android.application’时,app-message作为应用模块:
1 2 apply plugin: 'com.android.application'
此时模块图标为
当build.gradle的apply plugin配置为’com.android.library’时,app-message作为库模块:
1 2 apply plugin: 'com.android.library'
此时模块图标为
3.2 使用不同的Manifest和Application 在应用模块的build.gradle中有如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 ... android { ... sourceSets { main { if (moduling.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' println '[Module-Message]: Appling Application...' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { exclude 'debug/**' } println '[Module-Message]: Appling Library...' } } } ... }
当模块为独立应用模块时,模块使用 ‘src/main/debug/‘下的AndroidManifest.xml,当模块作为库模块被app空壳依赖时,使用’src/main’下的AndroidManifest.xml。
‘src/main/debug/AndroidManifest.xml’配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="utf-8" ?> <manifest xmlns:android ="http://schemas.android.com/apk/res/android" package ="org.blackist.modulize.message" > <application android:allowBackup ="true" android:icon ="@mipmap/ic_launcher" android:label ="@string/message_app_name" android:roundIcon ="@mipmap/ic_launcher_round" android:supportsRtl ="true" android:theme ="@style/message_AppTheme" > <activity android:name =".view.MessageActivity" > <intent-filter > <action android:name ="android.intent.action.MAIN" /> <category android:name ="android.intent.category.LAUNCHER" /> </intent-filter > </activity > </application > </manifest >
单模块调试时需要有独立的启动Activity、图标、app名称,如果需要独立的Application进行初始化操作,也在此配置。
‘src/main/AndroidManifest.xml’配置如下:
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="utf-8" ?> <manifest xmlns:android ="http://schemas.android.com/apk/res/android" package ="org.blackist.modulize.message" > <application > <activity android:name =".view.MessageActivity" /> </application > </manifest >
作为库模块被依赖时,只需要在Manifest中注册activity、service等即可。
单模块开发、调试的时候,activity的配置只注册在debug下的Manifest,请记得手动拷贝至main下Manifest中。
四、资源冲突合并问题 不同模块中资源文件难免会有相似的资源文件,比如app-message有个list_icon的drawable、app-main中也有个list_icon的drawable,非单模块调试的时候需要合并资源文件,聚合在app空壳中,此时会引起资源冲突。
一种比较好的实践就是在各模块中添加资源前缀,尽量避免冲突,这要求项目组成员遵守规范,即资源命名为message_list_icon和main_list_icon。
xml中资源可以被android studio检查,如string.xml或color.xml中的资源,但图片类的资源不会被检查,需要开发者严格遵守约定。
组件化开发还有个问题就是,在各应用模块中使用if-else判断资源id,而不是switch,
1 2 3 4 5 6 if (R.id.message_list_icon == id) { ... } else if (R.id.message_list_text == id) { ... }
五、组件间通信 因为应用模块(app-xxx)依赖库模块(lib-xxx),所以应用模块可以直接使用库模块中的View控件和方法,而应用模块之间项目隔离,无法直接使用其它模块的控件和方法,需要使用组件间通信工具来协助完成。
使用广播、EventBus会有一些相应的问题,比较好的一种实践就是阿里巴巴开源的ARouter以及一些自定义的组件间通信框架如Brouter ,组件间通信在组件化开发中经常使用。
5.1 app-main主界面的实现 主界面有三部分主页、消息中心、个人中心,主页一般在核心的业务逻辑模块中,消息中心页在app-message模块中,个人中心页在app-mine模块中。
app-main中MainActivity负责处理三个页面的展示和切换,即一个Activity包含三个Fragment,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ...private BaseFragment currentFragment;private BaseFragment mainFragment;private BaseFragment mineFragment;private BaseFragment messageFragment; ...BRouterRes roomRes = BRouter.push( getApplicationContext(), BRouterReq.build().action("room" ).path("room/home" ) ); mainFragment = (BaseFragment) roomRes.data();BRouterRes mineRes = BRouter.push( getApplicationContext(), BRouterReq.build().action("mine" ).path("/mine/info" ) ); mineFragment = (BaseFragment) mineRes.data();BRouterRes messageRes = BRouter.push( getApplicationContext(), BRouterReq.build().action("message" ).path("/message/info" ) ); messageFragment = (BaseFragment) messageRes.data(); ...
5.2 跨模块数据访问 直接返回数据 类似于获取其它模块的Fragment,
1 2 3 4 5 6 BRouterRes roomRes = BRouter.push( getApplicationContext(), BRouterReq.build().action("room" ).path("room/home" ) ); mainFragment = (BaseFragment) roomRes.data();
startActivityForResult 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 BRouterRes res = BRouter.push( getContext(), BRouterReq.build().action("repair" ).path("repair/main/clazz" ));Intent intent = new Intent (getActivity(), (Class) res.data()); intent.putExtra("COMMAD" , "func" ); getActivity().startActivityForResult(intent, REQUEST_CODE);@Override protected void onActivityResult (int requestCode, int resultCode, Intent data) { super .onActivityResult(requestCode, resultCode, data); ... }
组件化开发基本架构到此已有雏形,可以进行简单的业务功能开发。
项目Github地址:https://github.com/blackist/modulize
(完)