在这里先介绍一下写这个开源项目的原因,主要原因是想让自己封装的玩具有一个实际的应用,看看在实际开发中会有什么问题,看看哪些还需要完善。事实证明也确实如此,在开发的过程中发现自己封装的玩具还确实有很多不足。这些玩具包括一些基础架构,也包括一些插件,还有一些好的想法。
首先说下应用的技术栈
- 用kts和buildSrc进行了依赖版本库的统一控制
- 用协程+flow+retrofit 进行了一个网络库的封装
- 用Hilt对ViewModel ,ApiService ,Repository 进行了依赖注入
- 图片加载库使用的是Glide,后续可能会考虑Coil
- 整体架构方面是在MVVM的基础上,进行了MVI架构的尝试。
现在这个项目还并没有开发完成,有一些功能还没有完善,也有一些问题还没来得及修复,本来我是打算想等待开发完成之后再进行开源,但是我感觉在开发过程中开源,可以更快的吸收更好的建议,更好的想法,可以在开发过程中就可以将这些好的想法进行应用。
现在来说说细节吧
依赖管理方面是使用buildSrc+kts,在项目编译的时候 首先会去执行buildSrc下面的内容,这样就可以先把依赖常量在这里进行定义,然后直接在kts里面进行引用
object Libraries {
const val hiltlifecycleviewmodel = "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03"
const val hiltandroid = "com.google.dagger:hilt-android:2.36"
const val daggercompiler = "com.google.dagger:hilt-android-compiler:2.36"
const val hiltcompiler = "androidx.hilt:hilt-compiler:1.0.0"
const val base = "com.xl:base:1.1.3"
}
implementation(Libraries.hiltlifecycleviewmodel)
implementation(Libraries.hiltandroid)
implementation(Libraries.base)
kapt(Libraries.daggercompiler)
kapt(Libraries.hiltcompile
这里选择kts 的原因它如果有写法错误可以及时作出提醒,就像我们写业务代码一样,不用像groovy那种,只有去sync 的时候 才会报出错误。点击引用跳转也相对方便一些。
架构层次还是按照 View -> ViewModel ->Repository 这样的结构来的
按照我以前的处理操作,可能会像这样操作
进行数据的刷新,在view层 直接获取第一页的数据
mainViewModel.getArc(0)
ViewMoodel 层 从Repository 获取数据,然后交给livedata
private val _data = MutableLiveData<ArticleBean?>()
val data: LiveData<ArticleBean?> = _data
fun getArc(page: Int) {
viewModelScope.launch {
_data.value = repository.getArticles(page = page).data
}
}
最后在View层对数据进行订阅,然后更新UI。
mainViewModel.data.observe(this){it->
.......
//更新UI
}
这样做有问题吗?这样做是没有问题的。
但是当这个页面有N多个请求的时候,想想会怎么样?
可能就会发生View层和ViewModel层交互比较凌乱的情况,每个暴露的livedata 也都要定义两个,可变的内部使用,不可变的内部订阅。View层也会出现各种订阅满天飞的情况。
为了解决这个问题,所以就将用户的操作(例如刷新数据),和UI状态(例如:返回数据刷新UI )抽象起来,这里我是使用了flow 去实现
数据的订阅只需要关心ViewState 就可以了。
MVI 中的 I 就是Intent,这并不是跳转所用的Intent,它的意思是一个时间,例如刷新数据
在这里我定义的事件是ViewEvent,UI状态我定义的是ViewState
这是我定义的ViewState
data class ViewState(
var homeInfo: HomeInfo? = null,
var rankingInfo: RankingInfo? = null,
)
然后在View层直接这样订阅就可以:
viewModel.state.collectHandlerFlow(this) { state ->
state.homeInfo?.let { it ->
//更新UI
}
state.rankingInfo?.let {
//更新UI
}
}
关于collHandlerFlow 我是做了一下这样的封装:
inline fun <T> StateFlow<T>.collectHandlerFlow(
lifecycleOwner: LifecycleOwner,
crossinline action: (T) -> Unit,
) = lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { collect { T -> action(T) } }
}
关于ViewEvent ,我是选择使用了Channel
在Channel中,每个事件被传递给一个订阅者。试图在没有订阅者的情况下发布事件,一旦Channel缓冲区变满就会暂停,等待订阅者出现。发布的事件不会被丢弃。
private val pendingActions = Channel<ViewEvent>(Channel.BUFFERED)
init {
viewModelScope.launch {
pendingActions.consumeAsFlow().collect { action ->
when (action) {
ViewEvent.Refresh -> getHome(0)
}
}
}
}
fun submitAction(action: ViewEvent) {
viewModelScope.launch {
if (!pendingActions.isClosedForReceive) {
pendingActions.send(action)
}
}
}
在view层发送刷新事件
viewModel.submitAction(ViewEvent.Refresh)
这样就把用户操作的事件和UI状态做了一个整体的封装,对数据的关注点进行了集中,只需要关心状态和事件就可以。
这只是一套简单的封装,其中还有很多的不足,在以后的一些时间 会去慢慢完善。
在这个项目中,我是将一些基础共用 和 base 方面的东西,统一封装在了另外一个library中。在这个library 中就是单纯的写common 和 base,和一些基本的依赖,然后上传到了我的个人仓库中,这样做主要是因为 我可以将这套东西很快的集成到另外一个新项目中。
后面的计划是在功能完善和bug修复上,然后会尝试使用一下coil框架,做简单尝试;后面会尝试使用Compose重写一版。
暂时先水到这里吧,等有新的想法会再继续分享。有问题或者其他更好的建议,欢迎留言。
OpenEye:github.com/WngYilei/Op…