8

I try to make sample login page with two fields (username, password) and save button with android architecture component, using android data binding, validating the data in viewmodel and from view model I make call to repository for remote server call as mentioned in official doc, remote server return me userid with success so how can I start new fragment from view model using this success? I learn something about singleLiveEvent and EventObserver, but I'm not able to find there clear usage example:

LoginViewModel

private MutableLiveData<String> snackbarStringSingleLiveEvent= new MutableLiveData<>();

@Inject
public LoginViewModel(@NonNull AppDatabase appDatabase, 
                      @NonNull JobPortalApplication application,
                      @NonNull MyApiEndpointInterface myApiEndpointInterface) {
    super(application);
    loginRepository = new LoginRepository(application, appDatabase, myApiEndpointInterface);
    snackbarStringSingleLiveEvent = loginRepository.getLogin(username.get(), password.get(), type.get());
}

public MutableLiveData<String> getSnackbarStringSingleLiveEvent() {
    return snackbarStringSingleLiveEvent;
}

Repository

public SingleLiveEvent<String> getLogin(String name, String password, String type) {
    SingleLiveEvent<String> mutableLiveData = new SingleLiveEvent<>();
    
    apiEndpointInterface.getlogin(name, password, type).enqueue(new Callback<GenericResponse>() {
        @Override
        public void onResponse(Call<GenericResponse> call, Response<GenericResponse> response) {
            mutableLiveData.setValue(response.body().getMessage());
        }

        @Override
        public void onFailure(Call<GenericResponse> responseCall, Throwable t) {
            mutableLiveData.setValue(Constant.FAILED);
        }
    });

    return mutableLiveData;
}

Login Fragment

private void observeViewModel(final LoginViewModel viewModel) {
    // Observe project data
    viewModel.getSnackbarStringSingleLiveEvent().observe(this, new Observer<String>() {
        @Override
        public void onChanged(String s) {
        }
    });
}

How can I use EventObserver in above case? Any practical example?

TooCool
  • 10,598
  • 15
  • 60
  • 85
K Guru
  • 1,292
  • 2
  • 17
  • 36
  • refer this blog, it may help https://medium.com/@abhishektiwari_51145/how-to-use-singleliveevent-in-mvvm-architecture-component-b7c04ed8705 – Abhishek Tiwari May 10 '19 at 06:41

3 Answers3

9

Check out below example about how you can create single LiveEvent to observe only one time as LiveData :

Create a class called Event as below that will provide our data once and acts as child of LiveData wrapper :

public class Event<T> {
    private boolean hasBeenHandled = false;
    private T content;

    public Event(T content) {
        this.content = content;
    }

    public T getContentIfNotHandled() {
        if (hasBeenHandled) {
            return null;
        } else {
            hasBeenHandled = true;
            return content;
        }
    }

    public boolean isHandled() {
        return hasBeenHandled;
    }
}

Then declare this EventObserver class like below so that we don't end up placing condition for checking about Event handled every time, everywhere :

public class EventObserver<T> implements Observer<Event<T>> {
    private OnEventChanged onEventChanged;

    public EventObserver(OnEventChanged onEventChanged) {
        this.onEventChanged = onEventChanged;
    }

    @Override
    public void onChanged(@Nullable Event<T> tEvent) {
        if (tEvent != null && tEvent.getContentIfNotHandled() != null && onEventChanged != null)
            onEventChanged.onUnhandledContent(tEvent.getContentIfNotHandled());
    }

    interface OnEventChanged<T> {
        void onUnhandledContent(T data);
    }
}

And How you can implement it :

MutableLiveData<Event<String>> data = new MutableLiveData<>();

// And observe like below
data.observe(lifecycleOwner, new EventObserver<String>(data -> {
        // your unhandled data would be here for one time.
    }));

// And this is how you add data as event to LiveData
data.setValue(new Event(""));

Refer here for details.


Edit for O.P.:

Yes, data.setValue(new Event("")); is meant for repository when you've got response from API (Remember to return same LiveData type you've taken in VM instead of SingleLiveEvent class though).

So, let's say you've created LiveData in ViewModel like below :

private MutableLiveData<Event<String>> snackbarStringSingleLiveEvent= new MutableLiveData<>();

You provide value to this livedata as Single Event from repository like below :

@Override
public void onResponse(Call<GenericResponse> call, Response<GenericResponse> response) {
    mutableLiveData.setValue(new Event(response.body().getMessage())); // we set it as Event wrapper class.
}

And observe it on UI (Fragment) like below :

