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

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

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

上篇我们讲解了如何在compose中使用MVVM和MVI架构,并且在最后解决了如何解决多page的通信问题,本篇文章主要来讲解redux架构在compose的实现,不过由于上篇的MVI实现有点不是特别"优雅",没有充分发挥Flow和livedata之间的转换,因此本篇开始之前我们再换一种优雅的方式来实现一下上篇MVI

MVI实现之Flow和Livedata转换

之所以说上篇实现方式不太优雅,是因为我们在viewmodel层分别定义了用于观察state的livedata和响应action的flow,然后在flow的collect中将state转发给livedata,不过其实flow和livedata之间有更便捷的转换方式.asLiveData,可以直接将flow转换成livedata,同时也更贴合mvi的单向流,我们直接看代码

class MVIViewModel : ViewModel() {
    val userIntent = Channel<UiAction>(Channel.UNLIMITED)
    val viewState: LiveData<ViewState> = handleAction()


    private fun add(num: Int): ViewState {
        return if (viewState.value != null) {
            viewState.value!!.copy(viewState.value!!.count + num)
        } else {
            ViewState()
        }
    }

    private fun reduce(num: Int): ViewState {
        return if (viewState.value != null) {
            viewState.value!!.copy(viewState.value!!.count - num)
        } else {
            ViewState()
        }
    }

    private fun handleAction() =
        userIntent.receiveAsFlow().map {
            when (it) {
                is UiAction.AddAction -> add(it.num)
                is UiAction.ReduceAction -> reduce(it.num)
            }
        }.asLiveData()


    data class ViewState(val count: Int = 1)
    sealed class UiAction {
        class AddAction(val num: Int) : UiAction()
        class ReduceAction(val num: Int) : UiAction()
    }
}

可以看到这样实现出来的mvi更加符合单向流,用户发送action,根据action处理生成对应的state,view观察到state的变化渲染新的页面。

好了,接下来我们开始本篇文章的主题,redux

redux 架构

redux架构对于安卓开发者来说可能比较陌生,不过对于前端开发者来说应该是相当熟悉。不过其实它也不是很神秘,尤其在我们了解了mvi架构之后,其实他就是mvi架构的一个变版(或者说mvi架构借鉴了redux的思想),和mvi架构一样,redux也是一个强调单向流和state的架构,我们在上篇也说过mvi在多page通信时候有些不方便,我们也通过其他方法解决了,不过redux的解决方案更加暴力,它提供一个全局的viewmodel即store,state也是全局的state,通过全局的store就可以做到通信,因此我们可以把redux看成是一个全局的mvi即可,不过为了隔离每个页面的逻辑操作,redux中使用reducer专门来处理action和生成新的state

redux

我们来看下redux在compose中如何实现
redux中有以下几个概念

  1. state 即状态类,在compose中使用data class即可,不过为了提供initState方法,需要提供一个无参构造函数用来填充初始值,同时kotlin的copy方法可以方便创建新的state

  2. action 即操作类,也只需要data class即可,定义操作类型和传递的数据即可 建议这样定义

data class CountAction(val type: CountActionType, val data: Int) {
    enum class CountActionType {
        Add, Reduce
    }

    companion object {
        fun provideAddAction(data: Int): CountAction {
            return CountAction(CountActionType.Add, data = data)
        }

        fun provideReduceAction(data: Int): CountAction {
            return CountAction(CountActionType.Reduce, data = data)
        }
    }
}

通过enum定义action type
3. reducer 一个纯函数,通过action和当前state返回新的state即可,我这里定义了一个基类,使用时只需要实现对应方法即可,代码如下

abstract class Reducer<S, A>(val stateClass: Class<S>, val actionClass: Class<A>) {
    abstract suspend fun reduce(state: S, action: A): S
}

对于stateClass和actionClass用于store保存时候方便获取class type,用来表示该reducer需要处理的action和type,同时reduce函数标记为suspend ,可以方便切换执行的协程调度器

  1. store 全局处理类,负责分发action和获取state 以及监听state变化因此采用基于context的viewModel来实现,提供getState和dispatchAction方法,因为我们state是基于livedata的流实现,因此不需要提供专门的listener方法来监听state变化,代码如下
class StoreViewModel(val list: List<Reducer<Any, Any>>) : ViewModel() {
    private val _reducerMap = mutableMapOf<Class<*>, Channel<Any>>()
    private val _stateMap = mutableMapOf<Any, LiveData<Any>>()

    init {
        viewModelScope.launch {
            list.forEach {
                _reducerMap[it.actionClass] = Channel(Channel.UNLIMITED)
                _stateMap[it.stateClass] =
                    _reducerMap[it.actionClass]!!.receiveAsFlow().map { action ->
                        if (_stateMap[it.stateClass]?.value != null)
                            it.reduce(_stateMap[it.stateClass]!!.value!!, action = action)
                        else
                            it.stateClass.newInstance()
                    }.asLiveData()
                //send a message to init state
                _reducerMap[it.actionClass]!!.send("")

            }
        }

    }

    fun dispatch(action: Any) {
        viewModelScope.launch {
            _reducerMap[action::class.java]!!.send(action)
        }
    }

    suspend fun dispatchWithCoroutine(action: Any) {
        _reducerMap[action::class.java]!!.send(action)
    }

    fun <T> getState(stateClass: Class<T>): MutableLiveData<T> {
        return _stateMap[stateClass]!! as MutableLiveData<T>
    }
}

构建时传入所有reducer,并且创建用于发送action的flow流,flow流接收action并处理转换成state,同时保存所有state,getState方法通过state class来获取对应的state

