一、概念
MVC | Activity类过于臃肿 |
MVP | Presenter不仅要操作数据还要更新View |
MVVM | |
MVI |
二、MVC(Model-View-Controller)
三、MVP(Model-View-Presenter)
四、MVVM(Model-View-ViewModel)
五、MVI(Model-View-Intent)
模型 Model | 负责处理数据的状态和逻辑。 |
视图 View | 负责展示界面和数据的状态。 |
意图 Intent | 代表用户的操作(点击、输入、获取列表数据等)。 |
状态 State | 反应数据当前值。 |
5.1 唯一可信数据源
为了解决 MVVM 中 UI 订阅多个分散的 State(ViewModel中的LiveData/Flow)导致各种数据并行更新或数据相互依赖时,无法清晰掌握整个页面的状态。MVI使用 UiState 将所有 State 整合在一处,UI刷新只依赖这一个数据源。
5.2 数据单向流动
DataBinding 数据模型和视图一方发生变化就会同步到另一方,数据的流动是双向的,这样不便于追踪测试。MVI强调数据的源头只有一个,目的地也只有一个。数据从 Data Layer 流向 UI Layer 的 ViewModel 中,ViewModel 将数据转换成 State 传输给 Element 更新。
5.3 搭建项目(Compose版)
5.3.1 定义状态 State
- 命名采用:功能+UiState。
- 使用 data class 因为自带 copy() 功能,非常方便更新部分属性。界面刷新用到的状态全都定义成属性,集中在一起实现唯一可信数据源。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
data class NewsUiState(
val isLoading: Boolean = false,
val success: List<HotNewestTopBean.HotNewestTop> = emptyList(),
val fail: String = ""
)
5.3.2 定义事件 Event
- 命名采用:场景+Event。
- 使用可能的用户行为全部定义成子类。
- 子类不带参数就定义成 object 方便复用(因为创建的实例无状态区别)。
- 一般同 ViewModel 写在同一个 .kt 文件中,也可以单独写在一个 .kt 文件中。
sealed interface NewsEvent {
//条目点击
data class NewsItemClick(val url: String) : NewsEvent
//初始化数据
object InitData: NewsEvent
}
5.3.3 处理意图→更新状态 ViewModel
- Compose 向外部发送 Intent 属于副作用,因此是在协程中发送,跨协程通信选择 Channel 因为它并发安全(默认模式 RENDEZVOUS)。
class DemoViewModel(
private val repository: NewsRepository
) : ViewModel() {
//暴露给UI订阅State
var newsUiState by mutableStateOf(NewsUiState())
private set
//定义发送Event的Channel
private val eventChannel = Channel<NewsEvent>(Channel.UNLIMITED)
//初始化时就启动Event处理
init { handleEvent() }
//处理Event
private fun handleEvent() {
viewModelScope.launch {
eventChannel.consumeEach { newsEvent ->
when(newsEvent) {
is NewsEvent.NewsItemClick -> getNews()
NewsEvent.InitData -> getNews()
}
}
}
}
//暴露给UI发送Event(比直接在UI中获取Channel发送方便)
fun sendEvent(event: NewsEvent) {
viewModelScope.launch {
eventChannel.send(event)
}
}
//(在业务代码里)更新State
private suspend fun getNews() {
newsUiState = newsUiState.copy(isLoading = true) //状态设为加载中
runCatching {
repository.getNewsData()
}.onSuccess { response ->
response.getData().onSuccess {
newsUiState = newsUiState.copy(success = it) //状态设为成功
}.onFailure {//网络的错误
newsUiState = newsUiState.copy(fail = it.message.toString()) //状态设为失败
}
}.onFailure { //协程的错误
newsUiState = newsUiState.copy(fail = it.message.toString()) //状态设为失败
}
}
override fun onCleared() {
super.onCleared()
eventChannel.close() //释放资源
}
}
5.3.4 处理状态→发送意图 UI
Activity {
}
5.4 搭建项目(View版)
定义状态和意图同上。
5.4.1 处理意图→更新状态 ViewModel
ViewModel {
//将UiState暴露出去
val newsUiState = mutableStateOf<NewsUiState>(NewsUiState.Init)
//处理Intent(去调用业务代码)
fun dispatch(intent: NewsIntent) {
viewModelScope.launch {
when(intent) {
is NewsIntent.NewsItemClick -> getNews()
}
}
}
//(在业务代码里)更新State
private suspend fun getNews() {
newsUiState.value = NewsUiState.Loading //状态设为加载中
runCatching {
repository.getNewsData()
}.onSuccess { response ->
response.getData().onSuccess {
newsUiState.value = NewsUiState.Success(it) //状态设为成功
}.onFailure { //API错误
newsUiState.value = NewsUiState.Fail(it.message.toString()) //状态设为失败
}
}.onFailure { //协程错误
newsUiState.value = NewsUiState.Fail(it.message.toString()) //状态设为失败
}
}
}
5.4.2 处理状态→发送意图 UI
Activity {
onCreate {
initView()
observeUiState()
}
private fun initView() {
binding.btn.setOnClickListener {
viewModel.dispatch(NewsIntent.RefreshNewsList)
}
}
private fun observeUiState() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
val state = viewModel.newsUiState.value
when(state ) {
NewsUiState.Init -> TODO()
NewsUiState.Loading -> TODO()
is NewsUiState.Success -> {adapter.setData(state.list)}
is NewsUiState.Faild-> TODO()
}
}
}
}
}