I'm implementing the MVP design pattern. My presenter receives the new values from the view. I want to manage the state of a next button by automatically check if everything is valid when values are updated on the view.
In my form I have an optional part which is displayed only if the user select the correct option.
In this optional part I have a binary question. If the part is not displayed I need to set the value of the question to null on the Presenter side.
For example, the user select the option and the optional part is displayed. The user select the answer. Then the user change the option and the optional part is hidden. In that case I need to set the answer to the optional question to null, for the answer to not be already selected if the user display the optional part again.
To do so, I call a method on the Presenter with a null value instead of true/false.
Here is the code:
private final PublishSubject<Boolean> mObsOptionalAnswer = PublishSubject.create();
public MyPresenter(){
// Combine all the values together to enable/disable the next button
Observable.combineLatest(
// ... other fields
// I need this to return false if the optional part is
// displayed but nothing is selected
mObsOptionalAnswer.map(this::isValid),
(...) -> ...
).subscrible(enable ->{
mView.enableBtn(enable);
});
}
public void myFunction(Boolean isSomething){
// ... some code
mObsOptionalAnswer.onNext(isSomething);
}
private boolean isValid(Boolean value){
return value != null;
}
The problem is, since RxJava 2, null values are not allowed in the onNext() method.
So, how am I supposed to manage that?
If you want to be able to send a null value, you can use a wrapper. In this configuration, you send the wrapper, which isn't null even if the value itself is.
public class BooleanWrapper {
public final Boolean value;
public BooleanWrapper(Boolean value) {
this.value = value;
}
}
Your PublishSubject<Boolean> becomes a PublishSubject<BooleanWrapper> and you just have to create the wrapper and de-reference your Boolean when needed :
mObsOptionalAnswer.onNext(new BooleanWrapper(isSomething));
and
mObsOptionalAnswer.map(wrapper -> this.isValid(wrapper.value))
If you need to do that more than once in your code, you can create a generic wrapper (as described by this tutorial) :
public class Optional<M> {
private final M optional;
public Optional(#Nullable M optional) {
this.optional = optional;
}
public boolean isEmpty() {
return this.optional == null;
}
public M get() {
return optional;
}
}
you could use a constante Boolean object
public static final Boolean RESET_VALUE = new Boolean(false);
and you can emit this instead of emitting null. The receiver would have to check against this instance and behaving accordingly. Eg.
.subscrible(enable ->{
if (enable != RESET_VALUE) {
mView.enableBtn(enable);
}
});
Related
I have a two queries which return two long values. I am setting these two long values to be displayed in individual text views. Finally I have a third text view which displays the combined value of both longs. I am having a problem getting the combined total to show as its setting the value before the livedata is returned.
Below is a snippet of the code
private void getData() {
mViewModelReframing.totalWorkouts(pkUserId).observe(getViewLifecycleOwner(), new Observer<List<ModelStatsTotalWorkouts>>() {
#Override
public void onChanged(List<ModelStatsTotalWorkouts> modelStatsTotalWorkouts) {
for (ModelStatsTotalWorkouts list : modelStatsTotalWorkouts) {
totalReframeWorkouts = list.getTotalWorkouts();
}
if (totalReframeWorkouts == 0) {
tvTotalReframes.setText(0 + getString(R.string.workouts_empty));
} else {
tvTotalReframes.setText("" + totalReframeWorkouts);
}
}
});
mViewModelCheckIn.totalWorkouts(pkUserId).observe(getViewLifecycleOwner(), new Observer<List<ModelStatsTotalWorkouts>>() {
#Override
public void onChanged(List<ModelStatsTotalWorkouts> tableCheckIns) {
for (ModelStatsTotalWorkouts list : tableCheckIns) {
totalCheckInWorkouts = list.getTotalWorkouts();
}
tvTotalCheckIns.setText("" + totalCheckInWorkouts);
// Combine both longs together for a combined total.
totalWorkouts = totalReframeWorkouts + totalCheckInWorkouts;
tvTotalWorkouts.setText("" + totalWorkouts);
}
});
}
Is there a better way to write the logic to achieve the desired result without the issue of the livedata not being returned fast enough?
Whenever you use independent Reactive streams like this (LiveData, RxJava, etc) you are going to have race conditions. You need to make explicit the dependencies for an action to happen - in this case your ability to update the UI in the way that you want had dependencies on BOTH APIs returning. This is the RxJava equivalent of zip. A few tips:
Consider using only a single Viewmodel for a view. The viewmodel should really be preparing data for your view specificially. In this case, it should really be that singular ViewModel that handles combining this data before passing it to your vew at all.
Barring that, since you've chosen LiveData here, you can do what you want by using a MediatorLiveData. Essentially, it acts as a composite stream source that depends on whichever other LiveData streams you add to it as described by that article. In this way, you can explicitly wait for all the needed values to arrive before you try to update the UI.
I solved the question by using this method:
public LiveData<List<ModelStatsTotalWorkouts>> totalWorkoutsCombined(long userId) {
LiveData liveData1 = database.getUsersDao().totalReframeWorkouts(userId);
LiveData liveData2 = database.getUsersDao().totalCheckInWorkouts(userId);
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
return liveDataMerger;
}
I have Boolean booleanCheckAvailabilityData to check availability data in my activity to create add/remove favorite. then i create
dataFavoriteMovieById = favoriteMovieViewModel.getAllFavoriteMovieById(idMovie);
to get data by id. so i make conditional statement to check avaiability data then put the result to boolean and i use the boolean later to add or remove the favorite.
if (dataFavoriteMovieById == null) {
booleanCheckAvailabilityData = false;
} else {
booleanCheckAvailabilityData = true;
}
In the first run, it work. my dataFavoriteMovieById is null
But, after i add or remove favorite. it always always contains data (RoomTrackingLiveData).
How can i solve this...
my code link : https://github.com/komangss/Submission-Menjadi-Android-Developer-Expert/blob/master/app/src/main/java/com/dicoding/submissionmade2_1/activity/DetailMovieActivity.java
I played with your app (thanks for providing a github link) and here are my results.
Latest app version
Your latest implementation doesn't produce an NPE anymore since you use getAllFavoriteMovieById in a more consistent way. You no longer initialize a LiveData instance in FavoriteMovieRepository by yourself but delegate it to Room to do it for you. So, you won't get an NPE since Room will always create a list to return results. If there're no items, it will return an empty list. So, you can safely remove a try/catch here:
try {
favoriteMovieViewModel.getAllFavoriteMovieById(idMovie).observe(this, new Observer<List<FavoriteMovie>>() {
#Override
public void onChanged(List<FavoriteMovie> favoriteMovies) {
booleanCheckAvailabilityData = favoriteMovies.size() != 0;
}
});
} catch (NullPointerException e) {
Log.d("ini bug nya", e.getMessage());
}
Original app version
In addition to what #Paul Ost said about how favoriteMovieViewModel should be used properly (by listening to it, not using it directly), I will explain why you actually had an NPE.
In that version, you were running into a NullPointerException because you returned the favoriteMovieById LiveData before it was actually initialized in your GetFavoriteMovieByIdAsyncTask.
So, here what was happening in detail. First, once your DetailMovieActivity had been created, favoriteMovieViewModel called getAllFavoriteMovieById() as below:
DetailMovieActivity.java
...
favoriteMovieViewModel = ViewModelProviders.of(this).get(FavoriteMovieViewModel.class);
dataFavoriteMovieById = favoriteMovieViewModel.getAllFavoriteMovieById(idMovie);
...
FavoriteMovieViewModel.java
FavoriteMovieViewModel instance, in turn, delegated the call to FavoriteMovieRepository instance as below:
public LiveData<List<FavoriteMovie>> getAllFavoriteMovieById(int idMovie) {
return repository.getFavoriteMovieById(idMovie);
}
FavoriteMovieRepository.java
Finally, getFavoriteMovieById started a GetFavoriteMovieByIdAsyncTask and returned favoriteMovieById:
public LiveData<List<FavoriteMovie>> getFavoriteMovieById(int id_movie) {
new GetFavoriteMovieByIdAsyncTask(favoriteMovieDao).execute(id_movie);
return favoriteMovieById;
}
But that's wrong, since your favoriteMovieById was set to null by default, and so on the first run, you were always getting it.
Your AsyncTask was eventually setting a non-null value, but it was too late:
...
private static class GetFavoriteMovieByIdAsyncTask extends AsyncTask<Integer, Void, Void> {
...
#Override
protected Void doInBackground(Integer... integers) {
FavoriteMovieRepository.favoriteMovieById = favoriteMovieDao.getFavoriteMovieById(integers[0]);
return null;
}
}
...
From what I can see in your code - getAllFavoriteMovieById works as expected. The thing is - you are using LiveData as a return type of getAllFavoriteMovieById thus it returns not the value itself but a LiveData wrapper. But if you will try to observe this LiveData object you will(presumably since I haven't seen relevant code) receive null instead of favourite value. The only correct place to assign value to your booleanCheckAvailabilityData inside this observer(depending on your DAO code of course).
favouriteMovieViewModel.getAllFavoriteMovieById().observe(this, Observer { data ->
if (data == null) {
booleanCheckAvailabilityData = false;
} else {
booleanCheckAvailabilityData = true;
}
})
Something like that(once again it depends on your DAO code and getAllFavoriteMovieById implementation)
Hope it helps.
In ROOM Try Deleting the old TABLE before inserting the new data. In that case the old data will be deleted as we are deleting the old data
How to use sharedPreferences inside observable, as I know take value outside observable is a bad practise.
I have something like this:
class City {
private String tempCelsius;
private String tempKelvin;
// other fields
public getTempCelsius() {
return tempCelsius;
}
public getTempKelvin() {
return tempKelvin;
}
}
In Presenter I have:
public Single<City> getData() {
return dataManager.loadCitiesFromDb().
map(city -> dataManager.makeApiCall(city)
//getting data, error handling
)
.subscribe(city -> view.showCityData(city));
}
By the way my DataManager contains instance of PreferenceHelper.
How can I showCityData with temperature based on value from SharedPreferences.
If Kelvin use City's method getTempKelvin(), else if Celsius use City's method getTempCelsius().
From Api I received both, but need to show only once according to saved preferences.
I have created custom button, it has inner checked listener and I expose it as an observable with the help of a PublishSubject.
public class SwitchItem extends LinearLayout implements SwitchButton.OnCheckedChangeListener {
//other fields ...
private PublishSubject<Boolean> checkedObservable = PublishSubject.create();
//I call this method in each
private void initViews(String itemTitle, String itemStatus, boolean isChecked) {
//other initializations
if (switchButton != null) {
switchButton.setOnCheckedChangeListener(this);
}
}
#Override
public void onCheckedChanged(SwitchButton view, boolean isChecked) {
defaultItemStatusText(isChecked);
checkedObservable.onNext(isChecked);
}
//to get the observable
public Observable<Boolean> getCheckedObservable() {
return checkedObservable;
}
}
in th UI I have multiple buttons of this type, I get all of their observables and use CompineLatest to listen on any change, what I try to achieve is to enable a setting save buttton if anyone of the buttons changed its state.
Observable.combineLatest(button1, button2,button3,(cheked1, cheked2, cheked3) -> {
boolean isSettingsChanged = false;
isSettingsChanged = checked1 != inital1 || checked2 != inital2 || checked3 != inital3;
return isSettingsChanged;
}).subscribe(enable -> getView().enableSave(enable));
the problem that I have to change all of the buttons states so compineLatest start to fire its logic. I tried to use setChecked() to give them an initial values, but it doesn't work, it always wait to fire a specific button so it start to work. I don't know where the problem is.
Here's how I would approach the problem:
Observable.merge(button1.distinct(), button2.distinct(), button3.distinct())
.subscribe(ignore -> getView().enableSave(true));
By using .distinct(), you ensure the button only emits when there is a change. If you need to emit when there is a change and matches a certain value, then you'd be looking at .distinct().filter(<Predicate>).
Hope this helps
I have a number of EditText in an Activity. On clicking the submit button, I want to validate them, and prevent submission if there are errors in those EditText objects. I don't want to use TextWatcher because I don't want the methods to get fired at every single change. It does not make sense for an overall validation before submission. Is there a method that lets us loop through an array of the controls of the form? Thanks.
You have two options:
1) Create a Utils class with static methods for ensuring that the fields are valid.
i.e. toy example for checking email
public class Utils{
public static boolean isValidEmail(String str){
return str.contains("#");
}
}
and do so for checking the various fields (phone #, email, name, etc...). In your Activity that has the EditText(s), when you try to submit them, have something like:
public boolean validateFields(){
boolean result = true;
if(!Utils.isValidEmail(mEmailEdit.getText().toString()){
mEmailEdit.setError("Invalid email!");
result = false;
}
if(!Utils.isValidName(mEmailEdit.getText().toString()){
mNameEdit.setError("Invalid name!");
result = false;
}
return result;
}
This is a very simple idea of what you would do. Call validateFields() when clicking the submit button, and if the validateFields() method returns false, then do not proceed with the fields. If it returns true, well then all fields are valid and call another method to submit the data to w/e you are using it for.
2) (Best option for larger projects) Create an interface, call it Validator with a boolean-return function called validate(). This validator interface is extended for each various validation you wish to do, and you create a new interface like so:
public interface Validator{
public boolean validate(String s);
}
public interface EmailValidator extends Validator{
#Override
public boolean validate(String s){
return s.contains("#");
}
}
And extend a new EditText class view that has a Validator interface field, with a getter/setter. Then, in the validateFields() method, we do the same thing except call each EditText's validation interface's validate() method. There are a few more subtleties for this and I can type this all out if interested on how to do exactly. Let me know if that helps
The most straight forward way to do this is to get references to each of your sub views after you create the main view via setContentView(..). Use findViewById() to get references to each of them.
Then in your submit button click handler grab the inputs from each of them via something like nameField.getText() and do whatever validation you want. And if it fails show the error to the user in some fashion.
So, taking ideas from Lucas, creating custom components such as a DateEditText extending EditText and implementing a Validator interface, in my activity button for update onclick, I call this method that I wrote "isValidViewGroup(ViewGroup viewGroup), it will recursively go through all views, starting with the given viewGroup, and check children views, until it meets one implementing Validator and then call its isValid() method. It stops as soon as it finds an invalid one, or go through the end of views. Here's the method:
...
private boolean isValidViewGroup(ViewGroup viewGroup) {
int viewGroupChildCount = viewGroup.getChildCount();
View view = null;
for (int i = 0; i < viewGroupChildCount;i++) {
view = viewGroup.getChildAt(i);
if (view instanceof ViewGroup){
if (!isValidViewGroup((ViewGroup) view)){
return false;
}
} else {
if (view instanceof Validator){
if (!((Validator)view).isValid()){
return false;
}
}
}
}
return true;
}
...
I'm sure there could be a better, more efficient way, but for the moment that works really fine.