MVC、MVP、MVVM,谁才是最好的架构模式
四人帮著有经典设计模式23种,是为具体场景而抽象出来的一些解决方案,而MVC、MVP和MVVM不应该被称为设计模式,如果硬要说是设计模式,也是一种更高层面的设计模式,是对整个项目在视图、控制以及逻辑三个层面的切分与相互通信的设计。
一千个人心中有一千个哈姆雷特,同样的,对于MVC、MVP以及MVVP,一千个程序员也有一千种理解,但通俗的理解还是有一些共识的。
- MVC把整个项目结构分为Model(模型)、View(视图)和Controller(控制器)三部分,每个功能模块又可以按此方式划分。View主要管理UI相关,Model是业务逻辑的处理与数据的请求与储存,Controller是将View和Model衔接起来,是对数据接收和结果的响应,并更新UI,或者接收UI的用户事件,触发相应的业务逻辑。MVC把代码按功能划分为不同的模块,因此各个模块可以有接口定义,各个模块之间没有直接耦合,可实现可替换的效果,从而也增加了可重用性。但MVC模式在定义上不是很明确,比如View和Controller的职责可能在真正的开发时常被混淆,Activity/Fragment属于View还是Controller?说Activity/Fragment是View,但其中包含了很多数据处理与用户事件处理的逻辑,而要说是Controller的话,Activity/Fragment中又有很多View初始化、UI设置的功能。不是说Activity/Fragment将View和Controller混淆了,而是Controller的定义在真正的开发中很不明确。可能只有非常老道的架构师才能基于MVC模式写出让人觉得完美的代码,而老道的架构师可能已经不局限于什么架构模式了。
- MVP将MVC中的Controller换成了Presenter,其实名字不重要,重要的是P表示的概念要比C在架构层面让人更清晰。Presenter意为表示器,是为了隔断View和Model。Presenter通过接口给View调用,MVP对于MVC来说,进化在于MVP的M和V之间只能通过P来间接依赖,这样对于UI和逻辑,能够更清晰。
- MVVM应该被理解成Model+View+ViewModel,也就是将MVC和MVP中的C和P换成了ViewModel。ViewModel储存UI相关的状态字段,当Model中的业务逻辑结果需要回调时,就更新状态字段,这些状态字段与UI相互绑定,只要状态字段更新了,UI也发生相应的更新,这样就不需要在Controller或者Presenter中写数据绑定逻辑或者相互持有对象了。而且ViewModel中对生命周期做了特殊处理,在非Active的状态下,不允许更新UI,当状态变更为Active的时候,更新指令再执行,这样也省去了很多UI的状态判断。MVVP模式应被理解成一种数据与视图的绑定关系范式,当数据变化时,就更新视图的状态字段,状态与UI更新绑定,这样就形成了数据更新导致UI更新,UI事件触发业务操作。
ViewModel简单Demo
class ViewModelDemoActivity: AppCompatActivity() {
companion object {
const val TAG = "ViewModelDemoActivity"
class DemoViewModel: ViewModel() {
val demo = MutableLiveData<Int>().also { it.postValue(0) }
}
class GetDemoFragment(): Fragment() {
lateinit var demoViewModel: DemoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
demoViewModel = ViewModelProvider(activity!!, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DemoViewModel() as T
}
}).get(DemoViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val btn = Button(context)
btn.text = "get"
btn.setOnClickListener {
Toast.makeText(context, "get demo = ${demoViewModel.demo.value}", Toast.LENGTH_SHORT).show()
}
return btn
}
}
class SetDemoFragment(): Fragment() {
lateinit var demoViewModel: DemoViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
demoViewModel = ViewModelProvider(activity!!, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DemoViewModel() as T
}
}).get(DemoViewModel::class.java)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val btn = Button(context)
btn.text = "set"
btn.setOnClickListener {
demoViewModel.demo.postValue(demoViewModel.demo.value!! + 10)
}
return btn
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "activity onCreate")
val content = LinearLayout(this)
content.gravity = gravity.center is For Sale
content.orientation = LinearLayout.VERTICAL
val btn = Button(this)
btn.text = "change orientation"
btn.setOnClickListener {
requestedOrientation = if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
}
content.addView(btn)
val contentView = LinearLayout(this)
contentView.orientation = LinearLayout.HORIZONTAL
val fragment1 = FrameLayout(this)
fragment1.id = R.id.fragment1
val fragment1LayoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1.0F)
contentView.addView(fragment1, fragment1LayoutParams)
val fragment2 = FrameLayout(this)
fragment2.id = R.id.fragment2
val fragment2LayoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.MATCH_PARENT, 1.0F)
contentView.addView(fragment2, fragment2LayoutParams)
val demoViewModel = ViewModelProvider(this, object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return DemoViewModel() as T
}
}).get(DemoViewModel::class.java)
demoViewModel.demo.observe(this, Observer {
Toast.makeText(this, "demo changed to $it", Toast.LENGTH_SHORT).show()
})
content.addView(contentView)
setContentView(content)
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment1, GetDemoFragment())
transaction.replace(R.id.fragment2, SetDemoFragment())
transaction.commitAllowingStateLoss()
}
}
在这个demo中使用了DemoViewModel,其只有一个LiveData的属性demo。Activity中有两个Fragment,分别有一个按钮,点击时分别是get和set这个DemoViewModel中demo。Activity中有个按钮是用来旋转屏幕的,这是为了让屏幕旋转时,强制重新执行一遍Activity的onCreate,并重新创建两个Fragment,以此来演示ViewModel在Activity/Framgent重建过程中的作用,而两个Fragment也是为了演示用ViewModel在两个Fragment之间进行数据传输的作用。
ViewModel源码解析
要想弄懂ViewModel特性的实现原理,还是得看ViewModel的创建过程。ViewModel通过ViewModelProvider#get方法获取到,传入参数类型为ViewModelStoreOwner和Factory。这里的ViewModelStoreOwner接口只有一个方法getViewModelStore,返回一个ViewModelStore的对象。ComonentActivity和Fragment实现了ViewModelStoreOwner接口并实现了getViewModelStore方法。
ComponentActivity#getViewModelStore方法的实现是先调用getLastNonConfigurationInstance()方法获取一个NoConfigurationInstances对象,而这个NoConfigurationInstances对象是在onRetainNonConfigurationInstance方法中创建出来的,看这个方法的注释,意思应该是在因为configuration变化导致的destroy过程中,会调用这个onRetainNonConfigurationInstance方法来储存一些对象,新的Activity中可以通过getLastNonConfigurationInstance方法获取到储存的对象。而在ComponentActivity的onTetainNonConfigurationInstance方法就将viewModelStore对象储存到了NoConfigurationInstances对象中,再在getViewModelStore方法中通过getLastNonConfigurationInstance方法返回回来,这也是ViewModel在Activity重建后还能获取到原来的ViewModel的原因,从而让ViewModel在Activity重建后仍然能还原数据。而Fragment则有两种策略,当Fragment所在的Activity是个ViewModelStoreOwner对象时,就跟所在的Activity共享同一个ViewModelStore准确来说是在Activity的ViewModelStore中存一个FragmentManagerViewModel,而这个FragmentManagerViewModel中有一个Map<String, ViewModelStore>,其中String对应着Fragment,而ViewModelStore就表示这个Fragment的ViewModelStore。否则就自己创建一个,并保存在FragmentManagerImpl中,这样就算自己被销毁了,也还是能从FragmentManagerImpl中把ViewModel给还原回来。这两种的区别是如果Activity能储存,那么Activity销毁重建时,Fragment里的ViewModel也可以还原,而如果Activity不能储存时,就只有Activity不销毁,而Fragment销毁的时候能还原回ViewModel,因为Fragment虽然销毁了,但FragmentManagerIml是被Activity持有的。
ViewModelStoreOwner提供了ViewModelStore,其实ViewModel之所以能在Activity/Fragment销毁后还能还原,全依赖于ViewModelStore通过在新的Activity/Fragment中找到老的中的ViewModelStore对象。再看回ViewModelProvider来,Factory参数就不用说了,就是个如何创建ViewModel的工厂类,在ViewModelProvider的get方法中,判断如果ViewModelStore中找不到对应的ViewModel就用Factory的create方法新创建一个出来。ViewModelProvider的创建方法仅仅是将传入的ViewModelStore和Factory方法保存下来,而get方法仅仅是从ViewModelStore中获取ViewModel,获取不到就用Factory创建一个并存入ViewModelStore中。
总结
分析源码可以得知,ViewModel其实是依赖于Activity#onRetainNonConfigurationInstance和Activity#getLastNonConfigurationInstance方法结合起来可以在Activity因configuration变化时导致的Activity销毁重建时保存并还原对象。由于ViewModel的创建过程由Factory来确定,因此可以让ViewModel不持有Activity/Fragment这类带context的重对象。将ViewModel和LiveData结合起来,就可以在没有内存泄漏的情况下,友好地处理生命周期与UI操作之间的关系,还能安全地保存并还原数据并还原UI。LiveData主要是处理生命周期与数据绑定UI的关系,而ViewModel则是处理Activity销毁重建过程中的数据恢复问题,ViewModel+LiveData也被常用来实现MVVM架构,让LiveData处理数据绑定问题,让ViewModel处理数据与UI生命周期的关系问题。