Android 开发架构模式之MVPS

9 阅读19分钟

这篇文章要解决的问题是“MVPS是什么?”

本文的写作背景:我要参加的新项目中,使用了MVPS架构模式。在此之前,我没有做过Android UI开发,也很少使用Kotlin语言,同时完全不了解MVPS是怎么回事。因此我带着这个问题,和豆包展开对话,针对豆包给出的答案中不理解的概念和原理进行追问,最终弄懂了MVPS是怎么回事。我把学习到的内容梳理成文,以便以后遗忘时可以快速回忆。如果对屏幕前的你有所帮助,那就更好了~

本文的主要内容:从问题出发,以点带面,通过一个简化的MVPS示例代码,在逐行阅读代码的过程中,学习到了MVPS的原理、架构、用法,kotlin的实现,以及其中常用的一些工具类例如state、flow、协程、suspend函数等。

now let‘s start


我们先来看一些概括性的概念,如果感到这一段有些抽象,没关系,可以先看示例代码和后面的代码解析,再回来看这里,就容易理解了。

MVPS基本概念

首先,MVPS (Model-View-Presenter-State)是 Android 开发架构模式中的一种。

MVPS 通过 单向数据流(View → Presenter → State → View)实现严格的状态管理。

我对MVPS各个模块的之间的交互关系总结如下:View监听到用户操作,调用Presenter进行处理,Presenter驱动Model拿到数据,Presenter更新state,View自动感知到State变化并更新显示。 打个比方,View是瞭望台上放哨和往外发信号的,Presenter是指挥官,Model是一线小工,State是一块黑板,Presenter往黑板上写好字View马上自动往外发。

MVPS 架构的核心组件

Model: 负责数据处理(如网络请求、数据库操作)

View: 通常是 Activity/Fragment,负责 UI 展示和用户交互(如按钮点击、输入框输入),并将用户操作传递给 Presenter 处理。它订阅 State 的变化并更新 UI。

Presenter: 作为 View 和 Model 的桥梁,处理业务逻辑。它接收 View 的用户操作,调用 Model 处理数据,并根据数据更新 State。

State: 是一个不可变的数据容器,保存 UI 的所有状态(如加载中、成功、错误等)。Presenter 通过更新 State 来通知 View 刷新(实际上是用StateFlow实现实时监控的响应式编程)。

MVPS示例代码

程序员的世界,代码比文字要更直观,我们来看一个简化的MVPS示例。

// 1. State 层:保存 UI 所有状态
data class UserState(
    val isLoading: Boolean = false,     // 是否正在加载数据
    val user: User? = null,             // 用户数据(可为空)
    val error: String? = null           // 错误信息(可为空)
)
​
// 2. Model 层:数据获取与处理
interface UserRepository {
    suspend fun fetchUser(userId: String): User // 挂起函数:在协程中异步获取用户数据
}
​
// 3. Presenter 层:处理业务逻辑(不继承ViewModel)
class UserPresenter(
    private val repository: UserRepository,
    private val coroutineScope: CoroutineScope // XXX 注入协程作用域
) {
    private val _state = MutableStateFlow(UserState())
    val state: StateFlow<UserState> = _state
​
    // 获取用户数据的方法
    fun loadUser(userId: String) {
        coroutineScope.launch(Dispatchers.IO) {  // 使用注入的作用域启动协程 // 指定IO调度器
            _state.update { it.copy(isLoading = true) }
            try {
                val user = repository.fetchUser(userId)
                withContext(Dispatchers.Main) {  // XXX 切换回主线程更新状态
                    _state.update {
                        it.copy(
                            isLoading = false,
                            user = user,
                            error = null
                        )
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    _state.update {
                        it.copy(
                            isLoading = false,
                            error = e.message ?: "获取用户数据失败"  // XXX 空安全处理
                        )
                    }
                }
            }
        }
    }
}
​
// 4. View 层(Activity)
class UserActivity : AppCompatActivity() {
    private val presenter by lazy {
        UserPresenter(
            repository = UserRepositoryImpl(),
            coroutineScope = lifecycleScope // XXX 使用Activity的生命周期作用域
        )
    }
​
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
​
        // 订阅 State 变化
        lifecycleScope.launchWhenStarted {
            presenter.state.collect { state ->
                updateUI(state)
            }
        }
​
        // 触发加载
        presenter.loadUser("123")
    }
​
    private fun updateUI(state: UserState) {
        // 根据 State 更新 UI
        progressBar.visibility = if (state.isLoading) View.VISIBLE else View.GONE
        if (state.user != null) {
            userTextView.text = state.user.name
        }
        if (state.error != null) {
            errorTextView.text = state.error
        }
    }
}

