Opinionated Short List of Android ViewModel Do’s and Don’ts

Eric N
Coffee Meets Bagel Engineering
4 min readMay 31, 2021

Among the dozens of Android we’ve reviewed, the following misconceptions and sub-optimal practices tend to repeat a lot. I’m hoping the following pointers would help your next submission.

Disclaimer

“Premature optimization is the root of all evil”.

Assumptions are ok but we must make our best efforts to verify them. Mistakes are absolutely ok. Experiments are great. What’s important is we stay curious and dive deep to understand how things work.

Scope

In this article, I’ll focus on the primary use case of ViewModel for 90% of app screens: fetching and displaying some data. For secondary use cases such as handling button clicks or other user inputs, I believe that the same points hold true, but that deserves a separate article.

A) When to start fetching data?

Do: Use Kotlin’s init {} or Java’s constructor functions

Don’t: Create a start() or init() in the ViewModel and have the Activity/ Fragment explicitly call it

fun onCreate(savedInstanceState: Bundle?) {
viewModel.start() // You DON'T need this!
}

Why

Remember that MVVM (Android’s flavour or not) leverages the Observer design pattern!

The View and the ViewModel are very much de-coupled as opposed to View and Presenter in MVP. The View and the ViewModel agree to use a certain output stream (could be LiveData<T> or StateFlow<T> or whatever) at initialization time. The ViewModel can then simply emit results into that stream without worrying about when, how or whether at all the View consumes those results. So why wait? Let’s start fetching data immediately after the said output streams are initialized. What if the View has died or is in an undesirable state? It doesn’t matter, it’s not the ViewModel’s job to worry about that. That’s the beauty of the Observer pattern vs. callback methodologies in MVP.

Let me know in the comments if you’d like a marble diagram to visualize the application of the Observer pattern here.

B) (Background) work thread

Don’t: specify the work thread.

Do: Trust the repository to use the correct thread for background work.

Why

Because the repository knows best. For example, the I/O thread is most appropriate for network and local DB operations

C) viewModelScope default thread

Skip this if you don’t use Kotlin’s Coroutines.

Do: understand that viewModelScope uses the main thread (Dispatchers.Main) by default.

Don’t: assume that viewModelScope uses I/O thread or other non-main thread by default.

Why

Here are some reasons given by Manuel Vivo, A developer relations expert from Google’s own Android team.

My own reasoning goes as follows:

  1. Related to point B) above, it is best for the ViewModel not to do everything itself (for reasons of testability, reusability and maintainability). It should call other classes to do the work, then process the results in a very simplistic fashion. The current recommended way to keep track of the results is to use LiveData<T> streams which are meant to be used on the main thread! (More on this is point D, below)
  2. While there is only one main thread, how can we assume that viewModelScope would choose IO thread by default, instead of the Computation thread, or any new Thread? (I am using RxJava terminologies here, let me know if you want me to clarify.)

Updates Oct 2022

  1. I’ve recently learnt that coroutines are lightweight/ virtual thread, not classic Java threads
  2. Attempts to write to Room database using Dispatchers.Main cause app to crash with `java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.` whereas there is no error for Room database reads using Dispatchers.Main. The latter usually takes less than 16ms to complete but it can cause ANRs just like any other I/O operations.

D) To jump or not to jump LiveData thread

While LiveData<T> is thread safe and one can call postData() from any thread, I consider it a code smell.

Do: use setData() only

Don’t: use postData()

Why

postData() makes it hard to debug UI state change. Your UI changes could be caused by many different call-sites on many different threads 😢 (it gets worse when LiveData<T> stream is used outside of ViewModel (for example, like this infamous repository from the official docs).

Using setData() only (on the main thread) means there is only one place to look for what causes the UI state update.

Conclusion

ViewModel seems to be easy peasy at first 😄. But then nothing is ever as easy as it first appears 😅. It takes some trial and error, painful debugging, and debate to fine-tune the way you use any tool!

Do you agree with my list? I would love to see your list in the comment section too. Just make sure to back up your ideas with reasons 🙏.

We’re hiring

Coffee Meets Bagel is one of the 2 best companies I’ve worked for. Come join us!

https://jobs.lever.co/coffeemeetsbagel

Our culture is the best thing about us. Besides, we are very remote-friendly, even prior to the pandemic. Here’s our Glassdoor reviews, without my own super-biased 5 star review:

https://www.glassdoor.ca/Reviews/Coffee-Meets-Bagel-Reviews-E973161.htm

Further reading

You can read a longer list of recommendations by Google, 2 of which I strongly disagree with (can you tell which?) here:

--

--