功能组件是为了支撑业务组件的某些功能而独立划分出来的组件,例如视频播放功能组件,地图导航功能组件,日志管理功能组件,网络请求功能组件,图片加载功能组件等。其中有些是我们对第三方功能库的二次封装简称二方库,有些是自己写的通用的功能代码,这些功能组件不包含任何业务逻辑相关功能,不同的APP可以依赖这些功能组件为业务提供支持, 功能组件实质上跟项目中引入的第三方库是一样的, 在实际开发中我们按需引入这些库, 例如:所有的业务模块都需要用到网络请求功能组件,图片加载功能组件,那么可以在common公共组件中通过api的形式依赖它们,这样所有的业务组件都能获取到这些功能组件进行使用,但有些功能组件例如:移动支付功能组件一般只是在订单相关的业务组件中才能用到,其他的业务组件不需要这个支付的功能,因此这样的功能组件最好不要在common公共组件中进行引用,而是让订单业务组件单独依赖移动支付这个功能组件即可。
网络请求功能组件,图片加载功能组件,日志管理功能组件等这些通用组件基本上每一个业务组件都会使用到其中的功能,因此笔记建议将这些通用的功能组件封装到一个lib_core组件当中,让common公共组件中通过api的形式引用lib_core组件即可。这样可以解决功能组件分散的问题。
Common组件是当前App业务公共的组件库,它通过api方式依赖通用功能组件以提供对App相关业务提供功能支持,而且应用特有的相关逻辑也需要放到里面来,例如封装BaseActivity类,BaseApplication类,对网络接口统一处理,声明APP需要的uses-permission,定义全局通用的主题(Theme)及一些公用的类。Common组件有如下特点:
业务组件就是根据业务逻辑的不同拆分出来的组件,业务组件的特点有:
**业务组件中有一个main组件比较特殊 ** Main组件除了有业务组件的普遍属性外,Main组件在集成模式下的AndroidManifest.xml是跟其他业务组件不一样的,Main组件的表单中需要声明整个Android应用的launch Activity,这就是Main组件的独特之处;所以建议SplashActivity、登陆Activity以及主界面都应属于Main组件,也就是说Android应用启动后要调用的页面应置于Main组件。
app壳工程是就是一个空壳工程,负责管理各个业务组件,和打包apk,没有具体的业务功能;但它又必须被单独划分成一个组件,而不能融合到其他组件中,是因为它有如下几点重要功能:
通过Android Studio创建Android工程,并且在gradle.properties中添加isModule开关:
isModule = false
isModule为true时各个业务组件可以单独编译成App进行运行,isModule为true时为集成开发模式各个业务组件将会打包到壳工程中中合并成一个完成的App功能。
通过Android studio的File->New->New Module->Android Library创建项目所需要的功能模块组件,前缀以"lib_"开头,例如lib_pay,lib_map等工程。建议创建一个lib_core的公共功能模块组件,将常用的与业务无关的功能封装到此模块中,例如图片加载功能,网络请求功能,日志管理功能等,这些功能可以被所有的app通用。
通过Android studio的File->New->New Module->Android Library创建项目的公共组件lib_common工程,公共组件需要依赖lib_core公共功能模块组件,公共组件里面包含的代码逻辑有如下:
通过Android studio的File->New->New Module->phone application创建业务组件,前缀以"module_"开头例如module_mine,module_main等工程。在其build.gradle中配置模块切换的开关:
if (isModule.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
...
defaultConfig {
if (isModule.toBoolean()) {
applicationId "com.huke.module_mine"
}
}
}
//依赖公共项目
dependencies {
implementation project(path: ':lib_common')
}
在模块开发模式中可能需要一个可以启动的Activity和启动图标,应用名称,布局文件等使得app能够单独运行,但是当业务组件作为集成模式时这些东西是不需要打包到壳工程中的,因此需要在业务组件的main目录创建module目录用于存放单独作为模块时的代码,清单文件,资源文件等。
module->AndroidManifest.xml
在build.gradle中配置模块编译时将module文件夹中的内容打包进app,集成模式下则不需要调试的这些代码及资源:
...
android {
...
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
java.srcDirs = ['src/main/java', 'src/main/module/java']
res.srcDirs = ['src/main/res', 'src/main/module/res']
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
业务组件作为集成模式时src/main/AndroidManifest.xml要进行修改,将启动图标应用名称都剔除掉:
在子module模块的build.gradle中设置资源名必须以指定的字符串做前缀,并且按照规则修改res文件夹下面的所有的文件的前缀(防止资源文件冲突导致出现的问题)
android {
...
resourcePrefix "mine_"
...
}
可以将工程初始创建的app当成壳工程组件,根据需要依赖子module:
dependencies {
//依赖公共项目
implementation project(path: ':lib_common')
if (!isModule.toBoolean()) {
implementation project(path: ':module_mine')
implementation project(path: ':module_main')
implementation project(path: ':module_home')
}
}
在壳工程中配置应用的启动的application,启动图标,应用名称等:
在lib_common中创建BaseApplication,在壳app工程的manifest文件中引入BaseApplication,其他所有的module都引用了lib_common所以都可以获取到BaseApplication对象。
BaseApplication:
public class BaseApplication extends MultiDexApplication {
private static Context mApplicationContext;
private static Application mApplication;
@Override
public void onCreate() {
super.onCreate();
mApplicationContext = this;
mApplication = this;
}
/**
* 获取application
*/
public static Application getApplication() {
return mApplication;
}
/**
* 获取application
*/
public static Context getAppContext() {
return mApplicationContext;
}
}
组件化开发将不同的业务逻辑进行解耦,每个业务模块拥有不同的功能,有些组件的功能需要在Application 中进行初始化,比如百度地图,百度 OCR。已知在组件化架构中壳工程中只能指定一个Application,通常是lib_common的BaseApplication,如果是所有的业务模块共有的功能,当然是可以将其放到 BaseApplication中进行初始化的,每例如bugly,统计相关功能。但是有的功能是某一个业务模块所特有的,,例如商品详情业务模块需要第三方分享, 城市定位业务模块需要百度地位等。如果这些功能都在lib_common进行依赖并且在BaseApplication中去初始化,那么在模块化模式中,不需要用到这个功能的业务也会强行初始化这个功能, 前文中一直强调组件化的目的是为了业务解耦,这显然是不合适的。因此我们需要将 BaseApplication的生命周期分发到各个模块中去,由这些业务模块自行初始化相关功能。 目前有如下几种常见的方式:
interface ApplicationDelegate {
fun attachBaseContext(application: Application?, context: Context?)
fun onCreate(application: Application?)
}
open class BaseApplication : Application() {
var delegates: List? = null
private val MODULE_META_KEY = "ApplicationDelegate"
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
base?.let {
delegates = findApplicationDelegate(it)
}
delegates?.forEach { it.attachBaseContext(this, base); }
}
override fun onCreate() {
super.onCreate()
mApplicationContext = this
mApplication = this
Log.d("Application", "BaseApplication->onCreate()")
ARouter.init(this)
delegates?.forEach { it.onCreate(this); }
}
private fun findApplicationDelegate(context: Context): List {
val delegates: MutableList = ArrayList()
try {
val pm = context.packageManager
val packageName = context.packageName
val info = pm.getApplicationInfo(packageName, GET_META_DATA)
if (info.metaData != null) {
for (key in info.metaData.keySet()) {
val value = info.metaData[key]
if (MODULE_META_KEY == value) {
val delegate: ApplicationDelegate = initApplicationDelegate(key)
delegates.add(delegate)
}
}
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
return delegates
}
private fun initApplicationDelegate(className: String): ApplicationDelegate {
var clazz: Class<*>? = null
var instance: Any? = null
try {
clazz = Class.forName(className)
} catch (e: ClassNotFoundException) {
e.printStackTrace()
}
try {
instance = clazz?.newInstance()
} catch (e: Exception) {
e.printStackTrace()
}
if (instance !is ApplicationDelegate) {
throw RuntimeException("不能获取 " + ApplicationDelegate::class.java.name + " 的实例 " + instance)
}
return instance
}
}
class MineApplicationDelegate : ApplicationDelegate {
override fun attachBaseContext(application: Application?, context: Context?) {
}
override fun onCreate(application: Application?) {
Log.d("Application", "MineApplicationDelegate->onCreate()")
}
}
class MineContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
Log.d("Application", "MineContentProvider->onCreate()")
return false
}
override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
return 0
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
return 0
}
}
...
需要注意都各个业务模块配置的ContentProvider的authorities不能设置为一样的,否则就只能在同一设备安装其中一个应用。
和ContentProvider方式大同小异,略过。
//common组件 build.gradle
api 'com.google.auto.service:auto-service:1.0-rc7'
kapt 'com.google.auto.service:auto-service:1.0-rc7'
interface IApplication {
fun attachBaseContext(base: Context)
fun onCreate()
fun onLowMemory()
fun onTerminate()
fun onTrimMemory(level: Int)
fun onConfigurationChanged(newConfig: Configuration)
}
@AutoService(IApplication::class)
class MineApplicationImpl : IApplication {
override fun attachBaseContext(base: Context) {
app = base as Application
}
override fun onCreate() {
Log.d("Application", "ApplicationImpl->onCreate()")
}
override fun onLowMemory() {
}
override fun onTerminate() {
}
override fun onTrimMemory(level: Int) {
}
override fun onConfigurationChanged(newConfig: Configuration) {
}
companion object {
lateinit var app: Application
}
}
class BaseApplication : Application() {
private var mApplicationList: List = ServiceLoader.load(IApplication::class.java, javaClass.classLoader).toList()
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
mApplicationList.forEach {
it.attachBaseContext(this)
}
}
override fun onCreate() {
super.onCreate()
mApplicationList.forEach {
it.onCreate()
}
}
override fun onLowMemory() {
super.onLowMemory()
mApplicationList.forEach {
it.onLowMemory()
}
}
override fun onTerminate() {
super.onTerminate()
mApplicationList.forEach {
it.onTerminate()
}
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
mApplicationList.forEach {
it.onTrimMemory(level)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
mApplicationList.forEach {
it.onConfigurationChanged(newConfig)
}
}
}
前面说到,组件化的核心就是解耦,所以组件和组件之间是不能有依赖的,但业务模块不同,业务模块之间却存在互相通信的情况,核心情况有三种:
前面说到,组件化的核心就是解耦,所以组件和组件之间是不能有依赖的,例如在首页模块点击购物车按钮需要跳转到购物车模块的购物车页面,两个模块之间没有依赖,所以不能在使用Activity的显示跳转来跳转页面了 ,虽然隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。可以借助路由框架来实现界面跳转,比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。
ARouterARouter 是一个用于帮助 Android App 进行组件化改造的框架,支持模块间的路由、通信、解耦。
android.enableJetifier=true 解决arouter适配Androidx的问题
....
dependencies {
api 'com.alibaba:arouter-api:1.5.2'
}
apply plugin: 'kotlin-kapt'
...
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
...
dependencies {
...
kapt 'com.alibaba:arouter-compiler:1.5.2'
}
有些人引入路由框架后会把所有的 startActivity(intent) 都改成使用路由跳转。 笔者建议是, 路由框架只在业务组件间必要的交互上使用,其它非必要的情况能不用就不用 。因为阅读路由跳转的代码还需要知道 path 对应哪个 Activity,如果是 startActivity(intent) 就能直接看到跳转的 Activity,可读性更好。但是有路由跳转拦截需求的话还是得用路由跳转,如果能直接访问到所需的类且没有路由拦截的需求,用了路由和直接实现的效果是一样的,而使用路由会增加代码的复杂度,降低代码的阅读性,并没有必要使用路由。。
上文说到组件之间是没有依赖关系的,因此无法组件之间无法互相调用类的方法,但是很多时候又需要进行调用, 例如,module_home中有一个提现的按钮,点击的时候要获取到用户是否已经实名认证。而实名认证功能是module_mine中的能力。第一种方式是我们将实名认证功能下沉到lib_common中去那这样module_home和module_mine都可以获取到这个能力,但是别的业务模块也依赖了lib_common也会获取到这个能力,所以这种方式不好。 另外一种方式是使用接口进行通信, 接口通信概念并不是什么新颖的概念,我们可以理解为将一个模块当作SDK,提供接口+数据结构给其他需要的模块调用,从而实现通信。
interface IAuthService {
fun isAuth(): Boolean
fun getAuthTime(): String
}
class EmptyAuthService : IAuthService {
override fun isAuth(): Boolean {
return false
}
override fun getAuthTime(): String {
return ""
}
}
object ServiceFactory {
private val mServices = CopyOnWriteArrayList()
fun register(service: Any) {
if (!mServices.contains(service)) {
mServices.add(service)
}
}
private fun getService(cls: Class): Any? {
mServices.forEach {
if (cls.isInstance(it)) {
return it
}
}
return null
}
fun getAuthService(): IAuthService {
val service = getService(IAuthService::class.java)
return service as? IAuthService ?: EmptyAuthService()
}
}
class AuthService : IAuthService {
override fun isAuth(): Boolean {
return true
}
override fun getAuthTime(): String {
return "2020.9.1"
}
}
class MineApplicationDelegate : ApplicationDelegate {
override fun attachBaseContext(application: Application?, context: Context?) {
}
override fun onCreate(application: Application?) {
Log.d("Application", "MineApplicationDelegate->onCreate()")
ServiceFactory.register(AuthService())
}
}
class HomeFragment : BaseFragment() {
override fun getLayoutId(): Int = R.layout.home_fragment_page
override fun initPage(savedInstanceState: Bundle?) {
findViewById(R.id.btn_collect).setOnClickListener {
//到我的收藏页面需要登录
val authService = ServiceFactory.getAuthService()
if (loginService.isAuth()) {
Toast.makeText(
context,
"Share from ${authService.getAuthTime()}", Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
"Not Auth", Toast.LENGTH_SHORT
).show()
}
}
}
}
对于这种接口化的通信实现,ARouter的IProvider也已经实现,完全可以满足我们的要求。
interface IAuthService : IProvider {
fun isAuth(): Boolean
fun getUserId(): String
}
@Route(path = "/mine/AuthServiceImpl")
class AuthService : IAuthService {
override fun isAuth(): Boolean {
return true
}
override fun getAuthTime(): String {
return "2020.9.1"
}
}
class HomeFragment : BaseFragment() {
override fun getLayoutId(): Int = R.layout.home_fragment_page
override fun initPage(savedInstanceState: Bundle?) {
findViewById(R.id.btn_collect).setOnClickListener {
//到我的收藏页面需要登录
val authService = ARouter.getInstance().build("/mine/AuthServiceImpl").navigation() as? IAuthService
if (loginService.isAuth()) {
Toast.makeText(
context,
"Share from ${authService.getAuthTime()}", Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
"Not Auth", Toast.LENGTH_SHORT
).show()
}
}
}
}
组件之间的通信,例如A业务组件中有消息列表,而用户在B组件中操作某个事件后会产生一条新消息,需要通知A组件刷新消息列表,这样业务场景需求可以使用Android广播来解决,也可以使用第三方的事件总线来实现,比如EventBus。
而EventBus目前最被诟病的一点就是无法追溯事件,所以为了能更好的控制EvenBus,我们可以自建一个事件索引。
public class Event {
private Object index;
private T data;
public Event() {
}
public Event(Object index, T data) {
this.index = index;
this.data = data;
}
public Object getIndex() {
return index;
}
public void setIndex(Object index) {
this.index = index;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
Event
时,必须为事件添加index
索引标识。class EventIndex {
companion object {
// 事件从修改昵称页面发送至用户信息页面
const val EVENT_FROM_MODIFYNAME_TO_USERINFO = "From_ModifyName_To_UserInfo"
// 事件从修改邮箱页面发送至用户信息页面
const val EVENT_FROM_MODIFYEMAIL_TO_USERINFO = "From_ModifyEmail_To_UserInfo"
}
}
还有值得注意的一点,在公共模块中,仅存放模块之间的通信索引标识;模块内使用EventBus通信,建立的索引放在对应模块内。
优点:
缺点:
在 单工程Android组件化方案 中,由于所有组件都在同一个项目中,并且使用 implementation project(‘:组件名’) 方式依赖其他组件,这样就会导致很多问题。
作为android开发人员我们知道在使用第三库的时候,这些开源库一般都是上传到maven或jcenter仓库上供我们工程引用。那么我们自己开发的业务组件能不能也传到maven或jcenter中央仓库让后再让壳工程像是以第三方库一样引用这些业务组件呢?答案是肯定的,不过我们的业务代码并不是真正上传到开源仓库上去,而是可以通过nexus公司内部搭建一个私有的maven仓库,将我们开发好的组件上传到这个私有的maven仓库上,然后内部开发人员就可以像引用三方库那样轻而易举的将组件引入到项目中了。这样每个业务组件都是一个工程,在这个工程里可以创建app 作为壳组件,它依赖 业务组件 运行调试功能,因此不需要 isModule 来控制独立调试,等业务组件调试完毕以后就可以将其上传到搭建好的远程仓库中供项目壳工程使用。例如,购物车组件 就是 新建的工程Cart 的 module_cart模块,业务代码就写在module_cart中即可。app模块是依赖module_cart。app模块只是一个组件的入口,或者是一些demo测试代码。 组件存在于独立工程 那么当所有业务组件都拆分成独立组件时,原本的工程就变成一个只有app模块的壳工程了,壳工程就是用来集成所有业务组件的。
优点
缺点
比如有首页,订单,视频,个人中心,设备等不同业务组件,一般利用到一些公共布局或资源,会往公共common组件下沉。但是也需要避免过渡下沉造成公共common库臃肿! 比如一些很基础的组件,就可以下沉到一个单独的项目中,作为二方库使用,通常而言,二方库是对三方库的再一次封装,当然也有完全自己实现功能的。比如日志库、图片库、网络库等十分基础的常用的组件和功能,就可以下沉为二方库。就公司层面而言,组件下沉有几方面的好处:
对于组件,就公司层面而言,一般我们会把下沉的组件放到服务器上,方便公司的其他项目也一起使用。这就涉及到了组件项目的编译和上传过程。Google 提供的 library 插件可以把项目打包成一个 aar 包 。我们只需要关注如何将编译好的 aar 包上传到服务器即可。