jetpack compose 开发架构选择探讨(三)
本文所有代码均在compose_architecture中,需要的可以自取
前面两篇文章我们探讨了jetpack compose如何选用开发架构以及在compose中如何去实现MVVM 、 MVI 以及redux开发架构,当然这里的讨论不是让大家拘泥于某种开发架构以及某种实现形式,而是让大家明白compose的相关特性,以及如何根据compose的特性去灵活的实现相关开发架构,从而能够在项目中选用适合自己的开发架构。相信大家在上诉讨论中也能够了解compose相关特性,从而能够举一反三的实现其他开发架构。
今天我们再来看看redux在实际项目中可能遇到的一些问题,以及我们如何解决相关问题,首先我们先来看下面这个问题
依赖其他状态的状态
我们都知道redux是根据action通知reducer来改变具体的某一状态,但是实际项目中我们会遇到某些状态是依赖于其他状态的,当某一个状态值改变时,它也会跟着改变。举个简单的例子,购物车总价会跟着选择商品的状态而改变。
对于这个问题,在我们上篇实现的redux中会出现两种依赖情况
- 某状态只依赖于当前state的状态
- 某状态同时依赖于多个state状态
对于第一个问题很好解决,我们只需要在当前state中定义一个变量,它根据某些规则依赖于某一个状态值,如我们在Count例子中需要一个状态是count的两倍,只需要这样改变CountState即可,代码如下
data class CountState(val count: Int = 1) {
val doubleCount: Int get() = count * 2
}
doubleCount 依赖于count,始终是其两倍
对于第二个问题有些复杂,不过因为我们redux本身就是基于流(即flow和livedata来实现的,即js中的stream)来实现的,因此可以很轻松的根据流获取到值的变化,不过这里需要我们更换之前实现中的flow类型为SharedFlow。
这里先简单介绍一下SharedFlow,它是一个热流,并且可以被多次collect,即相当于是广播,因此我们可以提供一个方法来订阅多个状态,然后定义transform来定义状态间的依赖关系
变更StoreViewModel代码如下
class StoreViewModel(
private val list: List<Reducer<Any, Any>>,
middleWares: List<MiddleWare> = emptyList()
) :ViewModel(){
……
val sharedMap = mutableMapOf<Any, SharedFlow<Any>>()
……
init {
……
list.forEach {
_reducerMap[it.actionClass] = Channel(Channel.UNLIMITED)
sharedMap[it.stateClass] =
_reducerMap[it.actionClass]!!.receiveAsFlow().flatMapConcat { action ->
if (stateMap[it.stateClass]?.value != null)
it.reduce(stateMap[it.stateClass]!!.value!!, action = action)
else
flow {
try {
emit(it.stateClass.newInstance())
} catch (e: InstantiationException) {
throw IllegalArgumentException("${it.stateClass} must provide zero argument constructor used to init state")
}
}
}.shareIn(viewModelScope, SharingStarted.Lazily, 1)
stateMap[it.stateClass] = sharedMap[it.stateClass]!!
.asLiveData()
……
}
}
即增加一个sharedMap用来保存状态的广播,然后原有的stateMap改为订阅sharedMap得到用于其state值
然后我们增加一个函数用来增加依赖状态,代码如下
inline fun <reified T, reified R> depState(
crossinline transform: (T) -> R,
scope: CoroutineScope = viewModelScope
) {
sharedMap[R::class.java] = sharedMap[T::class.java]!!
.map {
transform(it as T)
}.shareIn(scope = scope, SharingStarted.Lazily, 1) as SharedFlow<Any>
stateMap[R::class.java] =
sharedMap[R::class.java]!!.asLiveData(context = scope.coroutineContext)
}
inline fun <reified T1, reified T2, reified R> depState(
crossinline transform: (T1, T2) -> R,
scope: CoroutineScope = viewModelScope
) {
sharedMap[R::class.java] = sharedMap[T1::class.java]!!.combine(sharedMap[T2::class.java]!!)
{ t1, t2 ->
transform(t1 as T1, t2 as T2)
}.shareIn(scope = scope, SharingStarted.Lazily, 1) as SharedFlow<Any>
stateMap[R::class.java] = sharedMap[R::class.java]!!
.asLiveData(context = scope.coroutineContext)
}
函数接收依赖的状态class和依赖的transform用于定义依赖规则,然后订阅其依赖状态的广播,当依赖的状态发生改变时,也会同步更新当前状态,同时在sharedMap和stateMap中增加新的状态,用于后续的订阅,本示例只提供了最多两个状态的依赖,对于更多的状态代码其实一样,只需要多增加几个函数利用combine合并流即可
我们看下如何使用,也非常简单,只需要定义新的state,然后定义transform,代码如下
data class DepState(val depCount: Int = 0) {
companion object {
fun transform(countState: CountState): DepState {
return DepState(countState.count * 2)
}
}
}
data class DepState2(val depCount: Int = 0) {
companion object {
fun transform(countState: CountState, depState: DepState): DepState2 {
return DepState2(countState.count + depState.depCount)
}
}
}
这里将transform定义在state中即可,然后初始化的时候将state通过depState将状态保存在store中即可,然后就可以直接和普通状态一样使用即可
s.depState(DepState::transform)
s.depState(DepState2::transform)
middleware
redux利用纯函数的reducer和action来实现单向数据流,这样状态便于管理,但是使用起来也会有许多不便,并且纯函数在做很多操作时候也会有很多局限,因此需要middleware来扩充其能力,今天我们就来讲解下如何实现middleware。
middleware就是在store dispatch之前进行拦截,执行一些操作,所以这是一个典型的责任链模式,然后我们需要将store和action一层层传递进去以便拦截时候调用,因此我们需要定义两个接口
interface DispatchAction {
abstract suspend fun dispatchAction(action: Any)
}
interface MiddleWare {
abstract suspend fun invoke(store: StoreViewModel): (DispatchAction) -> DispatchAction
}
其中Middleware为中间件顶层类,DispatchAction为具体处理类,MiddleWare invoke方法返回一个lambda方法(这里也可以定义成一个接口,为了减少接口数量,本例就直接使用lambda方法)用于串联责任链,他接收责任链的下一个DispatchAction方法,用来实现链式调用。
然后我们在StoreViewModel构造函数中增加middleWares用于接收中间件
然后在init方法中串联起责任链代码如下
dispatchActionHead = this@StoreViewModel
val reserve = middleWares.map {
it.invoke(this@StoreViewModel)
}.toList().asReversed()
reserve.forEach {
dispatchActionHead = it.invoke(dispatchActionHead)
}
首先执行Middleware invoke方法获取责任链每个的节点,然后反向,从尾部串联起责任链,并记录责任链头节点,这里为了统一,将store的dispatch方法也看成责任链一个节点,并且是尾节点,然后修改dispatch方法,从责任链头节点依次执行
override suspend fun dispatchWithCoroutine(action: Any) {
dispatchActionHead.dispatchAction(action = action)
}
//store的dispatchAction方法,即责任链的尾节点
override suspend fun dispatchAction(action: Any) {
_reducerMap[action::class.java]!!.send(action)
}
接下来我们来看下如何实现MiddleWare,我们就实现一个类似redux-thunk的中间件,让store可以dispatch一个方法,而不是Action,代码如下
class FunctionActionMiddleWare : MiddleWare {
interface FunctionAction {
suspend fun invoke(dispatchAction: StoreDispatch, state: StoreState)
}
override suspend fun invoke(store: StoreViewModel): (DispatchAction) -> DispatchAction {
return { next ->
object : DispatchAction {
override suspend fun dispatchAction(action: Any) {
if (action is FunctionAction)
action.invoke(store, store)
else {
next.dispatchAction(action = action)
}
}
}
}
}
}
代码很简单,即发现如果action是一个FunctionAction,就直接调用该方法,如果不是则交给下个中间件处理
这样我们就可以直接dispatch一个方法,代码如下
s.dispatch(object : FunctionActionMiddleWare.FunctionAction {
override suspend fun invoke(storeDispatch: StoreDispatch, state: StoreState) {
storeDispatch.dispatch(CountAction.provideAddAction(1))
storeDispatch.dispatch(CountAction.provideAddAction(1))
}
})
这样我们就可以一次发送两个action了,当然中间件还有许多其他用法,大家可以自己探索
这里为了能够在中间件dispatch和getState,从store中抽象出了两个顶级接口
interface StoreDispatch {
fun dispatch(action: Any)
suspend fun dispatchWithCoroutine(action: Any)
}
interface StoreState {
fun <T> getState(stateClass: Class<T>): MutableLiveData<T>
}
本文所有代码均在compose_architecture中,大家可以去查看完整代码,介于篇幅原因,这里就不贴出完整代码了
总结
本文讲解了redux使用过程中可能遇到的问题,以及相关解决方法,大家可以自行多体会体会,为什么这样做,以及其相关实现方法,尤其对于责任链的实现大家自己多琢磨下,体会这样实现的好处