1, Initial experience of MVVM
There are many online introductions about MVVM. I won't repeat it here. Just look at an example and feel what it's like to develop with MVVM with intuitive code
class MvvmViewModel : ViewModel() { private val _billLiveData = MutableLiveData<Int>(0) val billLiveData: LiveData<Int> get() = _billLiveData fun pay() { _billLiveData.value = _billLiveData.value?.inc() } } Copy code
class MvvmActivity : AppCompatActivity() { private val viewModel by viewModels<MvvmViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_mvvm) btn.setOnClickListener { viewModel.pay() } viewModel.billLiveData.observe(this, Observer { Log.d("sample", "I got it $it") }) } } Copy code
A very simple example: a log is output every time the count is added by one.
Think about how to implement it in the conventional way: add Callback? Or use EventBus? Here, a simple MVVM is implemented by using LiveData and ViewModel: the View can be automatically notified of data changes, then corresponding UI changes can be made, and the monitoring can be stopped before the end of the Activity life cycle
2, Understanding Architecture Components
Must MVVM use LiveData and ViewModel? The answer is definitely no, but Android provides me with a set of components, which can make it easier for us to develop with MVVM mode and reduce the workload of building wheels repeatedly
ViewModel
ViewModel It is an officially provided component for managing UI related data and has life cycle awareness. It can save data without loss when the Activity state changes, such as screen rotation. In MVVM, ViewModel plays the role of data transfer and logical processing before View and Model

Lifecycle of ViewModel
The lifecycle of the ViewModel is related to the lifecycle owner passed in when the ViewModel instance is created
// this is the lifecycle owner, which can be Activity or Fragment val viewModel = ViewModelProvider(this).get(MvvmViewModel::class.java) // Or if activity KTX or fragment KTX is introduced, it can also be used as follows: class MvvmActivity : AppCompatActivity() { private val viewModel by viewModels<MvvmViewModel>() } Copy code
The relationship between ViewModel and its bound lifecycle owner lifecycle is shown in the following figure

