乐趣区

关于android:MVVM-成为历史Google-全面倒向-MVI

前言

前段时间写了一些介绍 MVI 架构的文章,不过软件开发上没有最好的架构,只有最合适的架构,同时家喻户晓,Google举荐的是 MVVM 架构。置信很多人都会有疑难,我为什么不应用官网举荐的 MVVM,而要用你说的这个什么MVI 架构呢?

不过我这几天查看 Android 的利用架构指南,发现谷歌举荐的最佳实际曾经变成了 单向数据流动 + 状态集中管理 ,这不就是MVI 架构吗?看起来 Google 曾经开始举荐应用 MVI 架构了,大家也有必要开始理解一下 Android 利用架构指南的最新版本了~

总体架构

两个架构准则

Android的架构设计准则次要有两个

拆散关注点

要遵循的最重要的准则是拆散关注点。一种常见的谬误是在一个 ActivityFragment 中编写所有代码。这些基于界面的类应仅蕴含解决界面和操作系统交互的逻辑。总得来说,ActivityFragment 中的代码应该尽量精简,尽量将业务逻辑迁徙到其它层

通过数据驱动界面

另一个重要准则是您应该通过数据驱动界面(最好是持久性模型)。数据模型独立于利用中的界面元素和其余组件。
这意味着它们与界面和利用组件的生命周期没有关联,但仍会在操作系统决定从内存中移除利用的过程时被销毁。
数据模型与界面元素,生命周期解耦,因而不便复用,同时便于测试,更加稳固牢靠。

举荐的利用架构

基于上一部分提到的常见架构准则,每个利用应至多有两个层:

  • 界面层 – 在屏幕上显示利用数据。
  • 数据层 – 提供所须要的利用数据。

您能够额定增加一个名为“网域层”的架构层,以简化和复用应用界面层与数据层之间的交互

如上所示,各层之间的依赖关系是单向依赖的,网域层,数据层不依赖于界面层

界面层

界面的作用是在屏幕上显示利用数据,并响应用户的点击。每当数据发生变化时,无论是因为用户互动(例如按了某个按钮),还是因为内部输出(例如网络响应),界面都应随之更新,以反映这些变动。
不过,从数据层获取的利用数据的格局通常不同于 UI 须要展现的数据的格局,因而咱们须要将数据层数据转化为页面的状态
因而界面层个别分为两局部,即 UI 层与 State HolderState Holder 的角色个别由 ViewModel 承当

数据层的作用是存储和治理利用数据,以及提供对利用数据的拜访权限,因而界面层必须执行以下步骤:

  1. 获取利用数据,并将其转换为 UI 能够轻松出现的UI State
  2. 订阅UI State,当页面状态产生扭转时刷新UI
  3. 接管用户的输出事件,并依据相应的事件进行解决,从而刷新UI State
  4. 依据须要反复第 1-3 步。

次要是一个单向数据流动,如下图所示:

因而界面层次要须要做以下工作:

  1. 如何定义UI State
  2. 如何应用单向数据流 (UDF),作为提供和治理 UI State 的形式。
  3. 如何裸露与更新UI State
  4. 如何订阅UI State

如何定义UI State

如果咱们要实现一个新闻列表界面,咱们该怎么定义 UI State 呢? 咱们将界面须要的所有状态都封装在一个 data class 中。
与之前的 MVVM 模式的次要区别之一也在这里,即之前通常是一个 State 对应一个 LiveData,而MVI 架构则强调对 UI State 的集中管理

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf())

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    ...
)

以上示例中的 UI State 定义是不可变的。这样的次要益处是,不可变对象可保障即时提供利用的状态。这样一来,UI便可专一于施展繁多作用:读取 UI State 并相应地更新其 UI 元素。因而,切勿间接在 UI 中批改UI State。违反这个准则会导致同一条信息有多个可信起源,从而导致数据不统一的问题。

例如,如上中来自 UI StateNewsItemUiState对象中的 bookmarked 标记在 Activity 类中已更新,那么该标记会与数据层开展竞争,从而产生多数据源的问题。

UI State集中管理的优缺点

MVVM 中咱们通常是多个数据流,即一个 State 对应一个 LiveData,而MVI 中则是单个数据流。两者各有什么优缺点?
单个数据流的长处次要在于不便,缩小模板代码,增加一个状态只须要给 data class 增加一个属性即可,能够无效地升高 ViewModelView的通信老本
同时 UI State 集中管理能够轻松地实现相似 MediatorLiveData 的成果,比方可能只有在用户已登录并且是付费新闻服务订阅者时,您才须要显示书签按钮。您能够按如下形式定义UI State

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf()){val canBookmarkNews: Boolean get() = isSignedIn && isPremium
}