state

总结:

1、state类中所有成员定义成val

2、更新状态时,通过 data class 自带的 copy() 方法创建新对象

开局就是一个State,这个对象在后续其他类中都有大量访问,我们先看看它是个啥。答:UserState是一个自定义的数据类对象,用来存放状态数据

// 1. State 定义
data class UserState(
    val isLoading: Boolean = false,
    val user: User? = null,
    val error: String? = null
)

data class 是 Kotlin 中一种特殊的类,专门用于存储数据。

可以注意到UserState中的所有成员都是val(即不可变数据),这可不是随便写的,实际上不论是数据类的设计初衷、不可变性原则、还是状态管理的最佳实践,都要求这样做,这样做有以下好处:

  • 对象状态在创建后不可变,避免因意外修改导致的数据不一致。
  • 天然支持线程安全(无需额外同步机制)。
  • 可追溯性:每次状态变更都生成新对象,便于追踪状态变化历史。
  • 适配响应式编程:与 Flow、LiveData 等响应式框架无缝配合,确保状态变更触发 UI 刷新。

我们当前的这个场景(state/ Flow/响应式编程)尤其适合这个设计思路,总之你记得 【state类中所有成员定义成val】 就对了。

既然val成员不可变,那么更新状态的时候怎么办呢?答案如下——

当需要更新状态时,通过 data class 自带的 copy() 方法创建新对象,而非修改旧对象:

// 正确做法:通过 copy() 创建新状态
val oldState = UserState(isLoading = true)
val newState = oldState.copy(isLoading = false, user = loadedUser)
​
// 错误做法(若使用 var):直接修改状态可能导致逻辑混乱
oldState.isLoading = false  // 隐藏的状态变更难以追踪

tips:顺便说一下,data class的copy方法得到的对象newState中的成员,如果参数设置了就用新值,没设置的会用oldState中的值~所以参数中只填写要变化的成员即可,不变的不用写。

现在你一定能看懂示例中的这段代码了吧~这就是在更新状态呀

_state.value = _state.value.copy(
    isLoading = false,
    user = user,
    error = null
)

Flow

接下来迎面而来的代码是:

class UserPresenter(...) {
    private val _state = MutableStateFlow(UserState())
    val state: StateFlow<UserState> = _state

这是在干嘛?MutableStateFlow是个啥东西?它为什么要以UserState为参数?_state和state是什么关系?StateFlow又是啥?

如果你也一头雾水,说明你需要了解一下Flow的概念。Flow在MVPS架构中太关键了,如果不了解它的原理,代码肯定是看不懂的。你可以根据自己的疑问逐级查询下面的概念,目录能够帮助你导航。

Kotlin Flow

上面的例子中使用 Kotlin Flow 实现数据和状态的管理。

核心操作符

  • flow { ... }:创建 Flow。
  • emit(value):发送数据项
  • delay(time):暂停协程执行(非阻塞)。
  • collect { ... }:收集数据。

Flow 属于Kotlin协程库(kotlinx-coroutines-core)的一部分,需添加依赖:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'

使用Kotlin Flow 无需额外依赖第三方库,只要项目引入了 Kotlin 协程即可使用。是现代 Kotlin 项目(尤其是 Android 开发)处理异步数据的首选方案。

如果不用Kotlin Flow,还可以使用LiveData(Jetpack 组件)、RxJava(跨平台响应式库)、回调机制(传统方案)。但要么需要引入第三方库,要么用起来复杂。现代 Android 项目,推荐优先使用Kotlin Flow。我们这里也不多做展开,仅研究使用Flow的情况。

热流和冷流

可以比喻为水龙头的两种模式:

1、冷数据流:按需放水的水龙头

只有当 “收集者”(杯子)开始接水时,数据才会产生。就像水龙头只有在被打开时才放水,关闭时停止。例如:

