RxRedux - Android端的Redux框架

Android端先后出现了MVC、MVP、MVVM等多种应用架构,这些架构都是发源于Web前端开发而后被移动端借鉴和采用。随着React等前段框架的兴起,Redux架构逐渐成为前端UI开发新的主流,预计未来会像各种MVX架构一样从前端流行到移动端,本文介绍的 RxRedux 便是一个Android端上的Redux实现

Redux


Redux是一个便于在React等声明式UI开发方式中进行状态管理的框架。相对于MVVM的ViewModel与View之间的双向通信,Redux强调单向数据流的思想,状态变化的通知永远在一个方向上流动,这会更有利于状态的管理和追溯在这里插入图片描述
上图是Redux单向数据流运转中的各个角色,Redux定义了三大原则来保证单向数据流系统的正常工作:

  1. Single source of truth
    所有state都在全局唯一的store中管理,这样才能保证全局状态的一致性

  2. State is read-only
    state是只读的,不能在原对象上更新,状态更新需要生成新的state对象。如果直接修改则无法进行diff,无法察觉具体变更的位置

  3. 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 classcopy方法创建新对象
  • 有的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

当然,我们也可以将reduxStoresubscribe分开,有时一个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的监听。

通过向Rx流中发射数据分发Action
RxRedux很好地替代了LiveData的角色,基于Redux的单项数据流思想,所有的state的变化只发生在reducer,可以很好地集中管理、跟踪状态变化。LiveData的双向通信机制(既可以observe又可以set/post,虽然LiveData接口不提供set/post,但很多时候大家更爱用MutableLiveData),当项目复杂时,无法把握数据变化的来源,不能很好地进行state管理。

通过RxRedux最终实现了预期的页面效果
在这里插入图片描述


最后


在这里插入图片描述

  1. View发送LoadNextAction到Store
  2. SideEffect发起异步请求,同时发送LoadingPageAction给Store
  3. Reducer处理LoadingPageAction后更新State,UI响应State显示Loading
  4. SideEffect异步请求成功返回,发送PageLoadedAction
  5. Reducer处理PageLoadedAction,更新State,UI响应State并显示新数据

RxRedux巧妙地借助RxJava进行Action的分发以及SideEffect的处理,完成了一个Redux实现。从RxJava的角度看,reduxStore的作用类似scan操作符,接收state并通过reduce运算更新state发送到下游。

see more: https://github.com/freeletics/rxredux

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值