jetpack compose 开发架构选择探讨(三)

646 阅读6分钟

本文所有代码均在compose_architecture中,需要的可以自取

前面两篇文章我们探讨了jetpack compose如何选用开发架构以及在compose中如何去实现MVVM 、 MVI 以及redux开发架构,当然这里的讨论不是让大家拘泥于某种开发架构以及某种实现形式,而是让大家明白compose的相关特性,以及如何根据compose的特性去灵活的实现相关开发架构,从而能够在项目中选用适合自己的开发架构。相信大家在上诉讨论中也能够了解compose相关特性,从而能够举一反三的实现其他开发架构。

今天我们再来看看redux在实际项目中可能遇到的一些问题,以及我们如何解决相关问题,首先我们先来看下面这个问题

依赖其他状态的状态

我们都知道redux是根据action通知reducer来改变具体的某一状态,但是实际项目中我们会遇到某些状态是依赖于其他状态的,当某一个状态值改变时,它也会跟着改变。举个简单的例子,购物车总价会跟着选择商品的状态而改变。

对于这个问题,在我们上篇实现的redux中会出现两种依赖情况

  1. 某状态只依赖于当前state的状态
  2. 某状态同时依赖于多个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使用过程中可能遇到的问题,以及相关解决方法,大家可以自行多体会体会,为什么这样做,以及其相关实现方法,尤其对于责任链的实现大家自己多琢磨下,体会这样实现的好处