先上代码MVVM
aar/source
一般来说组件化项目中都会做aar和源码切换,开发同学正在进行的业务module需依赖源码,其它的不相干的模块依赖远程aar。大概会先定义一个全局变量做aar/source切换的开关,然后在app中进行依赖。
module.gradle
ext {
// source/aar
isBusinessDev = false
biz = [
business: "com.xxx.xxx:xxx:1.0.0",
]
}
app下build.gradle
dependencies {
...
if (rootProject.ext.isBusinessDev) {
implementation project(path: ':business')
} else {
implementation rootProject.ext.biz.business
}
}
没啥毛病,问题是随着业务迭代module逐渐变多,需要不停的往app中添加这样if else
的依赖控制代码,倒也不是if else不好,很多的if else就有点难受受了。思考一下项目中第三方依赖是怎么偷懒的。
config.gradle
中定义好依赖库版本号、依赖路径
versions = [
kotlin : '1.5.20',
coroutine : '1.5.2',
androidx_core : '1.3.2',
appcompat : '1.2.0',
lifecycle : '2.3.1',
work_manager : '2.5.0',
room : '2.2.5',
constraintlayout : '2.0.4',
recyclerview : '1.1.0',
material : '1.3.0',
]
common = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin",
coroutine : "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutine",
androidx_core : "androidx.core:core-ktx:$versions.androidx_core",
appcompat : "androidx.appcompat:appcompat:$versions.appcompat",
viewmodel : "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle",
livedata : "androidx.lifecycle:lifecycle-livedata-ktx:$versions.lifecycle",
lifecycle : "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle",
constraintlayout : "androidx.constraintlayout:constraintlayout:$versions.constraintlayout",
recyclerview : "androidx.recyclerview:recyclerview:$versions.recyclerview",
material : "com.google.android.material:material:$versions.material",
]
然后在管理依赖的module中一个循环搞定
dependencies {
rootProject.ext.common.each { k, v -> api v }
}
arr/source切换也搞个循环好了
ext {
// source/aar
biz = [
business: [false, "com.xxx.xxx:xxx:1.0.0"],
]
// module
modules = [
business: project(':business'),
]
}
app中修改依赖方式
dependencies {
...
biz.each { entry ->
if (entry.value[0]) {
implementation entry.value[1]
} else {
implementation modules.(entry.key)
}
}
}
application/library
为了方便调试,很多时候我们希望业务module能单独run起来,让业务module独立运行。可以给业务module加一个开关,然后让业务module用这个开关控制application/library的切换。当然此种情况下,我们希望app也能独立运行,如此一来,业务module作为application独立运行时,app需剔除该业务module的依赖。
修改module.gradle
ext {
// source/aar
biz = [
business: [false, "com.xxx.xxx:xxx:1.0.0"],
]
// library/application
isBusinessModule = true
// module
modules = [
business: [isBusinessModule, project(':business')],
]
}
app下build.gradle再加个判断,module作为applicaiton时不进行依赖。
dependencies {
biz.each { entry ->
if (entry.value[0]) {
implementation entry.value[1]
} else {
if (modules.(entry.key)[0]) {
implementation modules.(entry.key)[1]
}
}
}
}
业务moduel下的build.gradle用module.gradle中定义好的变量isBusinessModule
控制application/library切换。
def isModule = rootProject.ext.isBusinessModule
if (isModule) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
别忘了application需配置applicationId,manifest需指定启动项和Application。
android {
resourcePrefix "business_"
defaultConfig {
...
if (!isModule) {
applicationId "com.xxx.xxx.xxx"
}
}
sourceSets {
main {
if (isModule) {
manifest.srcFile 'src/main/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
}
}
}
}
反射初始化子类对象
之前看有的同学搞了些骚操作,在基类初始化ViewBinding
。想来也是在父类中拿到泛型类型,然后反射初始化。ViewBinding生成的类格式是固定的,直接匹配类名,反射实例化然后调用ViewBinding.inflate()
方法返回ViewBinding
实例。
abstract class BaseSimpleActivity<VB : ViewBinding> : BaseActivity() {
protected val binding by lazy {
createViewBinding()
}
open fun createViewBinding() = reflectViewBinding() as VB
@Suppress("UNCHECKED_CAST")
private fun reflectViewBinding(): VB? {
val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
types.forEach {
if (it.toString().endsWith("Binding") && it is Class<*>) {
val method = it.getDeclaredMethod("inflate", LayoutInflater::class.java)
return method.invoke(it, layoutInflater) as VB
}
}
return null
}
}
当然为了防止意外状况或者是性能问题,createViewBinding()
方法默认实现为反射,加个open
修饰让子类可以重写自己提供ViewBinding
对象。
嗯,这样很香啊~子类拿着binding
直接用就好了。等等ViewModel
是不是也可以这么搞呢,当然可以,搞一搞。
abstract class BaseVMActivity<VM : BaseViewModel<R>, R : BaseRepository, VB : ViewBinding> :
BaseSimpleActivity<VB>() {
protected val viewModel: VM by lazy {
createViewModel()
}
open fun createViewModel() = reflectViewModel()
@Suppress("UNCHECKED_CAST")
private fun reflectViewModel(): VM {
val types = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments
return ViewModelProvider(this)[types[0] as Class<ViewModel>] as VM
}
}
ViewModel的实现类命名可能并非以ViewModel结尾,这里直接取第一个泛型类型types[0]
。同样的createViewModel()
默认实现为反射,加个open让子类可以重写。
网络请求绑定进度对话框
在网络请求开始的时候弹一个菊花圈,结束/失败的时候关闭。因为用到协程viewModelScope
,就把launch
方法又封装了一下。
internal typealias NetLaunch<T> = suspend CoroutineScope.() -> BaseResponse<T>
val statusLiveData: MutableLiveData<CoroutineState> by lazy {
MutableLiveData<CoroutineState>()
}
fun <T> start(
refresh: Boolean = true,
block: NetLaunch<T>,
): LaunchHandler<T> {
val launchHandler = LaunchHandler<T>()
viewModelScope.launch {
try {
if (refresh) {
statusLiveData.value = CoroutineState.REFRESH
} else {
statusLiveData.value = CoroutineState.START
}
val result = block()
statusLiveData.value = CoroutineState.FINISH
launchHandler.successListener?.invoke(
LaunchResult.Success(result)
)
} catch (e: Exception) {
statusLiveData.value = CoroutineState.ERROR
if (launchHandler.errorListener == null) {
errorLiveData.value = e
} else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
}
}
return launchHandler
}
先忽略其它代码,主要看statusLiveData
,将协程状态发送到Activity基类BaseVMActivity,在基类中进行处理。
private fun initViewModelActions() {
viewModel.statusLiveData.observe(this, { status ->
status?.run {
when (this) {
CoroutineState.START -> {
//START
}
CoroutineState.REFRESH -> {
//REFRESH
ProgressDialog.showProgress(this@BaseVMActivity)
}
CoroutineState.FINISH -> {
//FINISH
ProgressDialog.dismissProgress()
}
CoroutineState.ERROR -> {
//ERROR
ProgressDialog.dismissProgress()
}
}
}
})
//默认错误处理
viewModel.errorLiveData.observe(this, {
ToastUtils.showShort(it.message)
})
}
不管用没用到协程,思路是一致的。网络请求入口包裹一层,将状态发送到页面基类,在基类统一处理。
网络请求API设计
现在都是MVVM了,那就在BaseViewModel里面统一。写kotlin还是要有kotlin的风格,搞一些lambda。
BaseViewModel
val statusLiveData: MutableLiveData<CoroutineState> by lazy {
MutableLiveData<CoroutineState>()
}
val errorLiveData: MutableLiveData<Exception> by lazy {
MutableLiveData<Exception>()
}
fun <T> start(
refresh: Boolean = true,
block: NetLaunch<T>,
): LaunchHandler<T> {
val launchHandler = LaunchHandler<T>()
viewModelScope.launch {
try {
if (refresh) {
statusLiveData.value = CoroutineState.REFRESH
} else {
statusLiveData.value = CoroutineState.START
}
val result = block()
statusLiveData.value = CoroutineState.FINISH
launchHandler.successListener?.invoke(
LaunchResult.Success(result)
)
} catch (e: Exception) {
statusLiveData.value = CoroutineState.ERROR
if (launchHandler.errorListener == null) {
errorLiveData.value = e
} else launchHandler.errorListener?.invoke(LaunchResult.Error(e))
}
}
return launchHandler
}
LaunchHandler
class LaunchHandler<T> {
var successListener: HandlerSuccess<T>? = null
var errorListener: HandlerError? = null
}
infix fun <T> LaunchHandler<T>.resumeWithSuccess(handler: HandlerSuccess<T>) = this.apply {
successListener = handler
}
infix fun <T> LaunchHandler<T>.resumeWithError(handler: HandlerError) = this.apply {
errorListener = handler
}
不传错误回调函数的情况下,将错误状态errorLiveData
发送给Activity基类Toast处理,当然这里也可以按照状态码做对应的Toast,最终调用处就很舒服了:MainViewModel
val contentLiveData by lazy {
MutableLiveData<String>()
}
fun getChapters() {
start {
repository.getChapters()
} resumeWithSuccess {
contentLiveData.value = GsonUtils.instance.toJson(it.response.data)
} resumeWithError {
ToastUtils.showShort(it.error.message)
}
}
resumeWithSuccess、resumeWithError
方法是中缀函数,调用时可以省略一个点。
repository.getChapters()
class MainRepository : BaseRepository() {
private val service = ApiServiceUtil.getApiService<MainApiService>()
suspend fun getChapters(): BaseResponse<List<Chapters>> {
return withContext(Dispatchers.IO) {
service.getChapters()
}
}
}
interface MainApiService : BaseService {
@GET("/wxarticle/chapters/json")
suspend fun getChapters(): BaseResponse<List<Chapters>>
}
没啥好说的,retrofit接口定义suspend方法,repository中withContext(Dispatchers.IO)
切换到IO线程。