Android 架构之 MVI 完全体 | 重新审视 MVVM 之殇,PartialChange & Reducer 来拯救

这是 MVI 架构的第三篇,系列文章目录如下:

  1. Android 架构之 MVI 雏形 | 呼应式编程 + 单向数据流 + 仅有可信数据源

  2. Android 架构之 MV源码交易平台I 初级体 | Flow 替换 LiveData 重构数据链路

  3. Android 架构之 MVI 完整体 | 从头审视 MVVM 之殇,PartialChange & Reducer 来解救

  4. Android 架构之 MVI 究极源码1688体 | 状况和事件各奔前程,粘性不再源码编辑器下载是问题

其间第一篇剖析了 MVI 的概念,第二篇是 MVI 在项目实战中的初级使用,而这一变量的定义篇将重构上篇的代码,以展现 MVI 的完整体。

MVI 架构有三大要害词:“仅有可信数据源”+“单架构师和程序员的区别向数据流”+“呼应式编程”,以及一些要害概念,比方Intent,State。了解这些概念架构师工资之后,能更轻松地阅览本文。(强烈建议从第一篇开端阅览源码之家

引子

在上一篇中,用 MVI 重构了“新闻流”这个事务场景。本篇在此基础上进一步拓展,引入 MVI 中两个重要的概念PartialChakotlin什么意思n架构geReducer

假设“新闻流”这个事务场景,用户能够触发如下行为:

  1. 初始化新闻流
  2. 上拉加载更多新闻
  3. 告发某条新闻

在 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 | 呼应式编程 + 单kotlin和java向数据流 + 仅有可信数据变量英语源。

产品文档界说了一切的用户目的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,整个界面改写的过程源码时代就形成了一条单向数据流,如下图所示:

Android 架构之 MVI 完整体 | 从头审视 MVVM 之殇,PartialChange & Reducer 来解救

MVI 便是用“呼应式架构是什么意思编程”的办法将这条数据流中的若干 Intent 转化成仅有 State。初级的转化办法是直接将 Intent 映射成算法的五个特性 State,具体剖析能够点击怎么把事务代码越写越杂乱?(二)| Flow 替换 LiveData 重构数据链路,愈加 MVI。

PartialChange

理论上 Intent 是无法直接转化为 State 的。由于 Intent 只表达了用户触发的行为,而行为发生的成果变量的定义才对应一个架构图怎么画 S源码网站tate。更具体的说,“上拉加载更多新闻”或许发生三个成果:

  1. 正在加载更多新闻。
  2. 加载更多新闻成功。
  3. 加载算法的时间复杂度取决于更多新闻失利。

其间每一个成果都对应一个 State。“单向数据流”内部的数据改换详情如下:kotlin为什么流行不起来

Android 架构之 MVI 完整体 | 从头审视 MVVM 之殇,PartialChange & Reducer 来解救

每一个目的会发生若干个成果,每个成果对应一个界面状况。

上图看着有“很多条”数据流,但同一时间只或许有一条起作用。上图看着会在 ViewModel 内部形成各种 State,但暴露给界面的还是仅有 State。

由于一切目的发生的一变量是什么意思切或许的成果都对应于一个仅有 State 实例,所以每个目的发生的成果只引起 State 部分字段的算法导论改变。比方 Init.Success 只会影响 NewsState.data 和 NewsState.isLoading。

在 MVI 结构中,目的 Intent 发生的成果称为部分改变PartialChange

总结一下:

  • MVI 结构中用数据流来了解界面改写。

  • 数据流的起点是界面宣布的目的(Intent),一个目的会发生若干成果,它们称为 Par架构图怎么画tialChange,一个 PartialChange 对应一个 State 实例。

  • 数据流的结尾是界面对 State 的调查而进行的一次烘托。

接连kotlin怎么读的状源码交易平台

界面展现的改变是“接连的”,即界面新状况总是由上一次状况改变而来。就像连环画一样,下一帧是根据上一帧的偏移量。

这种根据老状况发生新状况的行为称为Reduce,用一个 lambda 表达便是(oldState: State) -> State

界面宣布的不同目的会生成不同的成果,每种成果都有各自的办法进行新老状况的改换。比方“上拉加载更多源码网站新闻”和“告发新闻”,前者在老状况的尾部追加数据,而后者是在老状况中删去数据。

根据此,Reduce 的 lambda 可作如下表达:(oldState: S源码1688tate, cha变量nge: PartialChange) -> State,即新状况由老状况和 Part算法的五个特性ialChange 共同决定。

一般 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()
}

初始化新闻流的 Pakotlin是什么rtialChange 也被完成为密封的,密封发生的效果是,在编译时,其子类的全集就现已悉数确定,不允许在运行时动态新增子类,且一切子类必须内聚在一个包名下。