  • 数据库查询:只有当程序执行查询语句时,才会从数据库获取数据(冷流按需生成)。
  • 文件读取:只有当程序调用 “读取文件” 方法时,才会逐行读取数据(数据生成依赖收集动作)。

2、热数据流:一直放水的水龙头。

无论是否有 “收集者”(杯子),数据都会持续产生。就像水龙头一直开着,即使没人接水,水也会流走(数据会被丢弃或保留最新值)。例如:

  • 实时天气数据:不管有没有人查看,气象局都会持续更新数据(热流持续产生)。
  • 直播视频流:不管有没有观众,摄像头都在持续录制(数据持续生成)。

代码示例:

Tips:使用 flow { ... } 创建的是冷流,使用MutableStateFlowStateFlow创建的是热流。还有其他创建方式这里不提了

1、冷流:调用 collect 触发数据生成

val coldFlow = flow {
    println("开始生成数据")
    emit(1)
    delay(1000)
    emit(2)
}
​
// 第一次收集:重新执行上游代码
coldFlow.collect { println(it) }  // 输出: 开始生成数据、1、2// 第二次收集:再次执行上游代码
coldFlow.collect { println(it) }  // 再次输出: 开始生成数据、1、2

2、热流:无论是否 collect,数据都会生成,hotFlow.value的值发生变化时,各个hotFlow.collect{}中的代码就会被触发执行

val hotFlow = MutableStateFlow(0)  // 初始值为0// 收集者1
lifecycleScope.launch {
    hotFlow.collect { println("收集者1: $it") }
}
​
// 更新数据(无需collect)
hotFlow.value = 1  // 收集者1会收到1// 新收集者2
lifecycleScope.launch {
    hotFlow.collect { println("收集者2: $it") }  // 立即收到最新值1
}

tips:emit 是冷流(Flow)专用的操作符,用于在流中发送数据项。热流(如 StateFlow)不使用 emit,而是通过 value 属性直接更新状态。

MutableStateFlow(UserState())

MutableStateFlow 是Kolin协程库提供的典型 热流(Hot Flow) 实现

MutableStateFlow 是一个泛型类,可以传递任意类型的数据,例如:


// 整型
val intFlow = MutableStateFlow(0)
intFlow.value = 100
​
// 字符串
val stringFlow = MutableStateFlow("初始值")
stringFlow.value = "新文本"
​
// 自定义数据类
data class User(val name: String, val age: Int)
val userFlow = MutableStateFlow(User("张三", 25))
userFlow.value = User("李四", 30)
​
// 集合类型
val listFlow = MutableStateFlow(listOf<String>())
listFlow.value = listOf("A", "B", "C")

好了,现在我们知道了,MVPS-UserPresenter中下面一行代码的含义就是:创建一个热流对象_state,其中管理的是UserState类型的状态数据,每当状态值更新 的时候(即Presenter调用_state.update)


// 3. Presenter 层(业务逻辑)
class UserPresenter(
  ...
  private val _state = MutableStateFlow(UserState())

就会自动触发View中以下presenter.state.collect{}中的代码,从而实现ui显示效果随着状态变化自动更新~


// 4. View 层(Activity)
class UserActivity : AppCompatActivity() {
    ...
    presenter.state.collect { state ->
            updateUI(state)
        }

collect

StateFlow.collect() 是一个 suspend 函数,必须在协程作用域中调用。它的工作原理是:

  • 协程启动后,collect 会挂起当前协程,不会阻塞线程
  • StateFlow 的值发生变化时,协程恢复执行,执行 collect 内的代码块
  • 协程取消时(如 Activity 销毁),collect 自动停止收集

_state.update

最后再来熟悉一下状态更新的语法

_state.update { it.copy(isLoading = true) }

推荐**update + copy 组合使用**,这是一种标准组合用法,常用于 安全更新状态update 确保原子性,copy 负责生成新对象。标准用法示例:

// 点击加载按钮时更新状态
button.setOnClickListener {
    _state.update { state ->
        state.copy(isLoading = true) // 仅修改 isLoading 属性
    }
}

updateMutableStateFlow 提供的方法,update 保证状态值的修改是原子操作,避免多线程冲突。接收一个 Lambda 表达式,参数为当前状态值,返回新状态值。

copy 是 Kotlin 数据类(data class) 的内置方法,负责创建新对象。

copy 也可独立使用,以下两种写法基本是等价的,但仅用copy无法保证原子性,多线程环境下可能导致竞态条件(两次赋值覆盖)。

// 原始写法
_state.value = _state.value.copy(isLoading = true)
​
// 等价于 update 方法
_state.update { currentState ->
    currentState.copy(isLoading = true)
}

一对StateFlow实现单向数据流

代码里能看到有两个state成员,区别是一个私有可变,一个公开不可变。这体现了单向数据流的设计原则。

class UserPresenter(...) {
    private val _state = MutableStateFlow(UserState()) // 可变状态流:用于内部更新状态
    val state: StateFlow<UserState> = _state  // 不可变状态流:外部只能观察,不能修改

内部使用 MutableStateFlow:在 ViewModel 或业务逻辑层管理和修改状态。

外部暴露 StateFlow:在 View 层或接口中提供只读访问,确保状态的单向流动(Present→ View)

特性MutableStateFlowStateFlow
可变性可变(Mutable)只读(Read-only)
修改方式通过 valueupdate 直接修改状态无法直接修改,只能观察状态变化
适用角色作为状态的 生产者(通常在 ViewModel 内部)作为状态的 消费者(暴露给 View 层)

tips:StateFlow创建的也是热流

tips:私有前缀:在 Kotlin 代码中,使用 _ 作为私有属性前缀(如 _uiState)是一种常见的命名约定,其核心目的是明确区分内部可变状态与外部只读接口,并强化单向数据流的设计原则。

协程

示例代码中出现了很多带有Scope、launch、suspend等的关键字,它们是协程相关的概念,我们来看看它们的含义以及用法。

协程是 Kotlin 中轻量级 “线程”,基于挂起函数实现非阻塞编程,比传统线程更省资源。 常用方法:

  • suspend:标记挂起函数,可暂停恢复。
  • launch:启动协程,返回 Job。
  • async:启动协程并返回 Deferred 获取结果。
  • withContext:切换协程执行线程。
  • 作用域如viewModelScopelifecycleScope管理生命周期。

注入coroutineScope的原理

CoroutineScope 是 Kotlin 协程中的核心接口,用于管理协程的生命周期。

在 Kotlin 协程的最佳实践中,推荐大多数使用协程的类通过外部注入CoroutineScope。我们的示例代码就是这样做的。

// 3. Presenter 层:处理业务逻辑(不继承ViewModel)
class UserPresenter(
    private val repository: UserRepository,
    private val coroutineScope: CoroutineScope // 注入协程作用域
) {  
    ...
    // 获取用户数据的方法
    fun loadUser(userId: String) {
        coroutineScope.launch(Dispatchers.IO) {  // 使用注入的作用域启动协程 // 指定IO调度器
        ...// 协程逻辑

示例代码中,可以看到UserPresenter的构造函数有一个类型为 CoroutineScope 的参数coroutineScope

用于将协程作用域(即协程的生命周期)从外部注入到 Presenter 内部,即协程作用域由外部指定。

注入的作用域,一般与组件(如 Activity、ViewModel)的生命周期绑定。当宿主销毁时,作用域自动取消所有协程,避免内存泄漏。如果直接在类内部创建 CoroutineScope,可能导致内存泄漏(例如 Activity 销毁后协程仍在运行)。

常见注入的作用域类型:

优先使用 Android 提供的 lifecycleScopeviewModelScope(ViewModel 专属),或在自定义组件中手动管理作用域。

作用域类型生命周期绑定对象自动取消时机
lifecycleScopeActivity/Fragment组件销毁时(onDestroy)
viewModelScopeViewModelViewModel 清除时(onCleared)
GlobalScope应用进程进程结束(不推荐使用)
自定义作用域手动管理调用 cancel ()

View层负责注入lifecycleScope

在我们的例子中,coroutineScope是谁注入的呢?

可以看到是Activity(MVPS中的View层),由Activity在创建presenter的时候去指定,具体使用的是lifecycleScope,则Presenter 内部协程的生命周期会与外部组件(如 Activity)绑定,当外部组件销毁时,注入的作用域会自动取消协程。

// 4. View 层(Activity)
class UserActivity : AppCompatActivity() {
    private val presenter by lazy {
        UserPresenter(
            repository = UserRepositoryImpl(),
            coroutineScope = lifecycleScope //使用Activity的生命周期作用域
        )
    }

tips:除了注入coroutineScope,还有一种方法是让UserPresenter继承ViewModel,则viewModelScope 自动与 ViewModel 生命周期绑定,无需手动注入。这种方式更简单快速。但注入法更灵活,不依赖 Android 组件,适用于跨平台架构。这里不多深挖,用到的时候如果需要再自行查询选择。

class UserPresenter : ViewModel() { // 继承 ViewModel

Present层负责使用Scope

  • Presenter 无需关心作用域的创建和销毁,由外部(Activity)负责。
  • 当 Activity 销毁时,lifecycleScope 会自动取消所有协程,避免内存泄漏。

在我们的例子中,UserPresenter内部会使用注入的作用域启动协程:coroutineScope.launch(Dispatchers.IO) {...}

这里又出现一个新的概念:Dispatchers(调度器)

Dispatchers指定协程所在线程池

示例代码中启动协程时coroutineScope.launch(Dispatchers.IO) {...},有参数Dispatchers.IO

Dispatchers 决定协程在哪个线程或线程池执行,避免阻塞关键线程(如 Android 主线程)。

调度器作用适用场景
Dispatchers.Main在 Android 主线程执行,用于更新 UI。在非 Android 环境中与 Default 相同。更新 TextView、启动动画等 UI 操作。
Dispatchers.IO优化 IO 密集型操作,线程池大小动态调整(默认最多 64 线程)。网络请求、文件读写、数据库操作。
Dispatchers.Default优化 CPU 密集型操作,线程池大小为 CPU 核心数。复杂计算(如 JSON 解析、图像处理)。
Dispatchers.Unconfined初始在当前线程执行,遇到挂起点后由被调用的挂起函数决定运行线程。测试、无需特定线程执行的场景(不推荐在实际业务中使用)。

选择调度器的核心原则是:让合适的操作在合适的线程池执行。在 Android 开发中,牢记 IO 操作使用 Dispatchers.IO,UI 更新使用 Dispatchers.Main 这一基本准则即可覆盖大部分场景。

建议:在实际开发中,始终显式指定调度器,确保:

  • 网络请求使用 Dispatchers.IO
  • UI 操作使用 Dispatchers.Main
  • 计算任务使用 Dispatchers.Default

suspend挂起函数

在Model层中可以看到一个suspend函数

suspend fun fetchUser(userId: String): User 

被Presenter 层调用,且调用位置在协程内部(见launch{})

class UserPresenter(
...
    fun loadUser(userId: String) {
        coroutineScope.launch(Dispatchers.IO) {
            _state.update { it.copy(isLoading = true) }
            try {
                val user = repository.fetchUser(userId)
                ...

suspend 关键字标记一个函数为 “挂起函数”(会等待代码块执行完再继续),适合需要 “拿到结果再执行后续逻辑” 的场景

挂起函数只能在协程作用域或其他挂起函数内调用。我们前面讨论过的collect 就是一个 suspend 函数,这意味着你不能在普通函数中直接调用 flow.collect { ... },必须通过 launchasync 或其他协程构建器启动协程。

使用建议:

我们知道model层的职责是,负责数据的管理和处理,包括数据的获取(网络请求、数据库操作等)、存储、业务逻辑计算等。

1、若 Model 层包含 网络请求、文件读写、数据库操作 等耗时操作,推荐使用 suspend 函数,并在内部通过 withContext(Dispatchers.IO) 切换线程

2、若方法仅涉及 内存数据处理、简单计算,无需声明为 suspend

withContext切换线程

withContext:临时切换协程的执行线程(在协程中切换调度器),并在完成后切回原线程。

特别适合在异步任务里插一嘴 “主线程更新”,然后继续后台逻辑。(如网络请求后更新 UI: “网络请求 → 数据解析 → UI 更新”)。

我们的示例就是这样的典型用法,使用withContext切换回主线程更新状态,更新完了自动切回来,过程如下:

  • 暂停当前在 IO 线程的协程,把任务 “挪” 到 Main 线程(Android 主线程,用于更新 UI)执行 _state.update { ... }
  • 执行完 Main 线程里的代码后,自动切回原协程调度器(也就是 Dispatchers.IO )继续后续逻辑。
class UserPresenter(
...
    fun loadUser(userId: String) {
        coroutineScope.launch(Dispatchers.IO) {
            _state.update { it.copy(isLoading = true) }
            try {
                val user = repository.fetchUser(userId)
                withContext(Dispatchers.Main) {  // 切换回主线程更新状态
                    _state.update {...}
                }//结束了会自动切回原线程Dispatchers.IO

为什么需要切换:

  • repository.fetchUserDispatchers.IO 后台线程执行网络请求,避免阻塞主线程。
  • 获取数据后,需要在主线程更新 UI(Android 规定只能在主线程修改 View),因此通过 withContext(Dispatchers.Main) 切换回主线程。
  • 这种 “切过去、切回来” 的流程,完美适配 “后台异步任务 + 主线程更新 UI” 的需求(比如网络请求完,切回主线程刷新页面),既不阻塞主线程,又能安全更新 UI,比传统回调或 Handler 简洁很多。

launch 的区别:

  • launch 用于启动新协程,withContext 不启动新协程,而是在当前协程内切换线程。

性能优化

  • 避免频繁切换:过多的 withContext 会增加线程调度开销。
  • 批量操作:将同一类型的操作放在同一个 withContext 块中执行。

MVPS和MVP的区别

MVPS 并不是一个被广泛认可的标准架构术语(主流架构中只有 MVP、MVVM、MVC 等),它更可能是某些团队或场景下对 MVP 的自定义扩展(比如强调 “State” 的作用),因此不同语境下的定义可能有差异。

在我的使用场景中,MVPS就是在MVP的基础上多了一个State,另外Presenter和View的交互方式上有区别(多了state层)。

我们来看下MVP中Presenter的职责:

  • 作为 View 和 Model 之间的桥梁,接收 View 的用户操作,调用 Model 处理数据,再将处理结果通知 View 更新 UI。

与上面对比可以发现,MVP中Presenter可以直接访问View接口来更新UI。而MVPS中,Presenter 只更新 State,由View 通过观察 State 的变化自动更新 UI。

可以看看下面的代码示例就更直观了。

  1. 传统 MVP 中 Presenter 与 View 的交互
  • 直接调用 View 接口:Presenter 持有 View 的接口(如 IView),当数据处理完成后,会直接调用 View 接口的方法(如 updateUI(data)showLoading())来通知 View 更新。

  • 这里 Presenter 直接 “命令” View 做什么,属于 “命令式交互”

    // Presenter 中
    fun loadData() {
        model.fetchData(object : Callback {
            override fun onSuccess(data: Data) {
                view.updateUI(data) // 直接调用 View 接口方法
            }
            override fun onError() {
                view.showError()
            }
        })
    }
    
  1. 新的MVPS 中引入 “State” 作为中间层

    举例直接看我们的MVPS示例代码即可

  • 通过 State 间接影响 View:Presenter 不直接调用 View 方法,而是维护一个 “状态对象(State)”(如 UIState,包含数据、加载状态、错误信息等)。当数据处理完成后,Presenter 只更新 State,View 则通过观察 State 的变化(如监听回调、数据绑定)自动更新 UI。
  • 这里 Presenter 只负责 “维护状态”,View 负责 “响应状态变化”,属于 “状态驱动式交互”

MVPS 的优势

  1. 可测试性强:Presenter 和 State 可以独立测试,不依赖 UI 组件。
  2. 状态可控:所有 UI 状态集中在 State 中,便于调试和追踪。
  3. 代码分离清晰:View 只负责渲染,Presenter 只负责逻辑,职责明确。
  4. 响应式编程:结合 RxJava 或 Kotlin Flow 可实现高效的异步数据流处理。

MVPS和MVVM

现代 Android 开发的主流趋势是 MVVM,因其与官方框架深度整合、状态管理规范,尤其适合中大型项目。而 MVPS 更适合特定过渡场景或轻量级项目

经过学习我发现,我的场景中 “MVPS” 更接近 MVP 向 MVVM 的过渡形态,猜测应该是项目原本是用MVP做的,后来为了解决一些耦合问题,于是向MVVM方向进行重构,从而形成了MVPS这样一种状态。