Android端先后出现了MVC、MVP、MVVM等多种应用架构,这些架构都是发源于Web前端开发而后被移动端借鉴和采用。随着React等前段框架的兴起,Redux架构逐渐成为前端UI开发新的主流,预计未来会像各种MVX架构一样从前端流行到移动端,本文介绍的 RxRedux 便是一个Android端上的Redux实现
Redux
Redux是一个便于在React等声明式UI开发方式中进行状态管理的框架。相对于MVVM的ViewModel与View之间的双向通信,Redux强调单向数据流的思想,状态变化的通知永远在一个方向上流动,这会更有利于状态的管理和追溯
上图是Redux单向数据流运转中的各个角色,Redux定义了三大原则来保证单向数据流系统的正常工作:
-
Single source of truth
所有state都在全局唯一的store中管理,这样才能保证全局状态的一致性 -
State is read-only
state是只读的,不能在原对象上更新,状态更新需要生成新的state对象。如果直接修改则无法进行diff,无法察觉具体变更的位置 -
Mutations are written as pure functions
state变更函数(reducer)是一个纯函数。纯函数代表无任何副作用,唯一输入决定唯一输出,从而保证了UI的变化是可预期的
RxRedux
RxRedux基于Kotlin和RxJava的基础上实现了上述Redux中的各个角色并贯彻了其三大原则。
ReduxStore
ReduxStore是管理所有state的容器,接收三个参数创建后返回Observable对象用来订阅state变化
fun <S : Any, A : Any> Observable<A>.reduxStore(
initialStateSupplier: () -> S,
sideEffects: Iterable<SideEffect<S, A>>,
reducer: Reducer<S, A>
): Observable<S>
- initialStateSupplier: 初始state
- sideEffects:可处理的SideEffect(副作用)列表
- reducer:更新state的纯函数
Action
Action可以任意自定义类型,所有的Action都会发送给Store,最终被Store的reducer接收。reducer根据Action和当前state计算得到新的state,Action也可能是用来执行SideEffect的、不用来计算新state,此时reducer可以返回原state(后文有叙)。Action可以携带payload,为reducer提供计算state的所需信息
Reducer
Reducer就是一个lambda表达式 (State, Action) -> State
,通过Action和当前state,计算出一个新的state
Side Effect
SideEffect 可以用来进行异步请求等副作用,Redux中类似的角色被称为middleware
。
我们用(Observable<Action>, StateAccessor<State>) -> Observable<Action>
来定义一个SideEffect,有点像一个reducer接受一个Action和State,但是返回的是一个Action,Action的终点会分发给reducer并得到最终state。
每个SideEffect返回的是一个Observable,所以可能stream中有返回多个Action。这些Action会被Reducer或者其他SideEffect接收。例如一个数据请求过程可能会将load状态以及result多次分别返回。
StateAccessor
StateAccessor 就是一个 function () -> State
的lambda,通过 StateAccessor 可以获取最新的state。
Sample
通过一个例子看一下具体使用方法。我们通过RxRedux实现一个分页加载,最终显示一个Persons
的列表:
State
data class State {
val currentPage : Int,
val persons : List<Person>, // The list of persons
val loadingNextPage : Boolean,
val errorLoadingNextPage : Throwable?
}
val initialState = State(
currentPage = 0,
persons = emptyList(),
loadingNextPage = false,
errorLoadingNextPage = null
)
- state中存放当前页面显示用的列表信息
- data class 的成员全是
val
的,保证了state的不可变性
Action
sealed class Action {
object LoadNextPageAction : Action() // Action to load the first page. Triggered by the user.
data class PageLoadedAction(val personsLoaded : List<Person>, val page : Int) : Action() // Persons has been loaded
object LoadPageAction : Action() // Started loading the list of persons
data class ErrorLoadingNextPageAction(val error : Throwable) : Action() // An error occurred while loading
}
使用data class
定义携带payload的Action;object
定义没有payload的Action:
- LoadNextPageAction : 加载下一页数据
- LoadPageAction : 告诉UI加载已经开始
- PageLoadedAction : 通过SideEffect加载数据返回一个带有payload的Action
- ErrorLoadingNextPageAction:数据加载时的错误
SideEffect
fun loadNextPageSideEffect (actions : Observable<Action>, state: StateAccessor<State>) : Observable<Action> =
actions
.ofType(LoadNextPageAction::class.java) // This side effect only runs for actions of type LoadNextPageAction
.switchMap {
// do network request
val currentState : State = state()
val nextPage = state.currentPage + 1
backend.getPersons(nextPage)
.map { persons : List<Person> ->
PageLoadedAction(
personsLoaded = persons,
page = nextPage
)
}
.onErrorReturn { error -> ErrorLoadingNextPageAction(error) }
.startWith(LoadPageAction)
}
- ofType :指定SideEffect接收的Action类型
- backend.getPersons(nextPage):异步加载数据
- startWith:数据加载之前先发送一个Action
- onErrorReturn:加载出错时的Action
- startWith(LoadPageAction): 异步获取数据开始,先发送Action告诉UI显示Loading
Reducer
// Reducer is just a typealias for a function
fun reducer(state : State, action : Action) : State =
when(action) {
is LoadPageAction -> state.copy (loadingNextPage = true)
is ErrorLoadingNextPageAction -> state.copy( loadingNextPage = false, errorLoadingNextPage = action.error)
is PageLoadedAction -> state.copy(
loadingNextPage = false,
errorLoadingNextPage = null
persons = state.persons + action.persons,
page = action.page
)
else -> state // Reducer is actually not handling this action (a SideEffect does it)
}
- Redux中的state是
immutable
的,所以不能在原对象上更新state,需要通过data class
的copy
方法创建新对象 - 有的Action是为SideEffect准备的,例如
LoadNextPageAction
,所以reducer无需处理,为了兼容返回state的接口返回当前state即可,由于state没有发生变化,UI不会有任何变化
create store & observe state change
val input: Relay<Action> = PublishRelay.create()
val actions : Observable<Action> = input
val sideEffects : List<SideEffect<State, Action> = listOf(::loadNextPageSideEffect, ... )
actions
.reduxStore( initialState, sideEffects, ::reducer )
.subscribe( state -> view.render(state) )
- actions:Action在Observable中进行分发
- reduxStore:创建Store,所有的Action都会发送到store
- subscribe:监听state变化,刷新UI
当然,我们也可以将reduxStore
和subscribe
分开,有时一个store会被多处监听
val state = actions
.reduxStore( initialState, sideEffects, ::reducer )
.distinctUntilChanged()
distinctUntilChanged
可以过滤掉没有变化的state,避免UI的无效刷新
dispatch action
//dispatch
viewModel.input.accept(Action.LoadFirstPageAction)
//observe
viewModel.store.subscribe { ... }
我们将RxRedux放到ViewModel中管理,通过ViewModel进行Action的分发,以及State的监听。
RxRedux很好地替代了LiveData的角色,基于Redux的单项数据流思想,所有的state的变化只发生在reducer,可以很好地集中管理、跟踪状态变化。LiveData的双向通信机制(既可以observe
又可以set
/post
,虽然LiveData接口不提供set
/post
,但很多时候大家更爱用MutableLiveData
),当项目复杂时,无法把握数据变化的来源,不能很好地进行state管理。
通过RxRedux最终实现了预期的页面效果
最后
- View发送
LoadNextAction
到Store - SideEffect发起异步请求,同时发送
LoadingPageAction
给Store - Reducer处理
LoadingPageAction
后更新State,UI响应State显示Loading - SideEffect异步请求成功返回,发送
PageLoadedAction
- Reducer处理
PageLoadedAction
,更新State,UI响应State并显示新数据
RxRedux巧妙地借助RxJava进行Action的分发以及SideEffect的处理,完成了一个Redux实现。从RxJava的角度看,reduxStore的作用类似scan
操作符,接收state并通过reduce运算更新state发送到下游。
see more: https://github.com/freeletics/rxredux