Android: MVVM is it possible to display a message (toast/snackbar etc.) from the ViewModel
Asked Answered
S

2

26

I want to know what is the best approach to display some sort of message in the view from the ViewModel. My ViewModel is making a POST call and "onResult" I want to pop up a message to the user containing a certain message.

This is my ViewModel:

public class RegisterViewModel extends ViewModel implements Observable {
.
.   
.
public void registerUser(PostUserRegDao postUserRegDao) {

    repository.executeRegistration(postUserRegDao).enqueue(new Callback<RegistratedUserDTO>() {
        @Override
        public void onResponse(Call<RegistratedUserDTO> call, Response<RegistratedUserDTO> response) {
            RegistratedUserDTO registratedUserDTO = response.body();
            /// here I want to set the message and send it to the Activity

            if (registratedUserDTO.getRegisterUserResultDTO().getError() != null) {

            }
        }

    });
}

And my Activity:

public class RegisterActivity extends BaseActivity {   

@Override
protected int layoutRes() {
    return R.layout.activity_register;
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);

    ActivityRegisterBinding binding = DataBindingUtil.setContentView(this, layoutRes());
    binding.setViewModel(mRegisterViewModel);       
}

What would the best approach be in this case?

Saxen answered 26/11, 2018 at 15:54 Comment(1)
You can use regular callback interface for this.Abram
G
29

Display Toast/snackbar message in view (Activity/Fragment) from viewmodel using LiveData.

Step:

  • Add LiveData into your viewmodel
  • View just observe LiveData and update view related task

For example:

In Viewmodel:

var status = MutableLiveData<Boolean?>()
//In your network successfull response
status.value = true

In your Activity or fragment:

yourViewModelObject.status.observe(this, Observer { status ->
    status?.let {
        //Reset status value at first to prevent multitriggering
        //and to be available to trigger action again
        yourViewModelObject.status.value = null
        //Display Toast or snackbar
    }
})
Genethlialogy answered 30/11, 2018 at 6:9 Comment(4)
just a note for those not familiar with Kotlin syntax, that safe call operator ?. will run whether the status is true or false. docSpermiogenesis
Not the ideal pattern - LiveData emmits the last value once the view (activity/fragment) is resumed from background, which means that the Toast or Snackbar will be shown again. I myself am looking for a good design using SingleLiveEvent.Splendent
@Splendent No, it will not. You reset status value just when showing Toast, so the last value will be null - how would it show a Toast again? There is null check for this case. You can also use Flow instead of LiveData to avoid this.Dressingdown
@Mikołaj I seem to have missed that - but I think this is still not the most ideal pattern - resetting status to null is a side-effect in this case. The Event<T> wrapper with LiveData ( ` LiveData<Event<T>>` instead of LiveData<T> suggested by Google seems like the best approach with clear responsibility segregation.Splendent
D
31

We can use a SingleLiveEvent class as a solution. But it is a LiveData that will only send an update once. In my personal experience, using an Event Wrapper class with MutableLiveData is the best solution.

Here is a simple code sample.

Step 1 : Create an Event class (this is a boilerplate code you can reuse for any android project).

open class Event<out T>(private val content: T) {

    var hasBeenHandled = false
        private set // Allow external read but not write

    /**
     * Returns the content and prevents its use again.
     */
    fun getContentIfNotHandled(): T? {
        return if (hasBeenHandled) {
            null
        } else {
            hasBeenHandled = true
            content
        }
    }

    /**
     * Returns the content, even if it's already been handled.
     */
    fun peekContent(): T = content
}

Step 2 : At the top of your View Model class, define a MutableLiveData with wrapper (I used a String here, but you can use your required data type), and a corresponding live data for encapsulation.

private val statusMessage = MutableLiveData<Event<String>>()

val message : LiveData<Event<String>>
  get() = statusMessage

Step 3 : You can update the status message within the functions of the ViewModel like this:

statusMessage.value = Event("User Updated Successfully")

Step 4 :

Write code to observe the live data from the View (activity or fragment)

 yourViewModel.message.observe(this, Observer {
     it.getContentIfNotHandled()?.let {
         Toast.makeText(this, it, Toast.LENGTH_LONG).show()
     }
 })
Decima answered 19/3, 2020 at 3:12 Comment(3)
Brilliant solution. This takes into account when screen rotation and other lifecycle changes.Rosmunda
The solution is good but if I have to show Toast on different screens then I'll have to add code to every viewModel and fragment. Is there any way to reduce this process?Polecat
What if instead View (activity or fragment) I'm using compose function?Longhorn
G
29

Display Toast/snackbar message in view (Activity/Fragment) from viewmodel using LiveData.

Step:

  • Add LiveData into your viewmodel
  • View just observe LiveData and update view related task

For example:

In Viewmodel:

var status = MutableLiveData<Boolean?>()
//In your network successfull response
status.value = true

In your Activity or fragment:

yourViewModelObject.status.observe(this, Observer { status ->
    status?.let {
        //Reset status value at first to prevent multitriggering
        //and to be available to trigger action again
        yourViewModelObject.status.value = null
        //Display Toast or snackbar
    }
})
Genethlialogy answered 30/11, 2018 at 6:9 Comment(4)
just a note for those not familiar with Kotlin syntax, that safe call operator ?. will run whether the status is true or false. docSpermiogenesis
Not the ideal pattern - LiveData emmits the last value once the view (activity/fragment) is resumed from background, which means that the Toast or Snackbar will be shown again. I myself am looking for a good design using SingleLiveEvent.Splendent
@Splendent No, it will not. You reset status value just when showing Toast, so the last value will be null - how would it show a Toast again? There is null check for this case. You can also use Flow instead of LiveData to avoid this.Dressingdown
@Mikołaj I seem to have missed that - but I think this is still not the most ideal pattern - resetting status to null is a side-effect in this case. The Event<T> wrapper with LiveData ( ` LiveData<Event<T>>` instead of LiveData<T> suggested by Google seems like the best approach with clear responsibility segregation.Splendent

© 2022 - 2024 — McMap. All rights reserved.