Android 架构之 MVI 完全体 | 重新审阅 MVVM 之殇,PartialChange & Reduce

手机游戏开发者 2024-9-20 03:47:36 108 0 来自 中国
这是 MVI 架构的第三篇,系列文章目次如下:

  • Android 架构之 MVI 雏形 | 相应式编程 + 单向数据流 + 唯一可信数据源
  • Android 架构之 MVI 低级体 | Flow 更换 LiveData 重构数据链路
  • Android 架构之 MVI 完全体 | 重新审阅 MVVM 之殇,PartialChange & Reducer 来救济
  • Android 架构之 MVI 究极体 | 状态和变乱分道扬镳,粘性不再是题目 此中第一篇分析了 MVI 的概念,第二篇是 MVI 在项目实战中的低级应用,而这一篇将重构上篇的代码,以展示 MVI 的完全体。
MVI 架构有三大关键词:“唯一可信数据源”+“单向数据流”+“相应式编程”,以及一些关键概念,好比Intent,State。明确这些概念之后,能更轻松地阅读本文。(剧烈发起从第一篇开始阅读)
引子

在上一篇中,用 MVI 重构了“消息流”这个业务场景。本篇在此根本上进一步拓展,引入 MVI 中两个告急的概念PartialChange和Reducer。
假设“消息流”这个业务场景,用户可以触发如下举动:

  • 初始化消息流
  • 上拉加载更多消息
  • 举报某条消息