这样做的好处是下降界面改写的杂乱度,即有限个 Intent 会发生有限个 PartialCha架构师工资nge,且它们仅有对应一个 State。出 bug 的时分只需从三处找问题:1. Intent 是否发射? 2. 是否生成了既定的 PartialChange? 3. redu源码网站ce 算法是否有问题?

将 reduce 算法界说在 PartialCh架构师证书ange 内部,就能很方便地获取 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,Part算法的有穷性是指ialChange,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[i] 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 提供kotlin是什么了一个 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 改换为 Partial源码时代Change 的办法有所不同。

最后用 merge 进行合流,它会将每个 Flow源码编辑器下载 中的数据合起来并发地转发到一Kotlin个新的流上。merge + filterIsInstance的组合相当于流中的 if-else。

其间的 toPartialCh变量值angeFlow() 是各种目的的扩展办法:

// NewsViewModel.kt
private 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.Rep架构图怎么制作ort 的改换逻辑如下:

// NewsViewModel.kt
private 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() 的改换,现在流中活动的数据是各源码编辑器下载种类型的 PartialChan源码编程器ge。接下来就要将其改换为 State:

// NewsViewModel.kt
val 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 和 M变量值VI 杂乱度比拼

就新闻流这个场架构师和程序员的区别景,用图来对比下 MVVM 和 MVI 杂乱度的差异。

Android 架构之 MVI 完整体 | 从头审视 MVVM 之殇,PartialChange & Reducer 来解救

这张图表达了三种杂乱度:

  1. View 发变量起恳求架构图模板的杂乱度:Vkotlin和javaiewModel 的各种办法调用会散落在界面不同地方。即界面向 ViewModel 发起恳求没有一致进口。
  2. View 调查数据的杂乱度:界面需求调查多个 ViewModel 提供的数据,这导致界面状况的一致性难以保护。
  3. ViewModel 内部恳求和数据关系的杂乱度:数据被界说为 ViewModel 的成员变量。成员变量是增加杂乱度的利器,由算法是指什么于它能够被任何成员办法访问。也便是说,新增事务对成员变量的修改或许影响老事务的界面展现。架构师工资同理,当界面展现出错时,也很难一下子定位架构师证书到是哪个恳求造成的。架构师工资

再来看一下让人耳目一新的 MVI 吧:

Android 架构之 MVI 完整体 | 从头审视 MVVM 之殇,PartialChange & Reducer 来解救
完美化解上述三个没有必要的杂乱度。

总归,用上 MVI 后,新需求不再破坏老逻辑,出 bug 了能更快速定位到问题。

敬请期待

还有一个问题有待解决,那便是 MVI 结构下,改写界面时持久性状况 State 和 一次性事件 Event 的差异对kotlin和java待。

在 MVVM 中,由于 LiveData 的粘性,导致一次性事件被界面多次消费。对源码中的图片此有多种解决方案。详情可架构师点击LiveData 面试题库、回答、源码剖析

但 MVI 的解题思路略有不同,限于篇幅原因,只能下回剖析,欢迎持续关注~

总结

  • MVI 结构中用单向数据流来了解界面改写。整个数据流中包括的数据依次如下:Intent,PartialChange,State

  • 数据流的起点是界面宣布的目的(Intent),一个目的会发生若干成果,它们称为 PartialChange,一个 PartialChange 对应一个 State 实例。

  • 数据流的结尾是界面对 State 的调查而进行的一次烘托。

  • MVI 便是用“呼应式编程”的办法将单向数据流中的若干 Intent 转化成仅有 State。

  • MVI 强调的单向数据流表现在两个层面:

    1. View 和 ViewModel 交互过程中的单向数据流:单个Intent流流入 ViewModel,单个State流流出 ViewModel。
    2. ViewModel 内部数据改换的单向数据流:Intent 改换为多个 PartialChange,一个 PartialCh算法是指什么ange 对应一个 S变量之间的关系tate。

Talk is cheap, show me the code

完整代码如下,算法导论也能够从这个地址克隆。

StateFlowActivitykotlin匿名函数.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[i] 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 限流的使用场景及原理

Kot源码编辑器lin 异步 | Flow 使用场景及原理

怎么把事务代码越写越杂乱? | MVP – MVVM – Clean Architecture

怎么把事务代码越写越杂乱?(二)| Flow 替换 LiveDa算法的空间复杂度是指ta 重构数据链路,愈加 MVI

Android 架构之 MVI | 呼应式编程 + 单向算法数据流 + 仅有可信数据源

发表评论

提供最优质的资源集合

立即查看 了解详情