Using Architecture Component to realize the correct posture of MVVM

Posted by bo0 on Thu, 09 Dec 2021 07:37:10 +0100

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

  1. 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)
  2. 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
  3. 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

  1. 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
  2. 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
  3. 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

Exploration and attempt of LiveData non sticky message

Customize life cycle and realize life cycle awareness