如上所示,书签的可见性是其它两个属性的派生属性,其它两个属性发生变化时,canBookmarkNews也会主动变动,当咱们须要实现书签的可见与暗藏逻辑,只须要订阅 canBookmarkNews 即可,这样能够轻松实现相似 MediatorLiveData 的成果,然而远比 MediatorLiveData 要简略

当然,UI State集中管理也会有一些问题:

  • 不相干的数据类型:UI所需的某些状态可能是齐全互相独立的。在此类情况下,将这些不同的状态捆绑在一起的代价可能会超过其劣势,尤其是当其中某个状态的更新频率高于其余状态的更新频率时。
  • UiState diffingUiState 对象中的字段越多,数据流就越有可能因为其中一个字段被更新而收回。因为视图没有 diffing 机制来理解间断收回的数据流是否雷同,因而每次收回都会导致视图更新。当然,咱们能够对 LiveDataFlow 应用 distinctUntilChanged() 等办法来实现部分刷新,从而解决这个问题

应用单向数据流治理UI State

上文提到,为了保障 UI 中不能批改状态,UI State中的元素都是不可变的,那么如何更新 UI State 呢?
咱们个别应用 ViewModel 作为 UI State 的容器,因而响应用户输出更新 UI State 次要分为以下几步:

  1. ViewModel 会存储并公开 UI StateUI State 是通过 ViewModel 转换的利用数据。
  2. UI层会向 ViewModel 发送用户事件告诉。
  3. ViewModel会解决用户操作并更新UI State
  4. 更新后的状态将反馈给 UI 以进行出现。
  5. 零碎会对导致状态更改的所有事件反复上述操作。

举个例子,如果用户须要给新闻列表加个书签,那么就须要将事件传递给 ViewModel,而后ViewModel 更新 UI State(两头可能有数据层的更新),UI 层订阅 UI State 订响应刷新,从而实现页面刷新,如下图所示:

为什么应用单向数据流动?

单向数据流动能够实现关注点拆散准则,它能够将状态变动起源地位、转换地位以及最终应用地位进行拆散。
这种拆散可让 UI 只施展其名称所表明的作用:通过观察 UI State 变动来显示页面信息,并将用户输出传递给 ViewModel 以实现状态刷新。

换句话说,单向数据流动有助于实现以下几点:

  1. 数据一致性。界面只有一个可信起源。
  2. 可测试性。状态起源是独立的,因而可独立于界面进行测试。
  3. 可维护性。状态的更改遵循明确定义的模式,即状态更改是用户事件及其数据拉取起源独特作用的后果。

裸露与更新UI State

定义好 UI State 并确定如何治理相应状态后,下一步是将提供的状态发送给界面。咱们能够应用 LiveData 或者 StateFlowUI State转化为数据流并裸露给 UI
为了保障不能在 UI 中批改状态,咱们应该定义一个可变的 StateFlow 与一个不可变的StateFlow,如下所示:

class NewsViewModel(...) : ViewModel() {private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

这样一来,UI层能够订阅状态,而 ViewModel 也能够批改状态, 以须要执行异步操作的状况为例,能够应用 viewModelScope 启动协程,并且能够在操作实现时更新状态。

class NewsViewModel(
    private val repository: NewsRepository,
    ...
) : ViewModel() {private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {val newsItems = repository.newsItemsForCategory(category)
                _uiState.update {it.copy(newsItems = newsItems)
                }
            } catch (ioe: IOException) {
                // Handle the error and notify the notify the UI when appropriate.
                _uiState.update {val messages = getMessagesFromThrowable(ioe)
                    it.copy(userMessages = messages)
                 }
            }
        }
    }
}

在下面的示例中,NewsViewModel 类会尝试进行网络申请,而后更新 UI State,而后UI 层能够对其做出适当反馈

订阅UI State

订阅 UI State 很简略,只须要在 UI 层察看并刷新 UI 即可

class NewsActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {
        lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {// Update UI elements}
            }
        }
    }
}

UI State实现部分刷新

因为 MVI 架构下实现了 UI State 的集中管理,因而更新一个属性就会导致 UI State 的更新,那么在这种状况下怎么实现部分刷新呢?
咱们能够利用 distinctUntilChanged 实现,distinctUntilChanged只有在值发生变化了之后才会回调刷新,相当于对属性做了一个防抖,因而咱们能够实现部分刷新,应用形式如下所示

class NewsActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {
        lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map {it.isFetchingArticles}
                    .distinctUntilChanged()
                    .collect {progressBar.isVisible = it}
            }
        }
    }
}

当然咱们也能够对其进行肯定的封装,给 Flow 或者 LiveData 增加一个扩大函数,令其反对监听属性即可,应用形式如下所示

class MainActivity : AppCompatActivity() {private fun initViewModel() {
        viewModel.viewStates.run {
            // 监听 newsList
            observeState(this@MainActivity, MainViewState::newsList) {newsRvAdapter.submitList(it)
            }
            // 监听网络状态
            observeState(this@MainActivity, MainViewState::fetchStatus) {//..}
        }
    }
}

对于 MVI 架构下反对属性监听,更加具体地内容可见:MVI 架构更佳实际:反对 LiveData 属性监听

网域层

网域层是位于界面层和数据层之间的可选层。

网域层负责封装简单的业务逻辑,或者由多个 ViewModel 重复使用的简略业务逻辑。此层是可选的,因为并非所有利用都有这类需要。因而,您应仅在须要时应用该层。
网域层具备以下劣势:

  1. 防止代码反复。
  2. 改善应用网域层类的类的可读性。
  3. 改善利用的可测试性。
  4. 让您可能划分好职责,从而避免出现大型类。

我感觉对于常见的 APP,网域层仿佛并没有必要,对于ViewModel 反复的逻辑,应用 util 来说个别就已足够
或者网域层实用于特地大型的我的项目吧,各位可依据本人的需要选用,对于网域层的详细信息可见:https://developer.android.com…

数据层

数据层次要负责获取与解决数据的逻辑,数据层由多个 Repository 组成,其中每个 Repository 可蕴含零到多个 Data Source。您应该为利用解决的每种不同类型的数据创立一个Repository 类。例如,您能够为与电影相干的数据创立 MoviesRepository 类,或者为与付款相干的数据创立 PaymentsRepository 类。当然为了不便,针对只有一个数据源的Repository,也能够将数据源的代码也写在Repository,后续有多个数据源时再做拆分

数据层跟之前的 MVVM 架构下的数据层并没用什么区别,这里就不多介绍了,对于数据层的详细信息可见:https://developer.android.com…

总结

相比老版的架构指南,新版次要是减少了网域层并批改了界面层,其中网域层是可选的,各位各依据本人的我的项目需要应用。
而界面层则从 MVVM 架构变成了 MVI 架构,强调了数据的 单向数据流动 状态的集中管理 。相比MVVM 架构,MVI架构次要有以下长处

  1. 强调数据单向流动,很容易对状态变动进行跟踪和回溯,在数据一致性,可测试性,可维护性上都有肯定劣势
  2. 强调对 UI State 的集中管理,只须要订阅一个 ViewState 便可获取页面的所有状态,绝对 MVVM 缩小了不少模板代码
  3. 增加状态只须要增加一个属性,升高了 ViewModelView层的通信老本,将业务逻辑集中在 ViewModel 中,View层只须要订阅状态而后刷新即可

当然在软件开发中没有最好的架构,只有最合适的架构,各位可依据状况选用适宜我的项目的架构,实际上在我看来 Google 在指南中举荐应用 MVI 而不再是 MVVM,很可能是为了对立AndroidCompose的架构。因为在 Compose 中并没有双向数据绑定,只有单向数据流动,因而 MVI 是最适宜 Compose 的架构。

当然如果你的我的项目中没有应用 DataBinding,或者也能够开始尝试一下应用MVI,不应用DataBindingMVVM架构切换为 MVI 老本不高,切换起来也比较简单,在易用性,数据一致性,可测试性,可维护性等方面都有肯定劣势,后续也能够无缝切换到Compose

如果看完本文对你有播种,请动动你发财的小手,点点赞,你的点赞是我最大的能源。

相干学习视频举荐:

【2021 最新版】Android studio 装置教程 +Android(安卓)零基础教程视频(适宜 Android 0 根底,Android 初学入门)含音视频_哔哩哔哩_bilibili

Android 架构设计原理与实战——Jetpack 联合 MVP 组合利用开发一个优良的 APP!_哔哩哔哩_bilibili

Android 进阶必学:jetpack 架构组件—Navigation_哔哩哔哩_bilibili

Android 进阶零碎学习——Jetpack 先天优良的基因能够防止数据内存透露_哔哩哔哩_bilibili

退出移动版