viewModel.getSnackbarStringSingleLiveEvent().observe(this, new EventObserver<String>(data -> {
        // your unhandled data would be here for one time.
    }));
Jeel Vankhede
  • 11,592
  • 2
  • 28
  • 58
  • Thanks, but How I can observer my repository ?? the last portion of your code is for viewmodel or for UI layer??? – K Guru May 10 '19 at 11:54
  • actually, the repository portion is confusing, how can I observe repo with service ??? – K Guru May 10 '19 at 11:57
1

Event.java

public class Event<T> {

  private T content;

  private boolean hasBeenHandled = false;

  public Event(T content) {
    this.content = content;
  }

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

  /**
   * Returns the content, even if it's already been handled.
   */
  public T peekContent() {
      return content;
  }
}

EventObserver.java

public class EventObserver<T> implements Observer<Event<? extends T>> {

   public interface EventUnhandledContent<T> {
       void onEventUnhandledContent(T t);
   }

   private EventUnhandledContent<T> content;

   public EventObserver(EventUnhandledContent<T> content) {
       this.content = content;
   }

   @Override
   public void onChanged(Event<? extends T> event) {
       if (event != null) {
           T result = event.getContentIfNotHandled();
           if (result != null && content != null) {
               content.onEventUnhandledContent(result);
           }
       }
   }
}

Example, In ViewModel Class

public class LoginViewModel extends BaseViewModel {
   private MutableLiveData<Event<Boolean>> _isProgressEnabled = new MutableLiveData<>();
   LiveData<Event<Boolean>> isProgressEnabled = _isProgressEnabled;

   private AppService appService;

   private SchedulerProvider schedulerProvider;

   private SharedPreferences preferences;

  @Inject
  LoginViewModel(
          AppService appService,
          SchedulerProvider schedulerProvider,
          SharedPreferences preferences
  ) {
      this.appService = appService;
      this.schedulerProvider = schedulerProvider;
      this.preferences = preferences;
    }


   public void login(){
   appService.login("username", "password")
                          .subscribeOn(schedulerProvider.executorIo())
                          .observeOn(schedulerProvider.ui())
                          .subscribe(_userLoginDetails::setValue,
                                     _userLoginDetailsError::setValue,
                                     () -> _isProgressEnabled.setValue(new Event<>(false)),
                                     d -> _isProgressEnabled.setValue(new Event<>(true))
                          )
   }
}

In Login Fragment,

viewModel.isProgressEnabled.observe(this, new EventObserver<>(hasEnabled -> {
        if (hasEnabled) {
            // showProgress
        } else {
            // hideProgress
        }
    }));

Using Event and EventObserver class we can achieve the same like SingleLiveEvent class but if you are thinking a lot of boilerplate code just avoid this method. I hope it would help you and give some idea about why we are using SingleEvent in LiveData.

Santhosh
  • 160
  • 7
-1

I understand that Google gives the guidelines to use LiveData between the ViewModel and UI but there are edge cases where using LiveData as a SingleLiveEvent is like reinventing the wheel. For single time messaging between the view model and user interface we can use the delegate design pattern. When initializing the view model in the activity we just have to set the activity as the implementer of the interface. Then throughout our view model we can call the delegate method.

Interface

public interface Snackable:
   void showSnackbarMessage(String message);

UI

public class MyActivity extends AppCompatActivity implements Snackable {
    private MyViewModel myViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.my_layout);

        this.myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        this.myViewModel.setListener(this);
    }


    @Override
    public void showSnackbarMessage(String message) {
        Toast.makeText(this, "message", Toast.LENGTH_LONG).show();
    }
}

View Model

public class MyViewModel extends AndroidViewModel {

    private Snackable listener;

    public MyViewModel(@NonNull Application application) {
         super(application);
    }

    public void setListener(MyActivity activity){
         this.listener = activity;
    }

    private void sendSnackbarMessage(String message){
        if(listener != null){
            listener.showSnackbarMessage(message);
        }
    }

    private void anyFunctionInTheViewModel(){
        sendSnackbarMessage("Hey I've got a message for the UI!");
    }
}
Jordan Hochstetler
  • 1,308
  • 3
  • 17
  • 28
  • There are a lot of issues with this answer. You should definitely not define setListener(MyAcitivty activity) since then you explicitly just required a 1:1 mapping between the activity and viewmodel . You also need to unsubscribe to this as well or you will leak. If anything this should take a Snackable as a parameter. – Matt Wolfe Nov 03 '21 at 15:39