preface

Android Architecture: Communication between ViewModel and View This paper introduces the Communication between VM and V in MVVM Architecture. Thanks to author Shashank Gupta. The level is limited, welcome correction discussion. MVVM Architecture has been a trend since Google announced Architecture Components at last year’s I/O conference. Many developers who were previously familiar with the MVP architecture are now beginning to embrace and use the MVVM architecture. Using a ViewModel has the following advantages over Presenter: much less boilerplate code, automatic recovery of data when configuration changes are made, and easy sharing of data between multiple fragments. However, communication between viewModels and views becomes more difficult.

The body of the

Pain points

In the case of a user information modification interface, it is the responsibility of Presenter or ViewModel to verify the user data before requesting the server. It is also the responsibility of Presenter or ViewModel to display and unload the user data, and to display the verification or server results on the interface. In addition, if a Dialog is being displayed, the Dialog should also be restored after configuration changes.

Presenters and ViewModels should not hold references to views.

In MVP architecture, we often need to define Contract interfaces.View implements contract. View, Presenter implements contract.presenter, Presenter does not hold a reference to an Activity/Fragment, but only a View instance, which makes it easy to call methods exposed by the View interface. For example EditProfileContract. Kt:

interface EditProfileContract {

    interface view {

        fun setProgress(show: Boolean)

        fun showEmptyFirstNameError()

        fun showEmptyLastNameError()
    }

    interface presenter {

        fun saveProfile(firstName: String, lastName: String, bio: String, email: String, city: City, gender: String)
    }
}
Copy the code

However, in the MVVM architecture, the ViewModel no longer holds a reference to the View, but instead exposes the data to the View layer through LiveData or RxJava. Once the View subscribes to the ViewModel, it starts receiving data updates. This seems perfect, but when the ViewModel wants to update the state of the View, such as displaying and unloading, and feeding data validation or server results back to the UI, it becomes very difficult.

The solution

The fewer LiveData or observables in the ViewModel, the better. So we had better find a way to encapsulate the data and information that needs to be passed to the View layer. In most cases, the ViewModel needs to expose three types of data to the View layer:

  • Data
  • Status
  • State is described below.

Data

Data — Is what needs to be displayed on the View, such as the User entity class for User information, or list items in the social Feed stream.

val user = MutableLiveData<User>()
val feeds = MutableLiveData<List<Feed>>()
Copy the code

Status

Status – This can be any information that is passed only once, such as a checksum error, a network exception, or a server error. Status. Kt:

enum class Status {
    SUCCESS,
    ERROR,
    NO_NETWORK,
    EMPTY_FIRST_NAME,
    EMPTY_LAST_NAME,
    EMPTY_CITY,
    INVALID_URI
}
Copy the code

LiveData doesn’t provide any out-of-the-box solutions, but there is an implementation of SingleLiveEvent in Google’s official example that solves this problem.

A life-cycle-aware observed that sends new updates only after subscribing, often used for events such as navigation and Snackbar messages. This avoids some common problems: During configuration changes (such as screen rotation), SingleLiveEvent will send update events if the observer is active. It inherits from MutableLiveData and is an observed, even if the SingleLiveEvent#setValue() or SingleLiveEvent#call() methods are exposed. Note that only one observer is notified of the update.

Create a New SingleLiveEvent to expose Status data to the View layer. EditProfileViewModel. Kt:

private val status = SingleLiveEvent<Status>()

fun getStatus(): LiveData<Status> {
    returnstatus } fun handleImage(intent: Intent?) { intent? .data? .let { avatar.value = it.toString() } ? : run { status.value = Status.INVALID_URI } }Copy the code

The View only cares about Status data and executes logic for different states or errors. In the following example, we can easily display a different Toast or Snackbar for each error. EditProfileFragment. Kt:

viewModel.getStatus().observe(this, Observer { handleStatus(it) })

private fun handleStatus(status: Status?) {
    when (status) {
        Status.EMPTY_FIRST_NAME -> Toast.makeText(activity, "Please enter your first name!", Toast.LENGTH_SHORT).show()
        Status.EMPTY_LAST_NAME -> Toast.makeText(activity, "Please enter your last name", Toast.LENGTH_SHORT).show()
        Status.EMPTY_CITY -> Toast.makeText(activity, "Please choose your home city", Toast.LENGTH_SHORT).show()
        Status.INVALID_URI -> Toast.makeText(activity, "Unable to load the photo", Toast.LENGTH_SHORT).show()
        Status.SUCCESS -> {
            startActivity(HomeFragment.newIntent(activity))
            activity.finish()
        }
        else -> Toast.makeText(activity, "Something went wrong, please try again!", Toast.LENGTH_SHORT).show()
    }
}
Copy the code

State

State — UI states, such as loading progress bars and dialogs, that the ViewModel should notify to the View layer each time it starts subscribes to ViewModel data. As an easy way to do this, we can create a data class to hold these states. EditProfileState. Kt:

data class EditProfileState(
    var isProgressIndicatorShown: Boolean = false,
    var isCityDialogShown: Boolean = false,
    var isGenderDialogShown: Boolean = false)
Copy the code

Then create a MutableLiveData in the ViewModel to wrap the EditProfileState. Since the ViewModel only exposes LiveData to the View layer, we should provide setter methods for the View to update this state. EditProfileViewModel. Kt:

private val state = MutableLiveData<EditProfileState>()

fun getState(): LiveData<EditProfileState> {
    return state
}

fun setProgressIndicator(isProgressIndicatorShown: Boolean) { state.value? .isProgressIndicatorShown = isProgressIndicatorShown } funsetCityDialogState(isCityDialogShown: Boolean) { state.value? .isCityDialogShown = isCityDialogShown } funsetGenderDialogState(isGenderDialogShown: Boolean) { state.value? .isGenderDialogShown = isGenderDialogShown }Copy the code

Finally, based on the State data above, determine the display and cancellation of the Dialog. EditProfileFragment. Kt:

viewModel.getState().observe(this, Observer { handleState(it) })

private fun handleState(state: EditProfileState?) {
    if(state? .isCityDialogShown ==true) {
        showCitySelectionDialog()
        return
    }
    if(state? .isGenderDialogShown ==true) {
        showGenderSelectionDialog()
        return}}Copy the code

conclusion

Encapsulating information such as loading state, UI state, or server errors keeps the ViewModel clean and concise. For me, Status and State are a good solution.

Questions in the comments

  • In fact, if you use enums, I don’t care. Go ahead

  • About using DataBinding:

    • The communication between VM and V can be resolved using DataBinding.

reference

  • Android Architecture: Communication between ViewModel and View
  • Official documentation
  • The price of ENUMs (100 Days of Google Dev)
  • Modern Android development: Android Jetpack, Kotlin, and more (Google I/O 2018)

contact

I’m Xiaobailong24, you can find me through the following platforms:

  • Making: github.com/xiaobailong…
  • Jane: www.jianshu.com/u/3dac2ad17…
  • The Denver nuggets: juejin. Cn/user / 310467…