说在前头:
纪晓岚问和珅,为何他们往哀鸿粥里掺沙子,和珅道:“你是有所不知啊,如不掺沙子,哀鸿怕是一口粥也喝不上啊”。
同理,架构的存在是为 “在实践开发过程中消除不可预期问题”,而非为架构而架构。
为使架构组件真实能在团队中遍及,乃至毕竟有用达成 “消除大部分不可预期问题” 意图,本文采用 “淡化理论概念 + 规划简明易懂” 办法,让团队新手老手都能因为 “这结构好懂、简洁、用着舒畅”,而自可是然效仿和运用。
本文假设您已具有 State、Event、照应式编程、BehaviorSubject、PublishSubject、函数式编程、纯函数、副作用、MVI、软件工程、规划方式准则、一起性问题 等前置知识,且在团队中推行 MVI 遭遇晦气,想就近找到平替方案。
通过本文可快速了解:
1.为何运用 MVI,是否非用不可,
2.为何毕竟考虑 SharedFlow 完结,
3.repeatOnLifecycle + SharedFlow 完结 MVI 思路
前置知识
上一期《关于 MVI,我想聊的更了解些》,我们已烘托如下信息:
1.照应式编程暗示人们 应当总是向数据源央求数据,并在指定查询者中照应数据的改变。
2.照应式编程的优点是 便于检验,有输入必有回响。
3.照应式编程 存在 “多个粘性查询者回推不符预期数据” 的缝隙。
4.MVI 便是 通过 “聚合页面状态” 消除该缝隙。
5.鉴于 “照应式编程” 便于检验,官方出于齐备性考虑,也是以照应式编程作为架构示例。
6.因为 Kotlin 抹平语法杂乱度,便于照应式编程,且 Kotlin 开发者更简单跟着官方文档走,接受这套开发方式,乃至 有机遇踩坑,且有动力通过 MVI 改善。
7.Android 开发者 70% 仍是纯 Java,照应式编程在 Android Java 开发者中的推行不太抱负。
为何运用 MVI,是否非用不可
所以至此,第一个问题的答案呼之欲出,
因为对一部分隔发者来说,照应式编程很香,但又存在缝隙,即部分 BehaviorSubject 结构存在过度规划,导致存在 “多个粘性查询者不符预期回推” 的缝隙,所以需求 MVI 出马处理。
注:什么是过度规划,如何避免?详细见上期解析,本文不再累述。
那有人可能会问,已然部分 BehaviorSubject 结构过度规划,那替换成没有过度规划的 BehaviorSubject,比如 ObservableField 不就可以了,
可以是可以,不过也看状况,MVI 天然合适与 Jetpack Compose 搭配,
如果是运用 Jetpack Compose,就用不上 ObservableField,只能运用 LiveData/StateFlow 来回推 UiStates,也即只能通过 MVI 来消除缝隙,难有别的平替方案。
所以如果暂不运用 Jetpack Compose,依据上期的剖析易知,只要消除过度规划,就能从源头上把问题处理,无所谓开发者用不用 MVI。
鉴于上期文末已共享 MVI 最小本钱平替方案,本文直接从 “规划方式准则” 动身,探求一种更加普适的方案,信赖阅读后你会耳目一新。
MVI 经典模型
1.创建一个 UiStates,反映其时页面的全部状态。
data class UiStates {
val weather : Weather,
val isLoading : Boolean,
val error : List<UiEvent>,
}
2.创建一个 Intent,用于发送央求时带着参数,和指明其时想履行的业务。
sealed class MainPageIntent {
data class GetWeather(val cityCode) : MainPageIntent()
}
3.创建一个 Actions,用于 reduce 其时业务的 partialChange 并生成新的 UiStates。
sealed class MainPageActions {
fun reduce(oldStates : UiStates) : UiStates {
return when(this){
Loading -> oldStates.copy(isLoading = true)
is Success -> oldStates.copy(isLoading = false, weather = this.weather)
is Error -> oldStates.copy(isLoading = false, error = listOf(UiEvent(msg)))
}
}
object Loading : MainPageActions()
data class Success(val weather : Weather) : MainPageActions()
data class Error(val msg : String) : MainPageActions()
}
4.创建其时页面运用的 MVI-Model。
class MainPageModel : MVI_Model<UiStates>() {
private val _stateFlow = MutableStateFlow(UiStates())
val stateFlow = _stateFlow.asStateFlow
private fun sendResult(uiStates: S) = _stateFlow.emit(uiStates)
fun input(intent: Intent) = viewModelScope.launch{ onHandle() }
private suspend fun onHandle(intent: Intent) {
when(intent){
is GetWeather -> {
sendResult(MainPageActions.Loading.reduce(oldStates)
val response = api.post()
if(response.isSuccess) sendResult(
MainPageActions.Success(response.data).reduce(oldStates)
else sendResult(
MainPageActions.Error(response.message).reduce(oldStates)
}
}
}
}
5.创建 MVI-View,并在 stateFlow 中照应 MVI-Model 数据。
class MainPageActivity : Android_Activity(){
private val model : MainPageModel
fun onCreate(){
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.stateFlow.collect {uiStates ->
progressView.setProgress(uiStates.isLoading)
tvWeatherInfo.setText(uiStates.weather.info)
...
}
}
model.input(Intent.GetWeather(BEI_JING))
}
}
整个流程用一张图来标明即:
改善版别 1:运用 DataBinding 防抖
考虑到 DataBinding ObservableField 存在防抖特性,故页面可通过 ObservableField 完结结束状态改变,尽可能消除 “控件刷新” 功用开销。
class MainPageActivity : Android_Activity(){
private val model : MainPageModel
private val views : MainPageViews
fun onCreate(){
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.stateFlow.collect {uiStates ->
views.progress.set(uiStates.isLoading)
views.weatherInfo.set(uiStates.weather.info)
...
}
}
model.input(Intent.GetWeather(BEI_JING))
}
class MainPageViews : Jetpack_ViewModel() {
val progress = ObservableBoolean(false)
val weatherInfo = ObservableField<String>("")
...
}
}
不过这要求开发者具有 DataBinding 运用经历、额外书写 DataBinding 样板代码和 XML 绑定。
运用 distinctUntilChanged
除了 DataBinding,网上还说到有 2 类方案:
一类是通过 distinctUntilChanged 来为 ViewStates 的属性供应防抖,
但如此后续便难屏蔽 diff,只能露出给开发者手动 map distinct 分流,增加手写代码量和认知本钱,
class View-Controller : Android-Activity() {
fun onCreate() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState
.map { it.isDownload }
.distinctUntilChanged()
.collect { progress = it }
viewModel.uiState
.map { it.Setting }
.distinctUntilChanged()
.collect { btnChecked = it }
...
}
}
}
}
运用 RecyclerView DiffUtils
另一类是通过 RecyclerView 编写页面。
如此便难支持杂乱交互作用、简单引进其他不可预期问题,也难在大都开发者中遍及开(有点为 MVI 而 MVI),且 DiffUtils 需手动配备,equals 列表鳞次栉比易漏写或写错,
val diff = object : DiffUtil.ItemCallback<ViewStates>() {
override fun areItemsTheSame(oldItem: ViewStates, newItem: ViewStates): Boolean {
return oldItem.equals(newItem)
}
override fun areContentsTheSame(oldItem: ViewStates, newItem: ViewStates): Boolean {
return oldItem.progress().equals(newItem.progress())
&& ... equals ...
&& ... equals ...
&& ... equals ...
...
}
}
易得 diff 办法皆存在学习本钱和运用本钱,当搭档写多了感觉厌烦,便自动回归原始,架构意图前功尽弃。
故我们只好另辟蹊径,探求少有人走的路,
改善版别 2:运用 Sealed Class 分流
因为不强迫开发者仅运用 “照应式编程”,故无缝隙需添补,乃至无需通过 data class 聚合 UiStates、也无线程安全问题,乃至无需 Actions 和 reduce,
每个页面可以简单通过 Intent 来包含入参和成果的传递,loading、error 等 Action 可以通过单独的 Intent 来反映,如此将 MVI 中最繁琐的 Action 规划拍平:
sealed class MainIntent {
data class Loading(var progress: Boolean) : MainIntent()
data class Info(var title: String) : MainIntent()
...
}
class Model : Jetpack-ViewModel() {
private val _states = MutableLiveData<MainIntent>()
val states = _states.asLiveData()
fun request(intent: Intent){
when(intent){
is Intent.XXX -> {
_states.setValue(MainIntent.Loading(true))
_states.setValue(MainIntent.Info(DataRepository.getInfo()))
_states.setValue(MainIntent.Loading(false))
}
}
}
}
class View-Controller : Android-Activity() {
private val model : Model
private val holder : StateHolder
fun onCreate(){
model.states.observe(this){
when(it){
is MainIntent.Loading -> holder.progress = it.progress
is MainIntent.Info -> holder.tvTitle = it.title
...
}
}
}
}
可是 BehaviorSubject 天然不合适连续发送消息的场景,
例如息屏(页面生命周期脱离 STARTED)期间所获消息,BehaviorSubject 仅存留最后一个,那么分流规划下,亮屏后(页面生命周期重回 STARTED)多种类消息只会推送最后一个,其余皆丢掉(比如 Loading、Success、Error 等数据,毕竟只照应 Error),
故改用 PublishSubject,比如 SharedFlow 来处理。
改善版别 3:运用 SharedFlow 回推成果
SharedFlow 内有一行列,如欲亮屏后自动推送多种类消息,则可将 replay 次数设置为与行列长度一起,例如 10,
class Model : Jetpack-ViewModel() {
private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
replay = DEFAULT_QUEUE_LENGTH
)
}
val sharedFlow = _sharedFlow.asSharedFlow()
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
因为 replay 会重走设定次数中行列的元素,故重走 STARTED 时会重走全部,包括已消费和未消费过,视觉上给人感觉即,控件上旧数据 “一闪而过”,
这体验并不好,
改善版别 4:通过计数避免重复回推
故此处可加个判别 —— 如已消费,则下次 replay 时不消费。
class Model : class Model : Jetpack-ViewModel() {
private var observerCount = 0
private val _sharedFlow: MutableSharedFlow<ViewStates>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = DEFAULT_QUEUE_LENGTH,
replay = DEFAULT_QUEUE_LENGTH
)
}
val sharedFlow = _sharedFlow.asSharedFlow()
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
data class ConsumeOnceValue<E>(
var consumeCount: Int = 0,
val value: E
)
class View-Controller : Android-Activity() {
private val model : Model
private val holder : StateHolder
fun onCreate(){
lifecycleScope?.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.states.collect {
if (version > currentVersion) {
if (model.consumeCount >= observerCount) return@collect
model.consumeCount++
when(it){
is MainIntent.Download -> holder.progress = it.progress
is MainIntent.Setting -> holder.btnChecked = it.btnChecked
is MainIntent.Info -> holder.tvTitle = it.title
is MainIntent.List -> holder.list = it.list
}
}
}
}
}
}
}
但每次创建一页面都需如此写一番,岂不伤心,
故可将其内聚,一致抽取至单独结构维护,
MVI-Dispatcher-KTX 应运而生,
改善版别 5:将样板逻辑内聚
如下,通过将 repeatOnLifecycle、计数比对、mutable/immutable 等样板逻辑内聚,
open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
private var observerCount = 0
private val _sharedFlow: MutableSharedFlow<ConsumeOnceValue<E>>? by lazy {
MutableSharedFlow(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
extraBufferCapacity = initQueueMaxLength(),
replay = initQueueMaxLength()
)
}
protected open fun initQueueMaxLength(): Int {
return DEFAULT_QUEUE_LENGTH
}
fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
observerCount++
activity?.lifecycle?.addObserver(this)
activity?.lifecycleScope?.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
_sharedFlow?.collect {
if (it.consumeCount >= observerCount) return@collect
it.consumeCount++
observer.invoke(it.value)
}
}
}
}
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
observerCount--
}
protected suspend fun sendResult(event: E) {
_sharedFlow?.emit(ConsumeOnceValue(value = event))
}
fun input(event: E) {
viewModelScope.launch { onHandle(event) }
}
protected open suspend fun onHandle(event: E) {}
data class ConsumeOnceValue<E>(
var consumeCount: Int = 0,
val value: E
)
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
}
}
如此开发者哪怕不熟 MVI、mutable,只需注重 “input-output” 两处即可自动完结 “单向数据流” 开发,
改善版别 6:增加 version 避免订阅回推
为了改善 “副作用”(关于 “副作用” 见上期解析),通常是传输过程中兼并 UiStates 和 UiEvents,并在照应时分隔处理,这也和 “照应式编程” 串流规划不约而同。
对此官方做法是,将 UiEvents 整合到 UiStates,界面工作 | Android Developers
笔者以为,此做法相较于 UiStates 和 UiEvents 分隔发送的利益在于,使 UiEvents 同处于 STATRED 环节照应,避免手写遗失乃至引发 “弹窗无法获取 token” 等状况,
缺陷是,需求手动 filterNot 屏蔽已消费工作,增加学习本钱且埋下手写的一起性危险。
故笔者采用的是另一种办法 —— 将 UiState 整合到 UiEvent,照应时再将 UiState 和 UiEvent 解离。也即我们可以选用 PublishSubject 来做查询者,并在查询者回调中,单独对 UiState 采用 BehaviorSubject(比如 ObservableField)的办法来告诉控件照应和烘托。
故此处可再加个 verison 比对,
open class MviDispatcherKTX<E> : ViewModel(), DefaultLifecycleObserver {
private var version = START_VERSION
private var currentVersion = START_VERSION
private var observerCount = 0
...
fun output(activity: AppCompatActivity?, observer: (E) -> Unit) {
currentVersion = version
observerCount++
activity?.lifecycle?.addObserver(this)
activity?.lifecycleScope?.launch {
activity.repeatOnLifecycle(Lifecycle.State.STARTED) {
_sharedFlow?.collect {
if (version > currentVersion) {
if (it.consumeCount >= observerCount) return@collect
it.consumeCount++
observer.invoke(it.value)
}
}
}
}
}
protected suspend fun sendResult(event: E) {
version++
_sharedFlow?.emit(ConsumeOnceValue(value = event))
}
companion object {
private const val DEFAULT_QUEUE_LENGTH = 10
private const val START_VERSION = -1
}
}
如此即可从根源上消除 “照应式编程” 的缝隙,且不管团队成员是否了解 “照应式编程”,都可快速安稳迭代,不繁殖不可预期问题。
class MainPageActivity : Android_Activity(){
private val model : MainPageModel
private val views : MainPageViews
fun onOutput(){
model.output(this){ intent ->
when(intent){
MainIntent.Progress -> views.progress.set(intent.progress)
MainIntent.Weather -> views.weatherInfo.set(intent.weather)
MainIntent.Error -> showErrorDialog()
}
}
model.input(Intent.GetWeather(BEI_JING))
}
class MainPageViews : Jetpack_ViewModel() {
val progress = ObservableBoolean(false)
val weatherInfo = ObservableField<String>("")
...
}
}
与此同时该方式只是改善消息分发环节,结束仍然清晰差异有 State 和 Event,最大极限统筹学习本钱、功用和安稳。
注:SharedFlow 仅限于 Kotlin 项目,如 Java 项目也想用,可参阅 MVI-Dispatcher 规划,其内部维护一行列,通过依据 LiveData 改造的 Mutable-Result 亦满意完结上述功用。
综上
理论模型皆旨在特定环境下处理特定问题,直用于生产环境或存在不可预期问题,故我们不断检验、沟通和更新。
感谢脚踏实地检验反应沟通的小伙伴,让 MVI-Dispatcher 系结构得以演化至今。
Github:MVI-Dispatcher
Github:MVI-Dispatcher-KTX