Android组件化思路引文
前言
本文并不是具体的实现文,主要讨论组件化的思路,分模块 和 模块通信。并实践一个简单的路由组件,完成模块间页面跳转和通信这两个功能,意图在于帮助刚接触组件化的同学提供一个简单不复杂的思路,对组件化有感性认识快速入门
具体的实践文,可看这篇:Android 组件化最佳实践 - 掘金 (juejin.cn)
什么是组件化
组件化本质上是一种组织代码的方式,只不过它的粒度更大,以module为单位。在未使用组件化之前,所有的代码都放在app模块中,在app模块内部通过分包划分业务代码和功能代码
如下图所示,
根据业务划分三个包:
- find 发现
- home 首页
- shop 商城
根据功能划分两个包:
- http 网络请求
- utils 工具类
上述是未使用组件化情况,所有代码都在一个模块中编写,这样做并没有什么问题,但是当项目代码越来越多的 或者 有多人参数到项目中就有很大的问题了,比如:
- 代码都在写在一个模块中,不论怎么细致的分包,都免不了一个包下出现10多个类甚至更多的情况
- 分包的形式几乎对代码没有约束
- 开发人员多了,代码都写在一个模块中,每一位开发都拥有对文件读写的权利,容易出现代码覆盖冲突问题
总之组件化是为了应对代码多,人多 或者代码和人都多的情况而使用的一种组织代码的方式,一个模块中的代码分散到多个模块中。由于代码不在一个模块中,会出现到A模块无法引用到B模块中的类,引出通信问题。
所以组件化面对的主要问题主要有两个:
- 分模块
- 模块间通信
分模块
模块依据什么划分呢? 四个大字:单一职责 。老实说,写代码的时候 能够时刻牢记 单一职责,就能写出很不错的代码了。
拆分巨型单模块 与 拆分巨型单一类 的思想都是一致的。 其实它们出现的原因也一致,把不同职责的代码都放到一个类/模块中。所以拆分代码可以理解为代码归类
代码大致可以分为业务代码 和 功能代码,比如:
- 首页属于业务,网络请求属于功能
- 商城属于业务,数据库属于功能
所以当你的项目计划进行模块化的时候,只需要根据项目实际情况划分即可,没有什么硬性规定。
拆分代码有两个好处:
- 高复用性
- 体现功能模块上,比如:网络请求,轮播图,播放器,支付,分享等功能,任何一个业务都能可能会使用。实现为一个单独的模块,哪里使用哪里引入。
- 代码隔离
- 体现在业务模块,比如:A模块实现商城,B模块实现文章论坛,两者绝大部分代码没有任何关联,独立存在。
- 假设有一天项目不做文章论坛了,业务直接砍掉。那么删除B模块即可,A模块不受任何影响
- 但A,B模块都有可能有到分享功能,所以分享作为功能模块出现,不包含任何业务,只提供分享功能。
经过划分模块的代码结构如图
三个业务模块:
- module_find 发现
- module_home 首页
- module_shop 商城
两个功能模块:
- library_network 网络请求
- library_utils 工具类
总之代码模块的拆分是单一职责的体现,大概可以分为业务模块和功能模块两种,功能模块的粒度更小可复用性更高,比如:轮播图,播放器任何位置都可能使用。
业务模块的粒度更大,可以引用多个功能模块解决问题,大多数代码都是依据业务逻辑编写,与功能模块相比 除非是同一个公司有相同业务否则复用性没那么高,
通信分析
上面主要讲了拆分模块的思路,现在聊聊模块间通信。特意设计模块间通信方案,主要是用于业务模块间通信。
业务模块和功能模块之间是单向通信,业务模块直接引用功能模块,调用功能模块暴露的方法即可。
但业务模块不同,业务模块之间存在互相通信的情况,核心情况有两种:
- 页面跳转
- A模块跳转B模块的页面,B模块跳转A模块的页面
- 数据通信
- A模块获取B模块的数据,比如调用B模块的网络请求。
- 可能会有点疑问,直接在A模块写要调用的接口不就好了,为什么要费劲巴拉的进行模块间通信,可以是可以。组件化就是为了隔离,解耦,复用。如果A模块直接实现了要用的网络请求,还要组件化干嘛呢,出现类似情况都这么干,项目内就会出现很多重复代码,除了图方便 没有别好处
单一模块开发时所有的类都能直接访问,上述的问题简直不是问题,从MainActivity 跳转到 TestActivity ,可以直接获取TestActivity的class对象完成跳转
val intent = Intent(this@MainActivity,TestActivity::class.java)
startActivity(intent)
但是分开多模块就是问题了,MainActivity 和 TestActivity 分别在A,B两个模块中,两个业务模块之间没有直接引用代码隔离,所以不能直接调用到想使用的类。
这种情况就需要一个中间人,帮助A,B模块通信。
(需求简单,实现简单)中间人好像邮局,两人住在同一个村甚至对门,想要唠嗑,送点东西,因为距离近走着就去了。如果两人相隔千里不能见面,想要唠嗑需要写信,标记地址交给邮局,让邮局转发。
(需求复杂,实现复杂)信件好保存一般不会损坏,运送比较方便。如果想要快点到,加钱用更快的运送工具。 如果想要送一块家乡的红烧肉,为了保鲜原汁原味,可能要加更多的钱用飞机+各种保险措施送过去
模块间通信也是类似,A,B模块通过中间人,也就是路由组件通信。页面跳转是最简单的通信需求实现简单,如果想要访问数据,获取对象应用等更复杂的需求,可能需要更加复杂的设计和其他技术手段才实现目标。
但总之A,B模块代码隔离之后不会无缘无故就实现了通信,一定会存在路由角色帮助A,B模块通信。区别在于路由是否强大,支持多少功能。
粗糙的路由实现
页面跳转
实现路由组件最基本的功能页面跳转,讨论具体技术方案之前,先理清思路。
Android原生跳转页面只有一种办法 startActivity(intent(context,class))
,调用startActivity方法有三要素
- context 提供的 startActivity方法
- 构造intent 需要 context
- 构造intent 需要 目标类的class对象
世面上所有的路由组件封装跳转页面功能,就算他封装出花来,也是基于AndroidSDK,无法脱离原生提供的方法。
所以我们现在需要想办法调用完整的startActivity(intent(context,class))
关键点在于,由于代码隔离,我们无法直接获取目标activity的class,直白点说无法 直接**“.”**出class。那么怎么可以在代码隔离的情况下拿到目标类的class呢
有个小技巧,先要说明一个事,模块A,模块B仅仅在编码的时候处于代码隔离的状态,但是打包之后它们还是一个应用,代码在一个虚拟机中。所以可以使用 Class.forName(包名+类名)
运行时获取class对象,完成跳转
val clazz = Class.forName("com.xxx.TestActivity")
val intent = Intent(this,clazz);
startActivity(intent)
这种方式可以帮助我们实现页面跳转的逻辑,但是非常粗糙,总不能需要模块间页面跳转,就硬编码包名+类名 获取class,太麻烦了,太容易出错了,代码散落在程序各处。
但是这种粗糙的方式也为我们提供了一点思想火花
如果我们能通过一种方式收集到 有模块间跳转需求的页面class对象 或者 包名+类名,在需要跳转的时候取出不就可以了么。
大概步骤:
- 创建路由组件
- 模块向路由注册页面信息
- 从路由取出页面信息实现跳转
创建路由组件,只有一个Route类
object Route {
private val routeMap = ArrayMap<String, Class<*>>()
fun register(path: String, clazz: Class<*>) {
routeMap[path] = clazz
}
fun navigation(context: Context, path: String) {
val clazz = routeMap[path]
val intent = Intent(context, clazz)
context.startActivity(intent)
}
}
其他组件在初始化时注册路由
Route.register("home/HomeActivity", HomeActivity::class.java)
模块间跳转页面
class TestActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
val button: Button = findViewById(R.id.button)
button.setOnClickListener {
Route.navigation(this, "home/HomeActivity")
}
}
}
把握住核心思想快速实现简单的模块间页面跳转还是非常简单的,在来回顾一下
- 代码隔离后 实现页面跳转最关键的问题是,无法直接获取目标类的Class引用
- 项目只是在编码期隔离,打包之后仍然是在一个虚拟机内,可以通过
Class.forName(包名+类名)
获取引用 - key-value的形式存储 需要模块间跳转类的Class信息,在需要的时候取出
看没什么用的效果图
上述代码肯定是可用的,但是实际运行并不是仅仅引入一个路由组件就可以了,还有很多项目配置细节,可以参考 开头推荐的文章
模块间通信
接口下沉方案,在Route组件中定义通讯接口,使所有模块都可以引用,具体实现只在某个业务模块中,在初始化时注册实现类,运行时通过反射动态创建实现类对象。
添加模块通信后,Route组件有两种逻辑要处理,页面跳转和模块通信。 保存的Class可能是Activity 或 某个接口实现类,业务操作也不同。
为了区分两种不同的业务,对Route组件进行一点小改造,新增 RouteEntity 保存数据,RouteType 路由类型用于区分,如下:
object Route {
private val routeMap = ArrayMap<String, RouteEntity>()
/**
* 注册信息
*/
fun register(route: RouteEntity) {
routeMap[route.path] = route
}
/**
* 页面导航
*/
fun navigation(context: Context, path: String) {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
val intent = Intent(context, routeEntity.clazz)
context.startActivity(intent)
}
/**
* 获取通信实例
*/
fun getService(path: String): Any {
val routeEntity = routeMap[path] ?: throw RuntimeException("path错误 找不到类")
return routeEntity.clazz.newInstance()
}
}
/**
* 保存路由信息
* @param path 路径 用于查找class
* @param type 类型 区分 页面跳转 和 通信
* @param clazz 类信息
*/
data class RouteEntity(val path: String,@RouteType val type:Int,val clazz: Class<*>)
/**
* 路由类型
*/
@IntDef(RouteType.ACTIVITY, RouteType.SERVICE)
annotation class RouteType() {
companion object {
const val ACTIVITY = 0
const val SERVICE = 1
}
}
使用如下:
//在 Route组件中 定义接口
interface IShopService {
fun getPrice(): Int
}
//业务模块中实现接口
class ShopServiceImpl :IShopService {
override fun getPrice(): Int {
return 12
}
}
//模块初始化时注册
override fun create(context: Context) {
Route.register(RouteEntity("shop/ShopActivity",RouteType.ACTIVITY,ShopActivity::class.java))
Route.register(RouteEntity("shop/ShopService",RouteType.SERVICE,ShopServiceImpl::class.java))
}
//其他模块中使用
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity_home)
val btnGoShop = findViewById<Button>(R.id.btn_go_shop)
val btnGetPrice = findViewById<Button>(R.id.btn_get_price)
btnGoShop.setOnClickListener {
//跳转页面
Route.navigation(this, "shop/ShopActivity")
}
btnGetPrice.setOnClickListener {
//模块通信
val shopService: IShopService = Route.getService("shop/ShopService") as IShopService
Toast.makeText(this, "价格:${shopService.getPrice()}", Toast.LENGTH_SHORT).show()
}
}
}
几点路由优化思路
- 路由信息 每次都要手动注册 很麻烦
- 利用编译时注解结合APT技术优化
- 自定义注解,跳转的页面和通信类添加注解
- 定义注解处理器,在编译时读取注解
- 根据注解携带的信息 处理业务逻辑 生成java类 完成组件注册功能
- 路由组件 所有路由信息在初始化的时候一次性加载到内存中,需要优化
- 分组保存,懒加载信息
- 根据路径 把路由信息分组保存,
- RootManager 保存 内部持有map 保存所有group 信息
- Group 内部持有 List 保存所有 节点信息
- 当用到某一group时,
- 通过反射实例化Group 加载当前Group下的节点信息到内存中
- 每次获取对象时,都是通过反射创建新对象,消耗内存
- 新增缓存机制,只在第一次创建新对象
- 可以使用
LruCache
缓存
上述路由组件的例子是非常简单的,难点在于从零开始,没有任何借鉴的情况下搞出这个”简单的”路由组件,反正我是没有这个创造能力 哈哈。
如果想要搞一个成熟完美的路由组件还是非常难的,但是最初肯定都是从基础功能开始一点一点迭代。除非是大佬,不然不推荐自定义路由组件
作者:图个喜庆
链接:https://juejin.cn/post/7105576036720443405
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。