Introduction
In part 4, I started with implementing my benchmark movie app in Model-View-ViewModel (MVVM) using RxJava and with no use of Google architecture components (part of Jetpack now). Part 5 is where I first used LiveData as an architecture component, where I showed how LiveData can be used as a simple lifecycle-aware alternative to RxJava for implementing the observer pattern.
The architecture components, however, feature a component for the ViewModel, called ViewModel , as well. This part discusses what special it is about this component that makes it different to any other ViewModel in MVVM.
Therefore, the same movie search app is implemented here using this new component. This is a simple app with which the user can find the name and year of all the movies containing a keyword:
Sorry if you are already a reader of the series and see me repeat the description of the app each time in the beginning of each part. I just want each part to be standalone to avoid user’s loosing track by having to switch back and forth between the parts to understand the newest one.
The complete code for this part is in ‘mvvm-architecture_component-liveData&viewModel’ branch of my Github repo.
How is Jetpack ViewModel different than any other ViewModel in MVVM?
Before discussing this, let’s take a look at an issue in the app developed in part 5 which was not using Jetpack ViewModel . The issue appears when rotating the screen:
As the above shows, the search results clears when the screen rotates, which is not a good user experience.
The reason this happens is because of the OS destroying and recreating MainActivity each time configuration change happens. This causes any variable in memory to fade away, as such the list of search results needed for the RecyclerView adapter.
(You might be wondering why the text inside the EditText is still there after rotation. This is because android automatically saves the EditText content into storage on configuration change and reloads it upon recreation. This is the default and can be removed by setting setEnabled to false in the XML declaration of the EditText .)
The Jetpack ViewModel helps with keeping every variable inside the ViewModel related to the Activity in memory and thus making it persistent to configuration changes.
Let’s see some code
The Jetpack ViewModel implementation kicks in by extending it in the ViewModel class. It does not entail implementing any abstract method or interface and is as easy as adding : ViewModel() to the existing ViewModel definition.
Here is the MainViewModel of the movie app extending Jetpack 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 |
class MainViewModel() : ViewModel() { private val resultListObservable = MutableLiveData<List<String>>() private val resultListErrorObservable = MutableLiveData<HttpException>() private val itemObservable = MutableLiveData<MainModel.ResultEntity>() fun getResultListObservable(): LiveData<List<String>> = resultListObservable fun getResultListErrorObservable(): LiveData<HttpException> = resultListErrorObservable fun getItemObservable(): LiveData<MainModel.ResultEntity> = itemObservable private lateinit var entityList: List<MainModel.ResultEntity> lateinit var mainModel: MainModel private val schedulersWrapper = SchedulersWrapper() fun findAddress(address: String) { 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.postValue(fetchItemTextFrom(t)) } override fun onError(e: Throwable) { resultListErrorObservable.postValue(e as HttpException) } }) } 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.value = entityList[position] } } |
The instance of the ViewModel inherited from the Jetpack ViewModel needs to be bound to an Activity so that the OS knows to save the variables states inside it each time configuration change corresponding to the Activity happens. This enables the OS to restore these variables when recreating the Activity . Instantiating and binding is simply done by adding the following to the onCreate() of the Activity :
1 |
mMainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) |
where this relates to the Activity .
The above way of creating the ViewModel is applicable when the constructor is empty. What if this is not the case?
I will deal with that when incorporating dependency injection into the code in part 8 of the series. (I know this is part 6 :D)
Here is the complete code:
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 |
class MainActivity : AppCompatActivity() { private lateinit var mMainViewModel: MainViewModel private lateinit var addressAdapter: AddressAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) mMainViewModel = ViewModelProviders.of(this).get(MainViewModel::class.java) mMainViewModel.mainModel = MainModel() loadView() respondToClicks() listenToObservables() } private fun listenToObservables() { mMainViewModel.getItemObservable().observe(this, Observer { goToDetailActivity(it!!) }) mMainViewModel.getResultListObservable().observe(this, Observer { hideProgressBar() updateMovieList(it!!) }) mMainViewModel.getResultListErrorObservable().observe(this, Observer { 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() } 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) } } |
The Model is the same as that used in the last part:
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>> } } |
Here is how the app looks like in screen rotation after implementing Jetpack ViewModel :
Is it all ViewModel is there for?
One might argue we can handle configuration changes by storing variables into savedInstanceState bundle in onSaveInstanceState(savedInstanceState:Bundle ) and then restoring it in onRestoreInstanceState(savedInstanceState:Bundle) or onCreate(savedInstanceState:Bundle) . Then what is the point of having an architecture component just to handle this?
I would say yes, it is possible, but not efficient due to two main reasons:
- When user interface is complex, the number and types of objects to be stored becomes considerable. Saving and restoring each of them requires implementing Serializable and preferably Parcelable before being able to save each to savedInstanceState bundle and restoring later. Even though making parcelable objects can be done automatically in Android Studio, making a large number of parcelable objects can be error prone, causing NullPointerException when missing even one.
- The above persistence, including serialization and deserialization, takes place on the device storage which is far less efficient and slower compared to persistence in memory.
Bottom line
The Jetpack ViewModel is really great as we could see here how it helps with restoring variables from memory in configuration changes by adding very few lines of code to an existing MVVM code. The final question now is whether Jetpack ViewModel spares us the serialization/deserializaition we needed to do when not using Jetpack for the same purpose. The answer is still ‘No’ because not all the Activity destroy and recreation is due to configuration change. There are times such as when the app is in the background and the system is in high need of memory and finds no other way than destroying the process including the Activity . The Jetpack ViewModel does not help with this case and some storage persistence such as in savedInstanceState is necessary because the OS loses track of the state of the variables inside the Activity when it has to close it due to memory shortage.
Therefore, even though saving to storage is still necessary for some applications to cover the edge cases of unexpected Activity close, Jetpack ViewModel prevents from a large number of errors due to not manually saving the state of the Activity during configuration changes, which is much more probable to happen than Activity termination due to memory shortage considering the large memory size of devices today.
In the next part, I will write about another issue in the movie app where LiveData is not enough to handle at its present implementation. So stay tuned!
thanks for great articles!! I have read the whole 6 parts and my head exploded and my brain is on the table :D. Thanks for explaining MVC, MVP and MVVM
It’s great to know you have liked the series, thanks and stay tuned for the rest 🙂
I loved your series. The main problem with architectures is not which one is chosen, but that not all developers working on the project are across the implementation requirements, especially for the clean architecture. This is amplified on large projects. I’m sure this series will be really helpful to many developers to clear some of the confusion.
I am thinking of including your series in the architecture presentation at my workplace. There is only one thing missing though – unit tests are not complete. I was particularly interested to see how you have managed unit tests in MVVM with ViewModel and LiveData. Hope you get some time on your hands to finish that too.
Thanks a lot for your comment.
You’re right, unit tests are incomplete and missing for the sixth part of the series. I’ll update that soon.
I am happy you liked the series though 🙂
I really enjoyed your art of explanation by simplifying complex concepts and making them easier to understand. Please keep going bro!
It is great to know you liked it. Please stay tuned for the rest 🙂