It can be seen that in the scene of screen rotation, the life cycle of ViewModel is longer than that of Activity, so we should not hold View or any class referenced to Activity context in ViewModel, otherwise there will be a risk of memory leakage
ViewModel instantiation parameter
Sometimes we want to pass parameters to the created ViewModel. Here are some scenarios:
Considerations for using ViewModel in Fragment or Activity
When obtaining a ViewModel through the ViewModelProvider, it must be used after Activity onCreate or Fragment onAttach, otherwise an IllegalStateException will be reported
/** * Returns the {@link ViewModelStore} associated with this activity * <p> * Overriding this method is no longer supported and this method will be made * <code>final</code> in a future version of ComponentActivity. * * @return a {@code ViewModelStore} * @throws IllegalStateException if called before the Activity is attached to the Application * instance i.e., before onCreate() */ @NonNull @Override public ViewModelStore getViewModelStore() { if (getApplication() == null) { throw new IllegalStateException("Your activity is not yet attached to the " + "Application instance. You can't request ViewModel before onCreate call."); } if (mViewModelStore == null) { NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance(); if (nc != null) { // Restore the ViewModelStore from NonConfigurationInstances mViewModelStore = nc.viewModelStore; } if (mViewModelStore == null) { mViewModelStore = new ViewModelStore(); } } return mViewModelStore; } Copy code
/** * Returns the {@link ViewModelStore} associated with this Fragment * <p> * Overriding this method is no longer supported and this method will be made * <code>final</code> in a future version of Fragment. * * @return a {@code ViewModelStore} * @throws IllegalStateException if called before the Fragment is attached i.e., before * onAttach(). */ @NonNull @Override public ViewModelStore getViewModelStore() { if (mFragmentManager == null) { throw new IllegalStateException("Can't access ViewModels from detached fragment"); } return mFragmentManager.getViewModelStore(this); } Copy code
You can use the extension function provided by fragment KTX or activity KTX to lazy load the ViewModel:
@MainThread public inline fun <reified VM : ViewModel> ComponentActivity.viewModels( noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) } @MainThread public inline fun <reified VM : ViewModel> Fragment.viewModels( noinline factoryProducer: (() -> ViewModelProvider.Factory)? = null ): Lazy<VM> { val factoryPromise = factoryProducer ?: { defaultViewModelProviderFactory } return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise) } Copy code
When using:
// Lazy loading can only be initialized when it is used, but it should still be in activity Oncreate() or // Fragment. Use after onattach() private val viewModel: MyViewModel by viewModels<MyViewModel>() Copy code
LiveData
LiveData It is also an officially provided component that provides data that can be monitored and has life cycle awareness. This perception enables LiveData to update the UI only when the listener is in the active state, and unbind the monitoring relationship when the life cycle is destroyed.
Sticky message characteristics and applicable scenarios
LiveData supports sticky messages by default, that is, when observing (), you can get the value assigned to LiveData before observing (). Therefore, pay special attention to this when using LiveData, otherwise some unexpected problems may arise. For details, please move to another article: Correct use of LiveData, posture and anti pattern
Implementation of non sticky message
It has been mentioned on the Internet and on the official blog that if you want to use LiveData to implement non sticky messages (the value previously assigned to LiveData is not received when observing ()), there are various ways of workaround. For details, please move to another article: Exploration and attempt of LiveData non sticky message
LiveData transformation and combination
Sometimes we want to do some transformation or other processing on LiveData and then provide it to the View layer. We can use Transforms
One to one static Transformation -- map

In the example above, we convert the DataLayerModel passed from Repo to UiModel at ViewModel and then provide it to View. This is a very common mode. Sometimes the data of Repo layer is very complex, and View only cares about the data related to UI, such as the following code
object Repo { private val _userData = MutableLiveData<User>() val userData: LiveData<User> = _userData fun fetchData() { // The simulation delays two seconds to return data Thread.sleep(2000) _userData.value = User(1234, "joe") } } data class User(val id: Int, val name: String) Copy code
class MvvmViewModel : ViewModel() { // The UI layer doesn't care about the user's id. it just needs to display the user name. Here is a map conversion val uiModel: LiveData<String> = Transformations.map(Repo.userData, Function { user -> user.name }) fun fetchData() { Repo.fetchData() } } Copy code
class MvvmActivity : AppCompatActivity() { private val viewModel by viewModels<MvvmViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_mvvm) viewModel.uiModel.observe(this, Observer { Log.d("sample", it) }) btn.setOnClickListener { viewModel.fetchData() } } } Copy code
One to one dynamic transformation -- switchMap

This is another scenario. Sometimes we want to use this data to obtain another data after a data change, and then the View layer monitors the change of the last data. For example, we want to obtain the book information under the user name after obtaining the user id
object UserRepo { private val _userData = MutableLiveData<User>() val userData: LiveData<User> = _userData fun fetchData() { // The simulation delays two seconds to return data Thread.sleep(2000) _userData.value = User(1234, "joe") } } data class User(val id: Int, val name: String) Copy code
object BookRepo { fun getBooksOfUser(id: Int): LiveData<Book> { // The simulation delays two seconds to return data Thread.sleep(2000) return MutableLiveData<Book>(Book("How is steel made")) } } data class Book(val name: String) Copy code
class MvvmViewModel : ViewModel() { // Here, the user is monitored for changes. After the user is updated, go to BookRepo to obtain book information and return a LiveData val uiModel: LiveData<Book> = Transformations.switchMap(UserRepo.userData, Function { user -> BookRepo.getBooksOfUser(user.id) }) fun fetchData() { UserRepo.fetchData() } } Copy code
class MvvmActivity : AppCompatActivity() { private val viewModel by viewModels<MvvmViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_mvvm) // UI monitors changes in book information viewModel.uiModel.observe(this, Observer { Log.d("sample", it.name) }) btn.setOnClickListener { viewModel.fetchData() } } } Copy code
Another advantage of switchMap is that the life cycle of the observer can be transferred to the new LiveData, that is, when the life cycle of the observer is destroyed, the listening relationship between the two LiveData will be disconnected, which can effectively prevent the leakage of the ViewModel in the scenario where the ViewModel and Repo also communicate with LiveData
One to many conversion -- MediatorLiveData
Transforms' map() and switchMap() actually use an encapsulated class called MediatorLiveData internally. Using this class, multiple LiveData can be combined to realize the function of monitoring multiple LiveData changes

For example, in the figure above, our Repo generally has remote data and local cache, which is a very common scenario. At this time, we can use MediatorLiveData to monitor local and remote data changes
class MvvmViewModel : ViewModel() { private val local = LocalRepo.userData private val remote = RemoteRepo.userData // Jointly monitor changes in local and remote data private val _result = MediatorLiveData<String>().apply { addSource(local, Observer { // If the local cache data is not empty, use the local cache if (it != null) { this.value = it } }) addSource(remote, Observer { // Once the network data is obtained, use the network data this.value = it }) } val result: LiveData<String> = _result } Copy code
class MvvmActivity : AppCompatActivity() { private val viewModel by viewModels<MvvmViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_mvvm) // UI data change viewModel.result.observe(this, Observer { Log.d("sample", it) }) } } Copy code
3, Using Architecture Components to realize the correct posture of MVVM
Refer to official blog: Modes and anti modes of ViewModel and LiveData

Responsibility boundary between layers
- The LiveData in the ViewModel is provided to the View for listening. The View should only need the data related to the relational UI. The data obtained from the Repo (which may need to be processed) should be provided to the View after some processing. This processing process should be placed in the ViewModel (Transforms can be used)
- The View layer should not have too much logic code. The logic code should be handled in the ViewModel, and then notify the View to update the UI directly. The View only needs to relate how to update the UI and send the user's interaction events to the ViewModel. This mode is called Passive View
- Ideally, there should be no Android framework related code in the ViewModel, which will be more friendly for testability (no need to mock Android related code)
Pay attention to memory leakage
- View cannot be held in ViewModel. On the one hand, memory leakage is prevented, and on the other hand, this design is beneficial to write single test; If you need to use Context in ViewModel, you can use Android ViewModel
- The LifecycleOwner passed to LiveData must comply with the life cycle of the observer. If the life cycle of the LifecycleOwner is longer than that of the observer, it is easy to cause memory leakage. See the following for details: Correct use of LiveData, posture and anti pattern
- For the communication between ViewModel and Repo, pay attention to de registering when appropriate. If you use LiveData, because the ViewModel itself does not have a life cycle, you can consider using Transforms + LiveData; If you use other methods to register listeners, you can cancel listening / releasing resources in the onCleared() method of ViewModel
Communication mode between layers
Use Transforms to use LiveData between ViewModel and Model

Pay attention to the memory leakage of ViewModel when using LiveData. You can use observeForever and removeObserver to manually manage listening and cancel listening, and you can use Transforms to avoid memory leakage
class MvvmViewModel : ViewModel() { // After the View cancels listening to the uiModel, the listening between the ViewModel and UserRepo/BookRepo will also be disconnected val uiModel: LiveData<Book> = Transformations.switchMap( UserRepo.userData, Function { user -> BookRepo.getBooksOfUser(user.id) }) fun fetchData() { UserRepo.fetchData() } } Copy code
Related articles:
Correct use of LiveData, posture and anti pattern