مقایسه دو معماری MVP و MVVM در اندروید موضوعی است که در این مقاله به آن می پردازیم. این روز ها دو الگوی طراحی یا دیزاین پترن Design pattern اصلی در دنیای برنامه نویسی اندروید وجود دارند، MVP و MVVM. در این مقاله به تفاوت ها و شباهت های معماری MVP و MVVM می پردازیم.
هر دو دیزاین پترن Design Pattern سیستم را به سه قسمت اصلی: Model , View و Controller تقسیم می کنند. نکته جالب در رابطه با این دو Design Pattern اندروید این است که، ممکن است کسی که تجربه کار با هر دو را نداشته باشد، نتواند تفاوت این دو الگوی طراحی اندروید را تشخیص دهد.
بخش Model در هر دو Design Pattern یکسان است و تفاوتی ندارد. به دو بخش دیگر می پردازیم که شامل View و Presenter در MVP و View و ViewModel در MVVM هستند.
مهم ترین تفاوت بین این دو معماری، نبود فلش برگشت از ViewModel به View در MVVM می باشد.(تصویر بالا)
در MVP نقش Presenter ارائه دهنده تغییرات به View است و همچنین، دریافت کننده داده برای تکمیل عملیات می باشد. در MVVM اما، شاهد تفاوت بزرگی با MVP هستیم. ViewModel فقط و فقط وظیفه فراهم کردن داده را دارد در حالیکه View تنها داده ها را مصرف می کند.
حتی اگر View وظیفه تصمیم گیری درباره نحوه مصرف داده را به عهده داشته باشد، به این معنی نسیت که باید شامل پیاده سازی منطق پیچیده ای درون خود باشد. به عبارتی می توان گفت ایده ViewModel از ابتدا این بود که تنها داده را برای View آماده کند و از آن سو View داده را بدون انجام هیچ عملیات سخت و پیچیده ای نمایش دهد.
ViewModel می تواند عملیات منطقی و پیچیده بر روی داده های ورودی کاربر اعمال کند اما View تنها وظیفه گرفتن داده و نمایش آن را به عهده خواهد داشت.
در ادامه مقایسه MVP و MVVM را در قالب مثال های مختلف انجام خواهیم کرد.
در مثال زیر از معماریMVP استفاده شده است و هدف آن مقدار دهی TextView توسط Presenter می باشد.
class MainPresenter(private val view: MainView) {
fun setupView() {
view.changeText("Hello World!")
}
interface MainView {
fun changeText(textValue: String)
}
}
class MainActivity : AppCompatActivity(), MainPresenter.MainView {
private val presenter = MainPresenter(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
presenter.setupView()
}
override fun changeText(textValue: String) {
helloTextView.text = textValue
}
}
به دو نکته مهم در مثال فوق دقت کنید:
همچنین بخوانید: معماری mvp در اندروید با کاتلین
معادل کد فوق را با MVVM به صورت زیر پیاده می کنیم.
class MainViewModel {
val textValue = "Hello World!"
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = MainViewModel()
helloTextView.text = viewModel.textValue
}
}
همانطور که می بینید، وظیفه View است که تصمیم بگیرید چه زمانی از داده ای که درون ViewModel وجود دارد استفاده کند. اگر دقت کرده باشید دیگر نیازی به پیاده سازی یک واسط یا interface نداریم. که این امر به معنی کم تر شدن هرچه بیشتر کدها و بوجود آمدن خوانایی بیشتر است.
در MVVM ما استقلال بیشتری در کدها خواهیم داشت، این جمله به چه معناست؟ از آنجا که ViewModel دیگر اتصالی به View ندارد پس استقلال بیشتری دارد و وابستگی کمتری در کدها بوجود آمده است. این امر باعث می شود بتوانیم تغییرات بیشتری را در UI بدون نیاز به اعمال تغییر در لایه های منطقی کار، بوجود بیاوریم.
اما در دنیای واقعی معمولا خبری از کدنویسی ساده و بی دردسر نیست. داده ها تغییر می کنند، در صفحه نمایش هر لحظه ممکن است چرخش ایجاد شود، کاربران هر ثانیه ممکن است از اپلیکیشن خارج شوند. پس ما نیاز به ابزار یا ابزارهایی برای مدیریت تمام اتفاقات پیش رو خواهیم داشت.
یکی از راه حل های ارائه شده توسط MVP این است که از RxJava برای مدیریت چرخه حیات Activity یا Fragment استفاده کنیم که کار دشواری است.
همچنین بخوانید: کتابخانه RxJava را کاربردی بیاموزیم
بیایید با مثال به این موضوع بپردازیم.
class MainPresenter @Inject constructor(private val service: WorldService) {
private var view: MainView? = null
private var disposable: Disposable? = null
fun onButtonClicked() {
view?.changeProgressVisibility(View.VISIBLE)
disposable = service.requestHello()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onServiceResponse)
}
fun bind(view: MainView) {
this.view = view
}
fun unbind() {
disposable?.dispose()
this.view = null
}
private fun onServiceResponse(response: String) {
view?.changeProgressVisibility(View.GONE)
view?.changeText(response)
}
interface MainView {
fun changeText(textValue: String)
fun changeProgressVisibility(visibility: Int)
}
}
class MainActivity : AppCompatActivity(), MainPresenter.MainView {
@Inject lateinit var presenter: MainPresenter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
button.setOnClickListener { presenter.onButtonClicked() }
}
override fun onStart() {
super.onStart()
presenter.bind(this)
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
presenter.unbind()
}
override fun changeText(textValue: String) {
helloTextView.text = textValue
}
override fun changeProgressVisibility(visibility: Int) {
progressBar.visibility = visibility
}
}
در این مثال ما از Dependency Injection برای دریافت نمونه ای از Presenter استفاده کردیم. به علاوه برای جلوگیری از بوجود آمدن leak در View توسط presenter باید دستورات لازم را بنویسیم. (کتابخانه هایی برای انجام این کار در معماری MVP ارائه داده شده اند.)
زمان کلیک بر روی Button، Presenter مجددا در پس زمینه و به صورت asynchronous برای انجام عملیات خاصی ایجاد می شود و همچنین منتظر response خواهد ماند چرا که زمان دریافت response، اقدام به آمدن به thread اصلی برای نمایش response در UI می کند. مگر اینکه کاربر پیش از دریافت موفق ریسپانس اقدام به خروج از Activity جاری بکند که به همین خاطر است که ما هم دستور presenter.unbind() را در onSaveInstanceState() نوشته ایم که کرشی اتفاق نیافتد و تنها حرکت کاربر بین صفحات و یا خروج کاربر از صفحه انجام شود.
نکته دیگری که باید زمان پیاده سازی MVP به آن دقت کنیم، lifecycle-aware بودن کلاس هاست که کد های boilerplate زیادی را به دستورات ما اضافه می کند.
خوشبختانه گوگل راه حل سریع تری برای lifecycle-aware کردن کلاس ها با نام Android architecture components ارائه داده که کمک بزرگی به معماری های اندروید است. ما دو کلاس مهم و ساده ارائه شده توسط گوگل را در زیر آورده ایم که البته استفاده زیادی دارند:
یکی از کارهایی که ViewModel برای آن ساخته شده این است که از داده های مرتبط به UI زمان چرخش صفحه در برابر destroy شدن محافظت می کند.
با استفاد از LiveData می توان شی های داده ای ساخت که زمان تغییر داده در لایه های زیرین مرتبط به دیتابیس، به UI اطلاع داد تا همگام با دیتابیس به روز شود.
بیایید نگاهی به نحوه پیاده سازی مثال قبل در معماری MVVM بیندازیم.
class MainViewModel(private val service: WorldService) : ViewModel() {
val textValue = MutableLiveData<String>()
val inProgress = MutableLiveData<Int>()
private var disposable: Disposable? = null
fun onButtonClicked() {
inProgress.value = View.VISIBLE
disposable = service.requestHello()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onServiceResponse)
}
override fun onCleared() {
disposable?.dispose()
}
private fun onServiceResponse(response: String) {
inProgress.value = View.GONE
textValue.value = response
}
}
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModelFactory: MainViewModelFactory
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Dependencies.inject(this)
val viewModel = getViewModel().apply {
textValue.observe(this@MainActivity, Observer { helloTextView.text = it })
inProgress.observe(this@MainActivity, Observer { it?.let { progressBar.visibility = it } })
}
button.setOnClickListener { viewModel.onButtonClicked() }
}
private fun getViewModel() = ViewModelProviders.of(this, viewModelFactory).get(MainViewModel::class.java)
}
همانگونه که مشاهده می کنید ما مجددا از Dagger استفاده کردیم اما نکته قابل توجه در معماری MVVM اینجاست که ما کلاس ViewModel را از ViewModelProvider گرفتیم پس نگرانی از بابت حفظ داده در تغییرات حالت نخواهد بود. به علاوه به کامپوننت های lifecycle-awareness دیگری هم نیازی نخواهد بود چرا که ViewModel دیگر ارتباطی با View ندارد.
حال Activity به LiveData برای مشاهده تغییرات در UI دقت دارد و همچنین unsubscribing هم درون خود کامپوننت های کتابخانه مدیریت می شود و دیگر نیازی به نوشتن کد های boilerplate نخواهد بود.
دقت داشته باشید که دستورات اتصال به بک اند یا همان فراخوانی api ها همگی درون ViewModel انجام می شوند کاملا مستقل از چرخه حیات View. البته همان دستورات request هم در بک گراند و به صورت asynchronous انجام می شوند.
خب به طبع با این حالات ما نگرانی های کمتری نسبت به مواردی چون lifecycle-awareness خواهیم داشت. همین امر باعث دقت به موارد دیگر و همچنین سرعت کار خواهد شد.
بخشی از کد مثال قبل در هر دو معماری MVP و MVVM که به صورت asynchronous پیاده شده بودند که تا میزان زیادی به هم شبیه هستند و هر دو عملکرد قابل قبولی دارند. اما بیاید این دو معماری را زمانی که در حالت synchronous هستند مقایسه کنیم. زمانی که controller دستورات را مستقیما به view ارسال میکند.
class MainPresenter(private val view: MainView) {
fun onHelpButtonClicked() {
view.showHelp()
}
interface MainView {
fun showHelp()
}
}
class MainActivity : AppCompatActivity(), MainPresenter.MainView {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val presenter = MainPresenter(this)
helpButton.setOnClickListener { presenter.onHelpButtonClicked() }
}
override fun showHelp() {
HelpDialog().show(supportFragmentManager, null)
}
}
همانگونه که مشاهده می کنید Presenter مانند سابق view را مستقیما فراخوانی می کند.(اما این بار ممکن است دردسر ساز شود چرا که این اتفاقات synchronous رخ می دهد.)
در معماری MVVM نسبت به معماری MVP اوضاع کمی متفاوت است.
class MainViewModel : ViewModel() {
val showHelpEvent = SingleLiveEvent<Void>()
fun onHelpButtonClicked() {
showHelpEvent.call()
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
subscribeToNavigationChanges(viewModel)
helpButton.setOnClickListener { viewModel.onHelpButtonClicked() }
}
private fun subscribeToNavigationChanges(viewModel: MainViewModel) {
// The activity observes the navigation events in the ViewModel
viewModel.showHelpEvent.observe(this, Observer {
HelpDialog().show(supportFragmentManager, null)
})
}
}
همانگونه که در معماری MVVM مشخص است ما از ViewModel استفاده می کنیم و در حالت synchronous هم ViewModel خطرات MVP را برای ما از بین برد.
ViewModel دستورات را درون LiveData قرار می دهد و از طریق آن، و نتیجه را به View پاس می دهد که این نتیجه هم به صورت observe کردن جریان showHelpEvent انجام می گیرد.
نکته:
SingleLiveEvent کلاسی است که زمان اجرا اطمینان حاصل می کند که رویداد تنها یکبار آماده خوانده شدن است و از نمایش چندباره نتیجه جلوگیری می کند.
مثال فوق که در MVVM پیاده شد، در مقایسه با MVP کمی پیچیده تر شده است. اما MVVM ارزش این را دارد که با این پیچیدگی ها کنار بیاییم دستورات مطمئن تری پیاده کنیم. به علاوه در MVVM نتیجه نگه نداشتن نمونه ای از View در ViewModel باعث می شود خطر فراموش شدن stream داده که به راحتی قابل تست با Junit نیست، از بین برود.
اخیرا MVVM محبوبیت بسیاری بین توسعه دهندگان اندروید پیدا کرده است. در این مقاله نیز ما سعی کردیم مثال های کاربردی و ساده ای را پیاده کنیم تا نحوه تبدیل Presenter به ViewModel را ببینید و همچنین به چرایی مهاجرت به MVVM پی ببرید.
در این پست از تجاری اپ ما با مثال های ساده تفاوت دو معماری MVP و MVVM را مشخص کردیم. چیزی که در انتها می توان نتیجه گیری کرد این است که MVVM استقلال بیشتری در کدها ایجاد می کند و در نهایت نگرانی از بابت ایجاد وابستگی بین کدها نداریم. کدهای boilerplate کمتری ایجاد می شود. ما را تشویق به کد نویسی واکنش گرا می کند. به علاوه خود گوگل هم کتابخانه هایی برای توسعه راحت تر کدها با معماری MVVM پیشنهاد داده است که خیال توسعه دهندگان را از لحاظ مدیریت lifecycle کلاس ها راحت می کند. همین امر باعث crash و memory leak کمتری نیز می شود.
برای سناریو هایی که controller نیاز دارد به صورت synchronously با UI در ارتباط باشد و تغییرات اعمال کند، presenter بهتر عمل کرد. شاید به همین دلیل است که گوگل پیشنهاد می کند هر دو ViewModel و Presenter را در پروژه های پیچیده که UI با جزییات زیادی دارند، استفاده کنید. مخصوصا زمانی که شما میخواهید جزییات UI را test کنید مانند انیمیشن ها و…
در نهایت باید این را هم بگوییم که اگر از معماری MVP استفاده می کنید و در پروژه شما به خوبی عمل می کند واقعا نیازی نیست که به MVVM مهاجرت کنید. اما اگر هم می خواهید مهاجرت کنید می توانید صفحه به صفحه این کار را انجام دهید.
اگر از MVVM استفاده می کنید باید در نظر داشته باشید که از کتابخانه Data Binding هم استفاده کنید. که مکمل ویژه ای برای ViewModel به حساب می آید و کدها را کم و بهینه می کند.
امیدوارم این آموزش برای شما مفید بوده باشد…
کلمات کلیدی: مقایسه MVP و MVVM,مقایسه دو معماری MVP و MVVM,یزاین پترن MVP,یزاین پترن MVVM,الگوی طراحی MVP,الگوی طراحی MVVM