首先,谷歌官方似乎并没有把自己主张的运用架构命名为 MVVM 或 MVI, MVVM 和 MVI是开发者依据不同时期官方运用架构攻略的特色,达成的一个一致称谓。
关于学习这两个种架构,咱们需求自己去了解官方运用架构攻略,不然只能生搬硬套的运用别人了解的 MVVM 和 MVI。
首先看看现在最新的官方运用架构攻略,是怎么主张咱们建立运用架构的。
一,官方运用架构攻略
1,架构的准则
运用架构界说了运用的各个部分之间的边界以及每个部分应承担的责任。谷歌主张依照以下准则规划运用架构。
1,分离关注点
2,通过数据模型驱动界面
3,单一数据源
4,单向数据流
2,谷歌引荐的运用架构
每个运用应至少有两个层:
- 界面层 – 在屏幕上显现运用数据。
- 数据层 – 包括运用的事务逻辑并揭露运用数据。
能够额定增加一个名为“网域层”的架构层,以简化和重复运用界面层与数据层之间的交互。
总结下来,咱们的运用架构应该有三层:界面层、网域层、数据层。
其间网域层可选,即不管你的运用中有没有网域层,与你的运用架构是 MVVM 仍是 MVI 无关。
个人的了解是:界面层、网域层、数据层应该是运用等级的,而不是页面等级的。
假如我以为分层是页面等级的,那么我在接到一个事务需求 A 时(一般一个事务会新建一个页面activity 或 fragment 来接受),我的完结思路是:
- 新建页面 A_Activity
- 新建状况容器 A_ViewModel
- 新建数据层 A_Repository
这样的话,数据层 A_Repository 和 界面层:界面元素 A_Activity + 状况容器 A_ViewModel 强相关,数据层无复用性可言。
假如我以为分层是运用等级的,那么在接到事务A 时,完结思路是:
- 新建页面 A_Activity
- 新建状况容器 A_ViewModel
- 首先从运用的数据层寻觅事务 A 需求运用的 事务数据 是否有对应的存储库房 Respository,假如有,则复用(A_ViewModel 中依赖 Repository),拿到事务数据后,转成 UI 数据;假如无,则创立 这种事务数据 的存储库房 Repository,后续其他界面层假如页运用到这种事务数据,能够直接复用这种事务数据对应的 Repository。
2.1,界面层架构规划辅导
界面层在架构中的效果
界面的效果是在屏幕上显现运用数据,并充当主要的用户互动点。
从数据层获取是事务数据,有时候需求界面层将事务数据转换成 UI 数据供界面元素显现。
界面层的组成
界面层由以下两部分组成:
-
界面元素:在屏幕上呈现数据的界面元素能够运用 View 或Jetpack Compose 函数完结。
-
状况容器:用于存储数据、向界面供给数据以及处理逻辑的状况容器(如ViewModel类)。
界面层的架构规划遵从的准则
这儿以一个常见的列表页面为案例进行解说,这个列表页面有以下交互:
- 打开页面时,网络数据回来之前展现一个加载中 view。
- 初次打卡页面,假如没有数据或许网络恳求产生过错,展现一个过错 view。
- 具有下拉改写才能,改写后,假如有数据,则替换列表数据;假如无回来数据,则弹出一个 Toast。
接着咱们用这个事务,依照以下准则进行分析:
1, 界说界面状况
界面元素 加上 界面状况 才是用户看到的界面。
上面说的列表页面,依据它的事务需求,需求有以下界面状况
- 展现加载中 view 的界面状况
- 展现加载过错 view 的界面状况
- 列表数据 view 界面状况
- Toast view 界面状况
- 改写完结 view 界面状况
不管选用 MVVM 仍是 MVI,都需求这些界面状况,仅仅他们的完结细节不同,详细能够看下面的解说。
2,界说状况容器
状况容器:便是存放咱们界说的界面状况,而且包括履行相应任务所必需的逻辑的类。
ViewModel类型是引荐的状况容器,用于管理屏幕级界面状况,具有数据层访问权限。但并不是只能用 ViewModel作为状况容器。
不管选用 MVVM 仍是 MVI,都需求界说状况容器,来存放界面状况。
3,运用单向数据流管理状况
看看官方在界面层的架构辅导图:
界面状况数据活动是单向的,只能从 状况容器 到 界面元素。
界面产生的事情 events(如改写、加载更多等事情)活动是单向的,只能从 界面元素 到 状况容器。
不管选用 MVVM 仍是 MVI,都需求运用单向数据流管理状况。
4,仅有数据源
仅有数据源针对的是:界说的界面状况 和 界面产生的事情。
界面状况仅有数据源指的是将界说的多个界面状况,封装在一个类中,如上面的列表事务,不选用仅有数据源,界面状况的声明为:
/**
* 加载失利 UI 状况,显现失利图
* 首屏获取的数据为空、首屏恳求数据失利时展现失利图
* 初始值:躲藏
*/
val loadingError: StateFlow<Boolean>
get() = _loadingError
private val _loadingError = MutableStateFlow<Boolean>(false)
/**
* 正在加载 UI 状况,显现加载中图
* 首屏时恳求网络时展现加载中图
* 初始值:展现
*/
val isLoading: StateFlow<Boolean>
get() = _isLoading
private val _isLoading = MutableStateFlow<Boolean>(true)
/**
* 加载成功后回来的列表 UI 状况,将 list 数据展现到列表上
*/
val newsList: StateFlow<MutableList<News>>
get() = _newsList
private val _newsList = MutableStateFlow<MutableList<News>>(mutableListOf())
/**
* 加载完结 UI 状况
*/
val loadingFinish: StateFlow<Boolean>
get() = _loadingFinish
private val _loadingFinish = MutableStateFlow<Boolean>(false)
/**
* 界面 toast UI 状况
*/
val toastMessage: StateFlow<String>
get() = _toastMessage
private val _toastMessage = MutableStateFlow<String>("")
选用仅有数据源声明界面状况时,代码如下:
sealed interface NewsUiState {
object IsLoading: NewsUiState
object LoadingError: NewsUiState
object LoadingFinish: NewsUiState
data class Success(val newsList: MutableList<News>): NewsUiState
data class ToastMessage(val message: String = ""): NewsUiState
}
val newsUiState: StateFlow<NewsUiState>
get() = _newsUiState
private val _newsUiState: MutableStateFlow<NewsUiState> =
MutableStateFlow(NewsUiState.IsLoading)
界面产生的事情的仅有数据源指的是将界面产生的事情封装在一个类中,然后一致处理。比如上面描述的列表事务,它的界面事情有 初始化列表事情(首屏恳求网络数据)、改写事情、加载更多事情。
不选用仅有数据源,界面事情的调用完结逻辑为:在 activity 中直接调用 viewModel 供给的 initData、freshData 和 loadMoreData 办法;
选用仅有数据源,界面事情的调用完结逻辑为,先将事情中封装在一个 Intent 中,viewModel 中供给一个一致的事情入口处理办法 dispatchIntent,在 activity 中 各个场景下都调用 viewModel#dispatchIntent,代码如下:
sealed interface NewsActivityIntent {
data class InitDataIntent(val type: String = "init") : NewsActivityIntent
data class RefreshDataIntent(val type: String = "refresh") : NewsActivityIntent
data class LoadMoreDataIntent(val type: String = "loadMore") : NewsActivityIntent
}
fun dispatchIntent(intent: NewsActivityIntent) {
when (intent) {
is NewsActivityIntent.InitDataIntent -> {
//初始化逻辑
initNewsData()
}
is NewsActivityIntent.RefreshDataIntent -> {
//改写逻辑
refreshNewsData()
}
is NewsActivityIntent.LoadMoreDataIntent -> {
//加载更多逻辑
loadMoreNewsData()
}
}
}
由于有了仅有数据源这一特色,才将最新的运用架构称为 MVI,MVVM 不具有这一特色。
5,向界面揭露界面状况的方法
在状况容器中界说界面状况后,下一步考虑的是怎么将供给的状况发送给界面。
谷歌引荐运用 LiveData或StateFlow等可调查数据容器中揭露界面状况。这样做的长处有:
- 解耦界面元素(activity 或 fragment) 与 状况容器,如:activity 持有 viewModel 的引证,viewModel 不需求持有 activity 的引证。
不管选用 MVVM 仍是 MVI,都需求向界面揭露界面状况,揭露的方法也能够是相同的。
6,运用界面状况
在界面中运用界面状况时,关于LiveData,能够运用observe()办法;关于 Kotlin 数据流,您能够运用collect()办法或其变体。
留意:在界面中运用可调查数据容器时,需求考虑界面的生命周期。由于当未向用户显现视图时,界面不该调查界面状况。运用LiveData时,LifecycleOwner会隐式处理生命周期问题。运用数据流时,最好通过适当的协程效果域和repeatOnLifecycleAPI,如:
class NewsActivity : AppCompatActivity() {
private val viewModel: NewsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
不管选用 MVVM 仍是 MVI,都需求运用界面状况,运用的方法都是相同的。
2.2,数据层架构规划辅导
数据层在架构中的效果
数据层包括运用数据和事务逻辑。事务逻辑决议运用的价值,它由现实世界的事务规矩组成,这些规矩决议着运用数据的创立、存储和更改方法。
数据层的架构规划
数据层由多个库房组成,其间每个库房都能够包括零到多个数据源。您应该为运用中处理的每种不同类型的数据分别创立一个存储库类。例如,您能够为与电影相关的数据创立一个MoviesRepository类,或许为与付款相关的数据创立一个PaymentsRepository类。
每个数据源类应仅担任处理一个数据源,数据源能够是文件、网络来源或本地数据库。
层次结构中的其他层不能直接访问数据源;数据层的入口点始终是存储库类。
揭露 API
数据层中的类通常会揭露函数,以履行一次性的创立、读取、更新和删除 (CRUD) 调用,或接纳关于数据随时刻改变的告诉。关于每种情况,数据层都应揭露以下内容:
-
一次性操作:在 Kotlin 中,数据层应揭露挂起函数;关于 Java 编程言语,数据层应揭露用于供给回调来告诉操作结果的函数。
-
接纳关于数据随时刻改变的告诉:在 Kotlin 中,数据层应揭露数据流,关于 Java 编程言语,数据层应揭露用于发出新数据的回调。
class ExampleRepository(
private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
private val exampleLocalDataSource: ExampleLocalDataSource // database
) {
val data: Flow<Example> = ...
suspend fun modifyData(example: Example) { ... }
}
多层存储库
在某些触及更杂乱事务要求的情况下,存储库可能需求依赖于其他存储库。这可能是由于所触及的数据是来自多个数据源的数据聚合,或许是由于相应责任需求封装在其他存储库类中。
例如,担任处理用户身份验证数据的存储库UserRepository能够依赖于其他存储库(例如LoginRepository和 RegistrationRepository,以满足其要求。
留意:传统上,一些开发者将依赖于其他存储库类的存储库类称为 manager,例如称为UserManager而非UserRepository。
数据层生命周期
假如该类的责任效果于运用至关重要,能够将该类的实例的效果域限定为Application类。
假如只需求在运用内的特定流程(例如注册流程或登录流程)中重复运用同一实例,则应将该实例的效果域限定为担任相应流程的生命周期的类。例如,能够将包括内存中数据的RegistrationRepository的效果域限定为RegistrationActivity。
数据层定位考虑
数据层不该该是页面等级的(一个页面对应一个数据层),而应该是运用等级的(数据层有多个存储库房,每种数据类型有一个对应的存储库房,不同的界面层能够复用存储库房)。
比如我做的运用是运动健康app,用户的睡觉相关的数据有一个 SleepResposity,用户体重相关的数据有一个 WeightReposity,由于运用中很多界面都可能需求展现用户的睡觉数据和体重数据,所以 SleepResposity 和 WeightReposity 能够供不同界面层运用。
二,MVVM
1,MVVM 架构图
2,MVVM 完结一个详细事务
运用上面说到的列表页面事务,依照 MVVM 架构完结如下:
2.1,界面层的完结
界面层完结时,需求遵从以下几点。
1,挑选完结界面的元素
界面元素能够用 view 或 compose 来完结,这儿用 view 完结。
2,供给一个状况容器
这儿运用 ViewModel 作为状况容器;状况容器用来存放界面状况变量;ViewModel 是官方引荐的状况容器,而不是必须运用它作为状况容器。
3,界说界面状况
这个需求中咱们依据事务描述,界说出多个界面状况。
/**
* 加载失利 UI 状况,显现失利图
* 首屏获取的数据为空、首屏恳求数据失利时展现失利图
* 初始值:躲藏
*/
val loadingError: StateFlow<Boolean>
get() = _loadingError
private val _loadingError = MutableStateFlow<Boolean>(false)
/**
* 正在加载 UI 状况,显现加载中图
* 首屏时恳求网络时展现加载中图
* 初始值:展现
*/
val isLoading: StateFlow<Boolean>
get() = _isLoading
private val _isLoading = MutableStateFlow<Boolean>(true)
/**
* 加载成功后回来的列表 UI 状况,将 list 数据展现到列表上
*/
val newsList: StateFlow<MutableList<News>>
get() = _newsList
private val _newsList = MutableStateFlow<MutableList<News>>(mutableListOf())
/**
* 加载完结 UI 状况
*/
val loadingFinish: StateFlow<Boolean>
get() = _loadingFinish
private val _loadingFinish = MutableStateFlow<Boolean>(false)
/**
* 界面 toast UI 状况
*/
val toastMessage: StateFlow<String>
get() = _toastMessage
private val _toastMessage = MutableStateFlow<String>("")
4,揭露界面状况
这儿挑选数据流 StateFlow 揭露界面状况。当然也能够挑选 LiveData 揭露界面状况。
5,运用/订阅界面状况
我这儿运用的是数据流 StateFlow 揭露的界面状况,所以在界面层相对应的运用 flow#collect 订阅界面状况。
6,数据模型驱动界面
结合上面几点,界面层的完结代码为:
界面元素的完结:
class NewsActivity: ComponentActivity() {
private var mBinding: ActivityNewsBinding? = null
private var mAdapter: NewsListAdapter? = null
private val mViewModel = NewsViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityNewsBinding.inflate(layoutInflater)
setContentView(mBinding?.root)
initView()
initObserver()
initData()
}
private fun initView() {
mBinding?.listView?.layoutManager = LinearLayoutManager(this)
mAdapter = NewsListAdapter()
mBinding?.listView?.adapter = mAdapter
mBinding?.refreshView?.setOnRefreshListener {
mViewModel.refreshNewsData()
}
}
private fun initData() {
mViewModel.getNewsData()
}
private fun initObserver() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
mViewModel.isLoading.collect {
if (it) {
mBinding?.loadingView?.visibility = View.VISIBLE
} else {
mBinding?.loadingView?.visibility = View.GONE
}
}
}
launch {
mViewModel.loadingError.collect {
if (it) {
mBinding?.loadingError?.visibility = View.VISIBLE
} else {
mBinding?.loadingError?.visibility = View.GONE
}
}
}
launch {
mViewModel.loadingFinish.collect {
if (it) {
mBinding?.refreshView?.isRefreshing = false
}
}
}
launch {
mViewModel.toastMessage.collect {
if (it.isNotEmpty()) {
showToast(it)
}
}
}
launch {
mViewModel.newsList.collect {
if (it.isNotEmpty()) {
mBinding?.loadingError?.visibility = View.GONE
mBinding?.loadingView?.visibility = View.GONE
mBinding?.refreshView?.visibility = View.VISIBLE
mAdapter?.setData(it)
}
}
}
}
}
}
}
状况容器的完结:
class NewsViewModel : ViewModel() {
private val repository = NewsRepository()
/**
* 加载失利 UI 状况,显现失利图
* 首屏获取的数据为空、首屏恳求数据失利时展现失利图
* 初始值:躲藏
*/
val loadingError: StateFlow<Boolean>
get() = _loadingError
private val _loadingError = MutableStateFlow<Boolean>(false)
/**
* 正在加载 UI 状况,显现加载中图
* 首屏时恳求网络时展现加载中图
* 初始值:展现
*/
val isLoading: StateFlow<Boolean>
get() = _isLoading
private val _isLoading = MutableStateFlow<Boolean>(true)
/**
* 加载成功后回来的列表 UI 状况,将 list 数据展现到列表上
*/
val newsList: StateFlow<MutableList<News>>
get() = _newsList
private val _newsList = MutableStateFlow<MutableList<News>>(mutableListOf())
/**
* 加载完结 UI 状况
*/
val loadingFinish: StateFlow<Boolean>
get() = _loadingFinish
private val _loadingFinish = MutableStateFlow<Boolean>(false)
/**
* 界面 toast UI 状况
*/
val toastMessage: StateFlow<String>
get() = _toastMessage
private val _toastMessage = MutableStateFlow<String>("")
fun getNewsData() {
viewModelScope.launch(Dispatchers.IO) {
val list = repository.getNewsList()
if (list.isNullOrEmpty()) {
_loadingError.emit(true)
} else {
_newsList.emit(list)
}
}
}
fun refreshNewsData() {
viewModelScope.launch(Dispatchers.IO) {
val list = repository.getNewsList()
_loadingFinish.emit(true)
if (list.isNullOrEmpty()) {
_toastMessage.emit("暂时没有更新数据")
} else {
_newsList.emit(list)
}
}
}
}
2.2,数据层的完结
这儿的数据层只有一个新闻列表数据结构的存储库房 NewsRepository,另外获取新闻信息属于一次性操作,依据数据层架构规划,直接运用 suspend 就好。
class NewsRepository {
suspend fun getNewsList(): MutableList<News>? {
delay(2000)
val list = mutableListOf<News>()
val news = News("标题", "描述信息")
list.add(news)
list.add(news)
list.add(news)
list.add(news)
return list
}
}
个人的一些了解:
1, 数据层不该该是界面等级的,而应该是运用等级的
数据层不该该是界面等级的,即一个页面对应一个 Repository;数据层应该是运用等级的,即一个运用有一个或多个数据层,每个数据层中有多个存储库房 Respository,存储库房能够在不同的界面层复用。
之前我一向以为,一个页面对应一个数据层,一个页面对应一个 Repository。但后来发现这种了解不太对。上面的例子中 NewsViewModel 只用到 NewsRepository,是由于这个新闻列表事务中只用到新闻列表数据这种数据,假如列表中还能够点赞 那咱们就需求新建一种点赞存储库房 LikeRepository,来处理点赞数据,这时 NewsViewModel 与 Repository 的联系是这样:
class NewsViewModel : ViewModel() {
private val newsRepository = NewsRepository()
private val likeRepository = LikeRepository()
}
数据层供给的 新闻列表数据处理才能 NewsRepository 和 点赞数据处理才能 LikeRepository,应该是运用界别的,能够供不同的界面复用。
2,数据层应该是“不变的”
这儿的不变不是说数据层的事务逻辑不变,而是指不管是 MVP、MVVM 仍是 MVI,他们应该能够共用数据层。
2.3,网域层的完结
网域层是可选的,是否具有网域层,跟架构是否为 MVVM 无关,这个案例中不适用网域层。
三,MVI
1,MVI 架构图
2,MVI 完结一个详细事务
同样运用上面 MVVM 完结的新闻事务。依照 MVI 架构完结如下:
2.1,界面层的完结
除了和 MVVM 遵从以下几点相同准则之外:
1,挑选完结界面的元素
2,供给一个状况容器
3,界说界面状况
4,揭露界面状况
5,运用/订阅界面状况
6,单向数据流
MVI 还需求遵从准则:
1,单一数据源
所以 MVI 需求:1,把界面状况聚合起来;2,把界面事情聚合起来。
综合上面的准则,选用 MVI 完结界面的完结如下:
界面元素、聚合界面状况、聚合界面事情 代码:
sealed interface NewsUiState {
object IsLoading: NewsUiState
object LoadingError: NewsUiState
object LoadingFinish: NewsUiState
data class Success(val newsList: MutableList<News>): NewsUiState
data class ToastMessage(val message: String = ""): NewsUiState
}
sealed interface NewsActivityIntent {
data class InitDataIntent(val type: String = "init") : NewsActivityIntent
data class RefreshDataIntent(val type: String = "refresh") : NewsActivityIntent
data class LoadMoreDataIntent(val type: String = "loadMore") : NewsActivityIntent
}
class NewsActivity: ComponentActivity() {
private var mBinding: ActivityNewsBinding? = null
private var mAdapter: NewsListAdapter? = null
private val mViewModel = NewsViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityNewsBinding.inflate(layoutInflater)
setContentView(mBinding?.root)
initView()
initObserver()
initData()
}
private fun initView() {
mBinding?.listView?.layoutManager = LinearLayoutManager(this)
mAdapter = NewsListAdapter()
mBinding?.listView?.adapter = mAdapter
mBinding?.refreshView?.setOnRefreshListener {
mViewModel.dispatchIntent(NewsActivityIntent.RefreshDataIntent())
}
}
private fun initData() {
mViewModel.dispatchIntent(NewsActivityIntent.InitDataIntent())
}
private fun loadMoreData() {
mViewModel.dispatchIntent(NewsActivityIntent.LoadMoreDataIntent())
}
private fun initObserver() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
mViewModel.newsUiState.collect {
//更新UI
}
}
}
}
}
}
状况容器代码:
class NewsViewModel : ViewModel() {
private val repository = NewsRepository()
val newsUiState: StateFlow<NewsUiState>
get() = _newsUiState
private val _newsUiState: MutableStateFlow<NewsUiState> =
MutableStateFlow(NewsUiState.IsLoading)
fun dispatchIntent(intent: NewsActivityIntent) {
when (intent) {
is NewsActivityIntent.InitDataIntent -> {
//初始化逻辑
initNewsData()
}
is NewsActivityIntent.RefreshDataIntent -> {
//改写逻辑
refreshNewsData()
}
is NewsActivityIntent.LoadMoreDataIntent -> {
//加载更多逻辑
loadMoreNewsData()
}
}
}
/**
* 初始化
*/
private fun initNewsData() {
viewModelScope.launch(Dispatchers.IO) {
val list = repository.getNewsList()
if (list.isNullOrEmpty()) {
_newsUiState.emit(NewsUiState.LoadingError)
} else {
_newsUiState.emit(NewsUiState.Success(list))
}
}
}
/**
* 改写
*/
private fun refreshNewsData() {
viewModelScope.launch(Dispatchers.IO) {
val list = repository.getNewsList()
_newsUiState.emit(NewsUiState.LoadingFinish)
if (list.isNullOrEmpty()) {
_newsUiState.emit(NewsUiState.ToastMessage("暂时没有新数据"))
} else {
_newsUiState.emit(NewsUiState.Success(list))
}
}
}
/**
* 没有完结
*/
private fun loadMoreNewsData() {
}
}
2.2,数据层与网域层的完结
界面层:参阅上面 MVVM 的数据层介绍,不管 MVP、MVVM、MVI,不同运用架构的数据层应该是不变的,即通用。
网域层:运用架构是否具有网域层不影响它是什么类型的架构,这儿的列表事务没有网域层。