本项目中所使用的方案:
- 面向接口编程 + Java的SPI机制(ServiceLoader)+AutoService:
先来认识下 Java 的 SPI 机制:面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。JavaSPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移到了程序之外。这段话也是我复制的别人的,听起来很懵逼,大致意思就是我们可以通过 SPI 机制将实现类暴露出去。关于如何使用 SPI,这里不在陈述,总之是我们在各组件内通过 SPI 去将实现类暴露出去,在 Application 中我们通过 Java 提供的 SPI API 去获取这些暴露的服务,这样我们就拿到了这些类的实例,剩下的步骤就和上面的方案一样了,通过一个集合遍历实现类调用其相应的方法完成初始化的工作。由于使用 SPI 需要在每个模块创建对应的文件配置,这比较麻烦,所以我们使用 Google 的 AutoService 库来帮助我们自动创建这些配置文件,使用方式也非常的简单,就是在实现类添加一个 AutoService 注解。本框架中的核心类是这几个:lib_base-LoadModuleProxy、lib_base-ApplicationLifecycle。这种方案是我请教的一个米哈游的大佬,这位大佬告诉我在组件化中组件的初始化可以使用 ServiceLoader 来做,于是我就去研究了下,最后发现这种方案还不错,比前面提到的两种方案都要简单、安全。
资源命名冲突
在组件化方案中,资源命名冲突是一个比较严重的问题,由于在打包时会进行资源的合并,如果两个模块中有两个相同名字的文件,那么最后只会保留一份,如果不知道这个问题的小伙伴,在遇到这个问题时肯定是一脸懵逼的状态。问题既然已经出现,那我们就要去解决,解决办法就是每个组件都用固定的命名前缀,这样就不会出现两个相同的文件的现象了,我们可以在 build.gradle 配置文件中去配置前缀限定,如果不按该前缀进行命名,AS 就会进行警告提示,配置如下:
android {
resourcePrefix “前缀_”
}
组件划分
其实组件的划分一直是一个比较难的部分,这里其实也给不到一些非常适合的建议,看是看具体项目而定。
关于基础组件通常要以独立可直接复用的角度出现,比如网络模块、二维码识别模块等。
关于业务组件,业务组件一般可以进行单独调试,也就是可以作为 app 运行,这样才能发挥组件化的一大用处,当项目越来越大,业务组件越来越多时,编译耗时将会是一个非常棘手的问题,但是如果每个业务模块都可以进行的单独调试,那就大大减少了编译时间,同时,开发人员也不需要关注其他组件。
关于公共模块,lib_base 放一些基础性代码,属于框架基础层,不应该和项目业务有牵扯,而和项目业务相关的公共部分则应该放在 lib_common 中,不要污染 lib_base。
依赖版本控制
组件化常见的一个问题就是依赖版本,每个组件都有可能自己的依赖库,那我们应该统一管理各种依赖库及其版本,使项目所有使用的依赖都是同一个版本,而不是不同版本。本项目中使用 buildSrc 中的几个kt文件进行依赖版本统一性的管理,及其项目的一些配置。
MVVM相关
- MVVM 采用 Jetpack 组件 + Repository 设计模式 实现,所使用的 Jetpack 并不是很多,像 DataBinding、Hilt、Room 等并没有使用,如果需要可以添加。采用架构模式目的就是为了解偶代码,对代码进行分层,各模块各司其职,所以既然使用了架构模式那就要遵守好规范。
- Repository 仓库层负责数据的提供,ViewModel 无需关心数据的来源,Repository 内避免使用 LiveData,框架里使用了 Kotlin 协程的 Flow 进行处理请求或访问数据库,Repository 的函数会返回一个 Flow 给 ViewModel 的调用函数,Flow 上游负责提供数据,下游也就是 ViewModel 获取到数据使用 LiveData 进行存储,View 层订阅 LiveData,实现数据驱动视图
- 三者的依赖都是单向依赖,View -> ViewModel -> Repository
项目使用的三方库及其简单示例和资料
- Kotlin
- Kotlin-Coroutines-Flow
- Lifecycle
- ViewModel
- LiveData
- ViewBinding
- Hilt
- Android KTX
- OkHttp:网络请求
- Retrofit:网络请求
- MMKV:腾讯基于 mmap 内存映射的 key-value 本地存储组件
- Glide:快速高效的 Android 图片加载库
- ARoute:阿里用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
- BaseRecyclerViewAdapterHelper:一个强大并且灵活的 RecyclerViewAdapter
- StatusBarUtil:状态栏
- EventBus:适用于 Android 和 Java 的发布/订阅事件总线
- Bugly:腾讯异常上报及热更新(只集成了异常上报)
- PermissionX:郭霖权限请求框架
- LeakCanary:Android 的内存泄漏检测库
- AndroidAutoSize:JessYan 大佬的 今日头条屏幕适配方案终极版
Kotlin协程
关于 Kotlin 协程,是真的香,具体教程可以看我的一篇文章:
Flow 类似于 RxJava,它也有一系列的操作符,资料:
PermissionX
PermissionX 是郭霖的一个权限申请框架 使用方式:
PermissionX.init(this)
.permissions(“需要申请的权限”)
.request { allGranted, grantedList, deniedList -> }
资料:
GitHub: github.com/guolindev/P…
EventBus APT
事件总线这里选择的还是 EventBus,也有很多比较新的事件总线框架,还是选择了这个直接上手的 在框架内我对 EventBus 进行了基类封装,自动注册和解除注册,在需要注册的类上添加 @EventBusRegister 注解即可,无需关心内存泄漏及没及时解除注册的情况,基类里已经做了处理
@EventBusRegister
class MainActivity :
《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享
AppCompatActivity() {}
很多使用 EventBus 的开发者其实都没有发现 APT 的功能,这是 EventBus3.0 的重大更新,使用 EventBus APT 可以在编译期生成订阅类,这样就可以避免使用低效率的反射,很多人不知道这个更新,用着3.0的版本,实际上却是2.0的效率。 项目中已经在各模块中开启了 EventBus APT,EventBus 会在编译器对各模块生成订阅类,需要我们手动编写代码去注册这些订阅类:
// 在APP壳的AppApplication类中
EventBus
.builder()
.addIndex(“各模块生成的订阅类的实例 类名在base_module.gradle脚本中进行了设置 比如 module_home 生成的订阅类就是 module_homeIndex”)
.installDefaultEventBus()
屏幕适配 AndroidAutoSize
屏幕适配使用的是 JessYan 大佬的 今日头条屏幕适配方案终极版
GitHub: github.com/JessYanCodi…
使用方式:
// 在清单文件中声明
// 主单位使用dp 没设置副单位
// 默认是以竖屏的宽度为基准进行适配
// 如果是横屏项目要适配Pad(Pad适配尽量使用两套布局 因为手机和Pad屏幕宽比差距很大 无法完美适配)
// 以高度为基准进行适配 (还需要手动代码设置以高度为基准进行适配) 目前以高度适配比宽度为基准适配 效果要好
// 在Application 中设置
// 屏幕适配 AndroidAutoSize 以横屏高度为基准进行适配
AutoSizeConfig.getInstance().isBaseOnWidth = false
ARoute
ARoute 是阿里巴巴的一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦
使用方式:
// 1.在需要进行路由跳转的Activity或Fragment上添加 @Route 注解
@Route(path = “/test/activity”)
public class YourActivity extend Activity {
…
}
// 2.发起路由跳转
ARouter.getInstance()
.build(“目标路由地址”)
.navigation()
// 3.携带参数跳转
ARouter.getInstance()
.build(“目标路由地址”)
.withLong(“key1”, 666L)
.withString(“key3”, “888”)
.withObject(“key4”, new Test(“Jack”, “Rose”))
.navigation()
// 4.接收参数
@Route(path = RouteUrl.MainActivity2)
class MainActivity : AppCompatActivity() {
// 通过name来映射URL中的不同参数
@Autowired(name = “key”)
lateinit var name: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mBinding.root)
// ARouter 依赖注入 ARouter会自动对字段进行赋值,无需主动获取
ARouter.getInstance().inject(this)
}
}
// 5.获取Fragment
Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
资料:
ViewBinding
通过视图绑定功能,可以更轻松地编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个 XML 布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有 ID 的所有视图的直接引用。 在大多数情况下,视图绑定会替代 findViewById
使用方式:
按模块启用ViewBinding
// 模块下的build.gradle文件
android {
// 开启ViewBinding
// 高版本AS
buildFeatures {
viewBinding = true
}
// 低版本AS 最低3.6
viewBinding {
enabled = true
}
}
Activity 中 ViewBinding 的使用
// 之前设置视图的方法
setContentView(R.layout.activity_main)
// 使用ViewBinding后的方法
val mBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// ActivityMainBinding类是根据布局自动生成的 如果没有请先build一下项目
// ViewBinding会将控件id转换为小驼峰命名法,所以为了保持一致规范,在xml里声明id时也请使用小驼峰命名法
// 比如你有一个id为mText的控件,可以这样使用
mBinding.mText.text = “ViewBinding”
Fragment 中 ViewBinding 的使用
// 原来的写法
return inflater.inflate(R.layout.fragment_blank, container, false)
// 使用ViewBinding的写法
mBinding = FragmentBlankBinding.inflate(inflater)
return mBinding.root
资料:
官方文档: developer.android.com/topic/libra…
CSDN: blog.csdn.net/u010976213/…
ViewModel
ViewModel 类旨在以注重生命周期的方式存储和管理界面相关的数据。ViewModel 类让数据可在发生屏幕旋转等配置更改后继续留存。
使用方式:
class MainViewModel : ViewModel(){}
class MainActivity : AppCompatActivity() {
// 获取无参构造的ViewModel实例
val mViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
}