在 MVVM 中,这些举动被表达为 ViewModel 的一个方法调用。在 MVI 中被称为意图Intent,它们不再是一个方法调用,而是一个数据。通常可被如许界说:
sealed class FeedsIntent {    data class Init(val type: Int, val count: Int) : FeedsIntent()    data class More(val timestamp: Long, val count: Int) : FeedsIntent()    data class Report(val id: Long) : FeedsIntent()}如许做使得界面意图都以数据的情势流入到一个流中,利益是,可以用流的方式统一管理所故意图。更详细的讲授可以点击Android 架构之 MVI | 相应式编程 + 单向数据流 + 唯一可信数据源。
产品文档界说了全部的用户意图Intent,而计划稿界说了全部的界面状态State:
data class NewsState(    val data: List<News>, // 消息列表    val isLoading: Boolean, // 是否正在初次加载    val isLoadingMore: Boolean, // 是否正在上拉加载更多    val errorMessage: String, // 加载错误信息 toast    val reportToast: String, // 举报效果 toast) {    companion object {        // 消息流的初始状态        val initial = NewsState(            data = emptyList(),             isLoading = true,             isLoadingMore = false,             errorMessage = "",            reportToast = ""        )    }}在 MVI 中,把界面的一次展示明确为单个 State 的一次渲染。相较于 MVVM 中一个界面大概被分拆为多个 LiveData,State 这种唯一数据源低沉了复杂度,使得代码容易维护。
有了 Intent 和 State,整个界面革新的过程就形成了一条单向数据流,如下图所示:
1.png MVI 就是用“相应式编程”的方式将这条数据流中的多少 Intent 转换成唯一 State。低级的转换方式是直接将 Intent 映射成 State,详细分析可以点击怎样把业务代码越写越复杂?(二)| Flow 更换 LiveData 重构数据链路,更加 MVI。
PartialChange

理论上 Intent 是无法直接转换为 State 的。由于 Intent 只表达了用户触发的举动,而举动产生的效果才对应一个 State。更详细的说,“上拉加载更多消息”大概产生三个效果:

  • 正在加载更多消息。
  • 加载更多消息乐成。
  • 加载更多消息失败。
此中每一个效果都对应一个 State。“单向数据流”内部的数据变更详情如下:
每一个意图会产生多少个效果,每个效果对应一个界面状态。
上图看着有“许多条”数据流,但同一时间只大概有一条起作用。上图看着会在 ViewModel 内部形成各种 State,但袒露给界面的还是唯一 State。
由于所故意图产生的全部大概的效果都对应于一个唯一 State 实例,以是每个意图产生的效果只引起 State 部门字段的变革。好比 Init.Success 只会影响 NewsState.data 和 NewsState.isLoading。
在 MVI 框架中,意图 Intent 产生的效果称为部门变革PartialChange。
总结一下:


  • MVI 框架中用数据流来明确界面革新。


  • 数据流的出发点是界面发出的意图(Intent),一个意图会产生多少效果,它们称为 PartialChange,一个 PartialChange 对应一个 State 实例。


  • 数据流的止境是界面对 State 的观察而举行的一次渲染。
一连的状态

界面展示的变革是“一连的”,即界面新状态总是由上一次状态变革而来。就像连环画一样,下一帧是基于上一帧的偏移量。
这种基于老状态产生新状态的举动称为Reduce,用一个 lambda 表达便是(oldState: State) -> State。
界面发出的差别意图会天生差别的效果,每种效果都有各自的方法举行新老状态的变更。好比“上拉加载更多消息”和“举报消息”,前者在老状态的尾部追加数据,而后者是在老状态中删除数据。
基于此,Reduce 的 lambda 可作如下表达:(oldState: State, change: PartialChange) -> State,即新状态由老状态和 PartialChange 共同决定。
通常 PartialChange 被界说成密封接口,而 Reduce 界说为内部方法:
// 消息流的部门变革sealed interface FeedsPartialChange {    // 形貌怎样从老状态变革为新状态    fun reduce(oldState: NewsState): NewsState}这是 PartialChange 的抽象界说,消息流场景中,它应该有三个实现类,分别是 Init,More,Report。此中 Init 的实现如下:
sealed class Init : FeedsPartialChange {    // 在初始化消息流流场景下,老状态怎样变革成新状态    override fun reduce(oldState: NewsState): NewsState =         // 对初始化消息流能产生的全部效果分类讨论,并基于老状态拷贝构建新状态        when (this) {            Loading -> oldState.copy(isLoading = true)            is Success -> oldState.copy(                data = news,//方便地访问Success携带的数据                isLoading = false,                isLoadingMore = false,                errorMessage = ""            )            is Fail -> oldState.copy(                data = emptyList(),                isLoading = false,                isLoadingMore = false,                errorMessage = error            )    }    // 加载中    object Loading : Init()    // 加载乐成    data class Success(val news: List<News>) : Init()    // 加载失败    data class Fail(val error: String) : Init()}初始化消息流的 PartialChange 也被实现为密封的,密封产生的效果是,在编译时,其子类的全集就已经全部确定,不答应在运行时动态新增子类,且全部子类必须内聚在一个包名下。
如许做的利益是低沉界面革新的复杂度,即有限个 Intent 会产生有限个 PartialChange,且它们唯一对应一个 State。出 bug 的时间只需从三处找题目:1. Intent 是否发射? 2. 是否天生了既定的 PartialChange? 3. reduce 算法是否有题目?
将 reduce 算法界说在 PartialChange 内部,就能很方便地获取 PartialChange 携带的数据,并基于它构建新状态。
用同样的思绪,More 和 Report 的界说如下:
sealed class More : FeedsPartialChange {    override fun reduce(oldState: NewsState): NewsState = when (this) {        Loading -> oldState.copy(            isLoading = false,            isLoadingMore = true,            errorMessage = ""        )        is Success -> oldState.copy(            data = oldState.data + news,// 新数据追加在老数据后            isLoading = false,            isLoadingMore = false,            errorMessage = ""        )        is Fail -> oldState.copy(            isLoadingMore = false,            isLoading = false,            errorMessage = error        )    }    object Loading : More()    data class Success(val news: List<News>) : More()    data class Fail(val error: String) : More()}sealed class Report : FeedsPartialChange {    override fun reduce(oldState: NewsState): NewsState = when (this) {        is Success -> oldState.copy(            // 在老数据中删除举报消息            data = oldState.data.filterNot { it.id == id },            reportToast = "举报乐成"        )        Fail -> oldState.copy(reportToast = "举报失败")    }    class Success(val id: Long) : Report()    object Fail : Report()}状态的变更

Intent,PartialChange,Reduce,State 界说好了,是时间看看怎样用流的方式把它们串联起来!
总体来说,状态是如许变更的:Intent -> PartialChange -(Reduce)-> State
1. Intent 流入,State 流出

class StateFlowActivity : AppCompatActivity() {    private val newsViewModel by lazy {        ViewModelProvider(            this,            NewsViewModelFactory(NewsRepo(this))        )[NewsViewModel::class.java]    }    // 将所故意图通过 merge 举行合流    private val intents by lazy {        merge(            flowOf(FeedsIntent.Init(1, 5)),// 初始化消息            loadMoreFlow(), // 加载更多消息            reportFlow()// 举报消息        )    }    // 将上拉加载更多转换成数据流    private fun loadMoreFlow() = callbackFlow {        recyclerView.setOnLoadMoreListener {            trySend(FeedsIntent.More(111L, 2))        }        awaitClose { recyclerView.removeOnLoadMoreListener(null) }    }    // 将举报消息转换成数据流    private fun reportFlow() = callbackFlow {        reportView.setOnClickListener {            val news = newsAdapter.dataList as? News            news?.id?.let { trySend(FeedsIntent.Report(it)) }        }        awaitClose { reportView.setOnClickListener(null) }    }    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(contentView)        // 订阅意图流        intents            // Intent 流入 ViewModel            .onEach(newsViewModel::send)            .launchIn(lifecycleScope)        // 订阅状态流        newsViewModel.newState            // State 流出 ViewModel,并绘制界面            .collectIn(this) { showNews(it) }    }}class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {    // 用于吸收意图的 SharedFlow    private val _feedsIntent = MutableSharedFlow<FeedsIntent>()    // 意图被变更为状态    val newState =        _feedsIntent.map {} // 伪代码,省略了 将 Intent 变更为 State 的细节    // 将意图发送到流    fun send(intent: FeedsIntent) {        viewModelScope.launch { _feedsIntent.emit(intent) }    }}界面可以发出的所故意图都被构造到一个流中,而且摆列在一起。intents流可以作为明确业务逻辑的入口。同时 ViewModel 提供了一个 State 流,供界面订阅。
2. Intent -> PartialChange

class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {    private val _feedsIntent = MutableSharedFlow<FeedsIntent>()    // 供界面观察的唯一状态    val newState =        _feedsIntent            .toPartialChangeFlow()            .flowOn(Dispatchers.IO)            .stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial)    )}各种 Intent 转换为 PartialChange 的逻辑被封装在toPartialChangeFlow()中:
// NewsViewModel.kt// 将 Intent 流变更为 PartialChange 流private fun Flow<FeedsIntent>.toPartialChangeFlow(): Flow<FeedsPartialChange> = merge(    // 过滤出初始化消息意图并将其变更为对应的 PartialChange    filterIsInstance<FeedsIntent.Init>().flatMapConcat { it.toPartialChangeFlow() },    // 过滤出上拉加载更多意图并将其变更为对应的 PartialChange    filterIsInstance<FeedsIntent.More>().flatMapConcat { it.toPartialChangeFlow() },    // 过滤出举报消息意图并将其变更为对应的 PartialChange    filterIsInstance<FeedsIntent.Report>().flatMapConcat { it.toPartialChangeFlow() },)toPartialChangeFlow() 被界说为扩展方法。
filterIsInstance() 用于过滤出Flow<FeedsIntent>中的子范例并分类讨论,由于每种 Intent 变更为 PartialChange 的方式有所差别。
末了用 merge 举行合流,它会将每个 Flow 中的数据合起来并发地转发到一个新的流上。merge + filterIsInstance的组合相当于流中的 if-else。
此中的 toPartialChangeFlow() 是各种意图的扩展方法:
// NewsViewModel.ktprivate fun FeedsIntent.Init.toPartialChangeFlow() =    flowOf(        // 本地数据库消息        newsRepo.localNewsOneShotFlow,        // 网络消息        newsRepo.remoteNewsFlow(this.type.toString(), this.count.toString())    )        // 并发合流        .flattenMerge()        .transformWhile {            emit(it.news)            !it.abort        }        // 将消息数据变更为乐成或失败的 PartialChange        .map { news ->             if (news.isEmpty()) Init.Fail("no news") else Init.Success(news)         }        // 发射展示 Loading 的 PartialChange        .onStart { emit(Init.Loading) }该扩展方法形貌了怎样将 FeedsIntent.Init 变更为对应的 PartialChange。同样地,FeedsIntent.More 和 FeedsIntent.Report 的变更逻辑如下:
// NewsViewModel.ktprivate fun FeedsIntent.More.toPartialChangeFlow() =    newsRepo.remoteNewsFlow("news", "10")        .map {news ->             if(it.news.isEmpty()) More.Fail("no more news") else More.Success(it.news)         }        .onStart { emit(More.Loading) }        .catch { emit(More.Fail("load more failed by xxx")) }private fun FeedsIntent.Report.toPartialChangeFlow() =    newsRepo.reportNews(id)        .map { if(it >= 0L) Report.Success(it) else Report.Fail}        .catch { emit((Report.Fail)) }3. PartialChange -(Reduce)-> State

经过 toPartialChangeFlow() 的变更,现在流中活动的数据是各种范例的 PartialChange。接下来就要将其变更为 State:
// NewsViewModel.ktval newState =  _feedsIntent    .toPartialChangeFlow()    // 将 PartialChange 变更为 State    .scan(NewsState.initial){oldState, partialChange -> partialChange.reduce(oldState)}    .flowOn(Dispatchers.IO)    .stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial))使用scan()举行变更:
// 从 Flow<T> 变更为 Flow<R>public fun <T, R> Flow<T>.scan(    initial: R, // 初始值    operation: suspend (accumulator: R, value: T) -> R // 累加算法): Flow<R> = runningFold(initial, operation)public fun <T, R> Flow<T>.runningFold(    initial: R,     operation: suspend (accumulator: R, value: T) -> R): Flow<R> = flow {    // 累加器    var accumulator: R = initial    emit(accumulator)    collect { value ->        // 举行累加        accumulator = operation(accumulator, value)        // 向下游发射累加值        emit(accumulator)    }}从 scan() 的署名看,是将一个流变更为另一个流,看似和 map() 相似。但它的变更算法是带累加的。用 lambda 表达为(accumulator: R, value: T) -> R。
这不恰好就是上面提到的 Reduce 吗!即基于老状态和新 PartialChange 天生新状态。
MVVM 和 MVI 复杂度比拼

就消息流这个场景,用图来对比下 MVVM 和 MVI 复杂度的区别。
这张图表达了三种复杂度:

  • View 发起请求的复杂度:ViewModel 的各种方法调用会散落在界面差别地方。即界面向 ViewModel 发起请求没有统一入口。
  • View 观察数据的复杂度:界面需要观察多个 ViewModel 提供的数据,这导致界面状态的划一性难以维护。
  • ViewModel 内部请求和数据关系的复杂度:数据被界说为 ViewModel 的成员变量。成员变量是增长复杂度的利器,由于它可以被任何成员方法访问。也就是说,新增业务对成员变量的修改大概影响老业务的界面展示。同理,当界面展示出错时,也很难一下子定位到是哪个请求造成的。
再来看一下让人耳目一新的 MVI 吧:
完善化解上述三个没有须要的复杂度。
总之,用上 MVI 后,新需求不再粉碎老逻辑,出 bug 了能更快速定位到题目。
敬请等待

另有一个题目有待办理,那就是 MVI 框架下,革新界面时恒久性状态 State 和 一次性变乱 Event 的区别对待。
在 MVVM 中,由于 LiveData 的粘性,导致一次性变乱被界面多次斲丧。对此有多种办理方案。详情可点击LiveData 口试题库、解答、源码分析
但 MVI 的解题思绪略有差别,限于篇幅缘故原由,只能下回分析,接待连续关注~
总结


  • MVI 框架中用单向数据流来明确界面革新。整个数据流中包罗的数据依次如下:Intent,PartialChange,State
  • 数据流的出发点是界面发出的意图(Intent),一个意图会产生多少效果,它们称为 PartialChange,一个 PartialChange 对应一个 State 实例。
  • 数据流的止境是界面对 State 的观察而举行的一次渲染。
  • MVI 就是用“相应式编程”的方式将单向数据流中的多少 Intent 转换成唯一 State。
  • MVI 夸大的单向数据流体现在两个层面:

    • View 和 ViewModel 交互过程中的单向数据流:单个Intent流流入 ViewModel,单个State流流出 ViewModel。
    • ViewModel 内部数据变更的单向数据流:Intent 变更为多个 PartialChange,一个 PartialChange 对应一个 State。

Talk is cheap, show me the code

完备代码如下,也可以从这个地点克隆。
StateFlowActivity.kt

class StateFlowActivity : AppCompatActivity() {    private val newsAdapter2 by lazy {        VarietyAdapter2().apply {addProxy(NewsProxy())}    }    private val intents by lazy {        merge(            flowOf(FeedsIntent.Init(1, 5)),            loadMoreFlow(),            reportFlow()        )    }    private fun loadMoreFlow() = callbackFlow {        recyclerView.setOnLoadMoreListener {            trySend(FeedsIntent.More(111L, 2))        }        awaitClose { recyclerView.removeOnLoadMoreListener(null) }    }    private fun reportFlow() = callbackFlow {        reportView.setOnClickListener {            val news = newsAdapter.dataList as? News            news?.id?.let { trySend(FeedsIntent.Report(it)) }        }        awaitClose { reportView.setOnClickListener(null) }    }    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(contentView)        intents            .onEach(newsViewModel::send)            .launchIn(lifecycleScope)        newsViewModel.newState            .collectIn(this) { showNews(it) }    }    private fun showNews(state: NewsState) {        state.apply {            if (isLoading) showLoading() else dismissLoading()            if (isLoadingMore) showLoadingMore() else dismissLoadingMore()            if (reportToast.isNotEmpty()) Toast.makeText(                this@StateFlowActivity,                state.reportToast,                Toast.LENGTH_SHORT            ).show()            if (errorMessage.isNotEmpty()) tv.text = state.errorMessage            if (data.isNotEmpty()) newsAdapter2.dataList = state.data        }    }}NewsViewModel.kt

class NewsViewModel(private val newsRepo: NewsRepo) : ViewModel() {    private val _feedsIntent = MutableSharedFlow<FeedsIntent>()    val newState =        _feedsIntent            .toPartialChangeFlow()            .scan(NewsState.initial) { oldState, partialChange -> partialChange.reduce(oldState) }            .flowOn(Dispatchers.IO)            .stateIn(viewModelScope, SharingStarted.Eagerly,NewsState.initial)    fun send(intent: FeedsIntent) {        viewModelScope.launch { _feedsIntent.emit(intent) }    }    private fun Flow<FeedsIntent>.toPartialChangeFlow(): Flow<FeedsPartialChange> = merge(        filterIsInstance<FeedsIntent.Init>().flatMapConcat { it.toPartialChangeFlow() },        filterIsInstance<FeedsIntent.More>().flatMapConcat { it.toPartialChangeFlow() },        filterIsInstance<FeedsIntent.Report>().flatMapConcat { it.toPartialChangeFlow() },    )    private fun FeedsIntent.More.toPartialChangeFlow() =        newsRepo.remoteNewsFlow("", "10")            .map { if (it.news.isEmpty()) More.Fail("no more news") else More.Success(it.news) }            .onStart { emit(More.Loading) }            .catch { emit(More.Fail("load more failed by xxx")) }    private fun FeedsIntent.Init.toPartialChangeFlow() =        flowOf(            newsRepo.localNewsOneShotFlow,            newsRepo.remoteNewsFlow(this.type.toString(), this.count.toString())        )            .flattenMerge()            .transformWhile {                emit(it.news)                !it.abort            }            .map { news -> if (news.isEmpty()) Init.Fail("no more news") else Init.Success(news) }            .onStart { emit(Init.Loading) }            .catch {                if (it is SSLHandshakeException)                    emit(Init.Fail("network error,show old news"))            }    private fun FeedsIntent.Report.toPartialChangeFlow() =        newsRepo.reportNews(id)            .map { if(it >= 0L) Report.Success(it) else Report.Fail}            .catch { emit((Report.Fail)) }}NewsState.kt

data class NewsState(    val data: List<News> = emptyList(),    val isLoading: Boolean = false,    val isLoadingMore: Boolean = false,    val errorMessage: String = "",    val reportToast: String = "",) {    companion object {        val initial = NewsState(isLoading = true)    }}FeedsPartialChange.kt

sealed interface FeedsPartialChange {    fun reduce(oldState: NewsState): NewsState}sealed class Init : FeedsPartialChange {    override fun reduce(oldState: NewsState): NewsState = when (this) {        Loading -> oldState.copy(isLoading = true)        is Success -> oldState.copy(            data = news,            isLoading = false,            isLoadingMore = false,            errorMessage = ""        )        is Fail -> oldState.copy(            data = emptyList(),            isLoading = false,            isLoadingMore = false,            errorMessage = error        )    }    object Loading : Init()    data class Success(val news: List<News>) : Init()    data class Fail(val error: String) : Init()}sealed class More : FeedsPartialChange {    override fun reduce(oldState: NewsState): NewsState = when (this) {        Loading -> oldState.copy(            isLoading = false,            isLoadingMore = true,            errorMessage = ""        )        is Success -> oldState.copy(            data = oldState.data + news,            isLoading = false,            isLoadingMore = false,            errorMessage = ""        )        is Fail -> oldState.copy(            isLoadingMore = false,            isLoading = false,            errorMessage = error        )    }    object Loading : More()    data class Success(val news: List<News>) : More()    data class Fail(val error: String) : More()}sealed class Report : FeedsPartialChange {    override fun reduce(oldState: NewsState): NewsState = when (this) {        is Success -> oldState.copy(            data = oldState.data.filterNot { it.id == id },            reportToast = "举报乐成"        )        Fail -> oldState.copy(reportToast = "举报失败")    }    class Success(val id: Long) : Report()    object Fail : Report()}保举阅读

Kotlin 异步 | Flow 限流的应用场景及原理
Kotlin 异步 | Flow 应用场景及原理
怎样把业务代码越写越复杂? | MVP - MVVM - Clean Architecture
怎样把业务代码越写越复杂?(二)| Flow 更换 LiveData 重构数据链路,更加 MVI
Android 架构之 MVI | 相应式编程 + 单向数据流 + 唯一可信数据源
作者:唐子玄
链接:https://juejin.cn/post/7108498411149590558
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-11-21 20:43, Processed in 0.194954 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表