22

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?

DV82XL
  • 5,350
  • 5
  • 30
  • 59
user3182266
  • 1,270
  • 4
  • 23
  • 49

2 Answers2

29

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()
     }
 })
Rumit Patel
  • 8,830
  • 18
  • 51
  • 70
AlexM
  • 1,087
  • 14
  • 16
  • 6
    Brilliant solution. This takes into account when screen rotation and other lifecycle changes. – Haider Malik Jul 12 '20 at 13:19
  • 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? – Muhammad Yousuf Aug 01 '22 at 08:51
27

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
    }
})
Mitesh Vanaliya
  • 2,491
  • 24
  • 39
  • 1
    just a note for those not familiar with Kotlin syntax, that **safe call operator** `?.` will run whether the status is `true` or `false`. [doc](https://kotlinlang.org/docs/reference/null-safety.html#safe-calls) – lasec0203 Sep 24 '19 at 05:48
  • 18
    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. – devanshu_kaushik Nov 06 '19 at 09:43
  • @devanshu_kaushik 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. – Mikołaj May 18 '23 at 13:37
  • 1
    @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` wrapper with `LiveData` ( ` LiveData>` instead of `LiveData` [suggested by Google](https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150) seems like the best approach with clear responsibility segregation. – devanshu_kaushik May 19 '23 at 06:30