What is MVVM architecture?
Model-View-ViewModel (MVVM) is a successor of MVC, invented by Microsoft architects to apply event-driven programming to applications involving user interface (UI) (Here is the original article). Separation of concerns reaches its highest in this architecture, as the different parts of the architecture are completely separate and have minimum dependence on each other.
The figure below shows how MVVM compares to MVP and MVC. Contrary to MVC and MVP, there is no two-sided relation between the View and ViewModel in MVVM. In other words, the ViewModel does not even know who the View is. The same is also true for the ViewModel and the Model.
The above separation in MVVM is very favourable in writing modular applications. For example, replacing the View in MVVM for another View does not have a single effect on the architecture, since ViewModel is completely View agnostic. Also because the Model does not have or use any instance of the ViewModel, changing the ViewModel would not affect it, namely the Model is ViewModel agnostic. Thus the flow control is one-sided from the View (UI) to the Model, which is one of the principles of a clean architecture.
Due to the one-way flow control of MVVM, the outer layers of the architecture cannot be directly informed of the changes in inner layers by the inner elements. Instead they can observe for changes by subscribing to the data of interest in inner layer elements. I will explain this more in the next section.
MVVM architecture in android
Let’s suppose the same benchmark movie app example introduced in the first part of this series. User enters a search term for a movie and presses the ‘FIND’ button, based on which the app searches for the list of movies including that search term and shows them. Clicking on each movie on the list then shows its details.
I will now explain how this app is implemented in MVVM followed by the complete android app, which is available on my GitHub page.
When the user clicks on the ‘FIND’ button on the View, a method is called from the ViewModel with the search term as its argument:
1 2 3 4 |
main_activity_button.setOnClickListener({ showProgressBar() mMainViewModel.findAddress(main_activity_editText.text.toString()) }) |
The ViewModel then calls the findAddress method from the Model to search for the movie name:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fun findAddress(address: String) { val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() { override fun onSuccess(t: List<MainModel.ResultEntity>) { entityList = t resultListObservable.onNext(fetchItemTextFrom(t)) } override fun onError(e: Throwable) { resultListErrorObservable.onNext(e as HttpException) } }) compositeDisposable.add(disposable) } |
When the response comes from the Model, the onSuccess method of the RxJava observer carries the successful result, but as the ViewModel is View agnostic, it does not have or use any View instance to pass the result for showing. It instead triggers an event in the resultListObservable by calling resultListObservable.onNext(fetchItemTextFrom(t)) , which is observed by the View:
1 2 3 4 |
mMainViewModel.resultListObservable.subscribe({ hideProgressBar() updateMovieList(it) }) |
So the observable plays a mediator role between the View and ViewModel:
- ViewModel triggers an event in its observable
- View updates the UI by subscribing to ViewModel’s observable
Here’s the full code for the View. In this example, View is an Activity class, but Fragment can also be equally used:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
class MainActivity : AppCompatActivity() { private lateinit var mMainViewModel: MainViewModel private lateinit var addressAdapter: AddressAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mMainViewModel = MainViewModel(MainModel()) loadView() respondToClicks() listenToObservables() } private fun listenToObservables() { mMainViewModel.itemObservable.subscribe(Consumer { goToDetailActivity(it) }) mMainViewModel.resultListObservable.subscribe(Consumer { hideProgressBar() updateMovieList(it) }) mMainViewModel.resultListErrorObservable.subscribe(Consumer { hideProgressBar() showErrorMessage(it.message()) }) } private fun loadView() { setContentView(R.layout.activity_main) addressAdapter = AddressAdapter() main_activity_recyclerView.adapter = addressAdapter } private fun respondToClicks() { main_activity_button.setOnClickListener({ showProgressBar() mMainViewModel.findAddress(main_activity_editText.text.toString()) }) addressAdapter setItemClickMethod { mMainViewModel.doOnItemClick(it) } } fun showProgressBar() { main_activity_progress_bar.visibility = View.VISIBLE } fun hideProgressBar() { main_activity_progress_bar.visibility = View.GONE } fun showErrorMessage(errorMsg: String) { Toast.makeText(this, "Error retrieving data: $errorMsg", Toast.LENGTH_SHORT).show() } override fun onStop() { super.onStop() mMainViewModel.cancelNetworkConnections() } fun updateMovieList(t: List<String>) { addressAdapter.updateList(t) addressAdapter.notifyDataSetChanged() } fun goToDetailActivity(item: MainModel.ResultEntity) { var bundle = Bundle() bundle.putString(DetailActivity.Constants.RATING, item.rating) bundle.putString(DetailActivity.Constants.TITLE, item.title) bundle.putString(DetailActivity.Constants.YEAR, item.year) bundle.putString(DetailActivity.Constants.DATE, item.date) var intent = Intent(this, DetailActivity::class.java) intent.putExtras(bundle) startActivity(intent) } class AddressAdapter : RecyclerView.Adapter<AddressAdapter.Holder>() { var mList: List<String> = arrayListOf() private lateinit var mOnClick: (position: Int) -> Unit override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): Holder { val view = LayoutInflater.from(parent!!.context).inflate(R.layout.item, parent, false) return Holder(view) } override fun onBindViewHolder(holder: Holder, position: Int) { holder.itemView.item_textView.text = mList[position] holder.itemView.setOnClickListener { mOnClick(position) } } override fun getItemCount(): Int { return mList.size } infix fun setItemClickMethod(onClick: (position: Int) -> Unit) { this.mOnClick = onClick } fun updateList(list: List<String>) { mList = list } class Holder(itemView: View?) : RecyclerView.ViewHolder(itemView) } } |
Here is the ViewModel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class MainViewModel() { lateinit var resultListObservable: PublishSubject<List<String>> lateinit var resultListErrorObservable: PublishSubject<HttpException> lateinit var itemObservable: PublishSubject<MainModel.ResultEntity> private lateinit var entityList: List<MainModel.ResultEntity> private val compositeDisposable: CompositeDisposable = CompositeDisposable() private lateinit var mainModel: MainModel private val schedulersWrapper = SchedulersWrapper() constructor(mMainModel: MainModel) : this() { mainModel = mMainModel resultListObservable = PublishSubject.create() resultListErrorObservable = PublishSubject.create() itemObservable = PublishSubject.create() } fun findAddress(address: String) { val disposable: Disposable = mainModel.fetchAddress(address)!!.subscribeOn(schedulersWrapper.io()).observeOn(schedulersWrapper.main()).subscribeWith(object : DisposableSingleObserver<List<MainModel.ResultEntity>?>() { override fun onSuccess(t: List<MainModel.ResultEntity>) { entityList = t resultListObservable.onNext(fetchItemTextFrom(t)) } override fun onError(e: Throwable) { resultListErrorObservable.onNext(e as HttpException) } }) compositeDisposable.add(disposable) } fun cancelNetworkConnections() { compositeDisposable.clear() } private fun fetchItemTextFrom(it: List<MainModel.ResultEntity>): ArrayList<String> { val li = arrayListOf<String>() for (resultEntity in it) { li.add("${resultEntity.year}: ${resultEntity.title}") } return li } fun doOnItemClick(position: Int) { itemObservable.onNext(entityList[position]) } |
and finally the Model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class MainModel { private var mRetrofit: Retrofit? = null fun fetchAddress(address: String): Single<List<MainModel.ResultEntity>>? { return getRetrofit()?.create(MainModel.AddressService::class.java)?.fetchLocationFromServer(address) } private fun getRetrofit(): Retrofit? { if (mRetrofit == null) { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val client = OkHttpClient.Builder().addInterceptor(loggingInterceptor).build() mRetrofit = Retrofit.Builder().baseUrl("http://bechdeltest.com/api/v1/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(client).build() } return mRetrofit } class ResultEntity(val title: String, val rating: String, val date: String, val year: String) interface AddressService { @GET("getMoviesByTitle") fun fetchLocationFromServer(@Query("title") title: String): Single<List<ResultEntity>> } } |
Full project is on my GitHub page here.
What I like and dislike about MVVM?
I really like it, no flies on it to me. It has all the advantages of MVP, plus better separation of concerns and thus maintainability, extendability and testability. It also leads to less code than MVP on larger projects. For this sample app, however, the difference was not significant.
The only downside to this architecture might be the need for learning some event-driven or reactive programming. In android this is usually realised using Eventbus, RxJava/android or LiveData among others.
Also care must be taken to cancel the subscription of the observers to the observables according to the lifecycle of the view. If not properly done this can result in exceptions.
The bottom line is that MVVM seems to be the preferred architecture by Google. The architecture components introduced by Google in the last two years, i.e. ViewModel, LiveData, Lifecycle, are all based on MVVM architecture. I will write a complete series on architecture components later on.
Great explained, thanks!
Thanks for reading 🙂
Great article, very clean and easy to understand. Thank you so much.
“MVVP architecture in android” -> this will be “MVVM” right?
Yeah, right, thanks so much for finding that. Corrected now 🙂
You wrote: “Care must be taken to cancel the subscription of the observers to the observables according to the lifecycle of the view. If not properly done this can result in exceptions.”
Which is why you shouldn’t use Rx in views but only in viewmodels. Use LiveData to handle the communication with the view.
I agree
LiveData
is better suited for view-viewModel communications. Rx can be used for Views as well though.Great set of articles, thanks so much.
I have one question though. In other blogs describing MVVM i read that for a more pure version of it in Android it would be advisable to use Databinding. This way, there would be separation between xml version of the UI and the ‘controller’ version of the UI (Activity/Fragment).
For instance, an Activity would only care about listening to user events and the logic of when and what to show, but not how to show it. Somehow is a smaller version of an observable between these two parts of the UI (the layout ‘listening’ to the Activity).
What do you think about it? Do you think it´s an important part for implementing pure MVVM in Android?
Thanks.
Hey,
Thank you very much for your clear explanations.
I learned a lot and will definitely keep following this blog.
Thank you! 🙂
A Kotlin learner.