本文所有代码均在compose_architecture中,需要的可以自取
上篇我们讲解了如何在compose中使用MVVM和MVI架构,并且在最后解决了如何解决多page的通信问题,本篇文章主要来讲解redux架构在compose的实现,不过由于上篇的MVI实现有点不是特别"优雅",没有充分发挥Flow和livedata之间的转换,因此本篇开始之前我们再换一种优雅的方式来实现一下上篇MVI
之所以说上篇实现方式不太优雅,是因为我们在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架构对于安卓开发者来说可能比较陌生,不过对于前端开发者来说应该是相当熟悉。不过其实它也不是很神秘,尤其在我们了解了mvi架构之后,其实他就是mvi架构的一个变版(或者说mvi架构借鉴了redux的思想),和mvi架构一样,redux也是一个强调单向流和state的架构,我们在上篇也说过mvi在多page通信时候有些不方便,我们也通过其他方法解决了,不过redux的解决方案更加暴力,它提供一个全局的viewmodel即store,state也是全局的state,通过全局的store就可以做到通信,因此我们可以把redux看成是一个全局的mvi即可,不过为了隔离每个页面的逻辑操作,redux中使用reducer专门来处理action和生成新的state
我们来看下redux在compose中如何实现
redux中有以下几个概念
state 即状态类,在compose中使用data class即可,不过为了提供initState方法,需要提供一个无参构造函数用来填充初始值,同时kotlin的copy方法可以方便创建新的state
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 ,可以方便切换执行的协程调度器
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采用了全局状态,如果对于某些页面退出后不需要保存状态,需要在退出时清理状态,这个缺点可以学习fish-redux来区分全局状态和局部状态,这样就可以解决这个问题,其实对于大多数应用来说,页面的局部状态是要多于全局状态的。
其实我们还发现由于每次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中实现各种架构,并且我们也发现多数架构的理念是相同的,其实更希望大家能够举一反三,明白架构是为了方便开发的,而不是拘泥于某种架构,需要大家根据自己项目灵活选择架构和对架构进行变形,以让此来更好的服务我们项目开发