我们还需要一个创建StoreViewModel的factoty,用来接受reducer参数代码如下

class StoreViewModelFactory(val list: List<Reducer<out Any, out Any>>?) :
    ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (StoreViewModel::class.java.isAssignableFrom(modelClass)) {
            return StoreViewModel(list = list!! as List<Reducer<Any, Any>>) as T
        }
        throw RuntimeException("unknown class:" + modelClass.name)
    }

}

同时我们需要提供一个快速获取store的函数,代码如下

reducer state action代码如下

@Composable
fun storeViewModel(
    list: List<Reducer<out Any, out Any>>? = null,
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalContext.current as ViewModelStoreOwner) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    }
): StoreViewModel =
    viewModel(
        StoreViewModel::class.java,
        factory = StoreViewModelFactory(list = list),
        viewModelStoreOwner = viewModelStoreOwner
    )

第一次初始化的时候需要传入reducer list,后续获取不需要,该viewmodel基于context提供,因此可以在全局获取到

接下来我们基于redux实现count add

代码如下

data class CountAction(val type: CountActionType, val data: Int) {
    enum class CountActionType {
        Add, Reduce
    }

    companion object {
        fun provideAddAction(data: Int): CountAction {
            return CountAction(CountActionType.Add, data = data)
        }

        fun provideReduceAction(data: Int): CountAction {
            return CountAction(CountActionType.Reduce, data = data)
        }
    }
}

data class CountState(val count: Int = 1)


class CountReducer :
    Reducer<CountState, CountAction>(CountState::class.java, CountAction::class.java) {
    override suspend fun reduce(
        countState: CountState,
        action: CountAction
    ): CountState {
        return withContext(Dispatchers.IO) {
            when (action.type) {
                CountAction.CountActionType.Add -> return@withContext countState.copy(count = countState.count + action.data)
                CountAction.CountActionType.Reduce -> return@withContext countState.copy(count = countState.count - action.data)
            }
        }
    }
}

screen代码如下

@Composable
fun Screen1(
    navController: NavController
) {
    val s = storeViewModel()
    val state: CountState by s.getState(CountState::class.java)
        .observeAsState(CountState(1))
    
    Content1(count = state.count,
        { navController.navigate("screen2") }
    ) {
        s.dispatch(CountAction.provideAddAction(1))
    }
}

redux优缺点和改进

  1. 首先redux采用了全局状态,如果对于某些页面退出后不需要保存状态,需要在退出时清理状态,这个缺点可以学习fish-redux来区分全局状态和局部状态,这样就可以解决这个问题,其实对于大多数应用来说,页面的局部状态是要多于全局状态的。

  2. 其实我们还发现由于每次reduce只能生成一个状态,虽然这个是redux的设计哲学,但是实际应用中可能存在诸多不便,比如我们获取数据的时候,通常需要先切换view状态到loading,在获取数据后刷新数据并切换view到显示状态,按照redux的处理,这么简单的操作需要发送两个action才能做到,这样使用起来很不方便,因此我们可以简单改造下让redux支持一个reduce发送多个状态

首先我们改造reduce函数,让其不再直接返回state,而是返回flow,这样就可以通过flow emit多次state,代码如下

abstract class Reducer<S, A>(val stateClass: Class<S>, val actionClass: Class<A>) {
    abstract  fun reduce(state: S, action: A): Flow<S>

}

同时改造store代码

class StoreViewModel(val list: List<Reducer<Any, Any>>) : ViewModel() {
    private val _reducerMap = mutableMapOf<Class<*>, Channel<Any>>()
    private val _stateMap = mutableMapOf<Any, LiveData<Any>>()

    init {
        viewModelScope.launch {
            list.forEach {
                _reducerMap[it.actionClass] = Channel(Channel.UNLIMITED)
                _stateMap[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")
                                }
                            }
                    }.asLiveData()
                //send a message to init state
                _reducerMap[it.actionClass]!!.send("")

            }
        }

    }

    fun dispatch(action: Any) {
        viewModelScope.launch {
            _reducerMap[action::class.java]!!.send(action)
        }
    }

    suspend fun dispatchWithCoroutine(action: Any) {
        _reducerMap[action::class.java]!!.send(action)
    }

    fun <T> getState(stateClass: Class<T>): MutableLiveData<T> {
        return _stateMap[stateClass]!! as MutableLiveData<T>
    }
}

通过flatMapConcat 返回reduce flow,我们来看下如何完成reduce

class CountReducer :
    Reducer<CountState, CountAction>(CountState::class.java, CountAction::class.java) {
    override fun reduce(
        countState: CountState,
        action: CountAction
    ): Flow<CountState> {
        return flow {
            emit(action)
        }.flowOn(Dispatchers.IO).flatMapConcat { action ->
            flow {
                if (action.type == CountAction.CountActionType.Add)
                    emit(countState.copy(count = countState.count + action.data))
                else
                    emit(countState.copy(count = countState.count - action.data))
                kotlinx.coroutines.delay(1000)
                emit(countState.copy(count = countState.count + 3))
            }
        }.flowOn(Dispatchers.IO)
    }
}

通过flow 的emit就可以实现多个state发送

总结

通过两篇讲解,我们发现结合jetpack组件我们可以很方便在compose中实现各种架构,并且我们也发现多数架构的理念是相同的,其实更希望大家能够举一反三,明白架构是为了方便开发的,而不是拘泥于某种架构,需要大家根据自己项目灵活选择架构和对架构进行变形,以让此来更好的服务我们项目开发

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值