Reputation: 10672
I understand that the purpose of Android's data-binding library is for views to observe data and automatically update when that data changes.
Question: Is it possible for data to observe other data? For example, can I have one ObservableField
"depend on" or "bind to" the value of another or a set of other ObservableField
s? Currently, I have implemented this manually - every time any of the "dependee" ObservableField
s change, I compute the dependent field and update its value.
My use-case is I want all "logic" to be outside the View - so I want to put all my logic in the "data" class (ViewModel
, if I may). I have a button whose state I want to set to enabled/disabled depending on the contents of several other fields. This example illustrates what I have in mind.
My layout file looks like this
<layout>
<data>
<variable name="register" class="com.example.RegisterViewModel"/>
</data>
<LinearLayout>
<EditText
android:id="@+id/edUsername"
android:text="@{register.username}"/>
<EditText android:id="@+id/edPassword" />
<EditText android:id="@+id/edConfirm" />
<Button android:text="Register" android:enabled="@{register.isValid}" />
</LinearLayout>
</layout>
And, my View code is as follows:
class RegisterView extends View {
@Override
protected void onFinishInflate() {
RegisterViewBinding binding = DataBindingUtil.bind(this);
RegisterViewModel register = new RegisterViewModel();
binding.setRegister(register);
binding.edPassword.setOnFocusChangeListener(new OnFocusChangeListener(){
@Override public void onFocusChange(View v, boolean hasFocus){
register.updateUsername(edPassword.getText().toString());
}
});
//Similarly for other fields.
}
}
Here is my ViewModel
class RegisterViewModel {
public final ObservableField<String> username = new ObservableField<>();
private final ObservableField<String> password = new ObservableField<>();
private final ObservableField<String> confirmPassword = new ObservableField<>();
public final ObservableBoolean isValid;
//Dependee Observables - isValid depends on all of these
private final ObservableBoolean isUsernameValid = new ObservableBoolean();
private final ObservableBoolean isPasswordValid = new ObservableBoolean();
private final ObservableBoolean arePasswordsSame = new ObservableBoolean();
public RegisterViewModel(){
//Can this binding be made observable so that isValid automatically
//updates whenever isUsernameValid/isPasswordValid/arePasswordsSame change?
isValid = new ObservableBoolean(isUsernameValid.get() &&
isPasswordValid.get() &&
arePasswordsSame.get());
}
public void updateUsername(String name) {
username.set(name);
isUsernameValid.set(ValidationUtils.validateUsername(name));
updateDependents();
}
public void updatePassword(String pwd) {
password.set(pwd);
isPasswordValid.set(ValidationUtils.validatePassword(pwd));
updateDependents();
}
public void updateConfirmPassword(String cnf) {
confirmPassword.set(cnf);
arePasswordsSame.set(password.get().equalsIgnoreCase(cnf.get()));
updateDependents();
}
//Looking to avoid this altogether
private void updateDependents() {
isValid.set(isUsernameValid.get() &&
isPasswordValid.get() &&
arePasswordsSame.get());
}
}
Upvotes: 16
Views: 8808
Reputation: 13402
I would suggest rethinking your approach. One of the primary benefits of data-binding is allowing for more expressive view code (XML in this case). While there is a balance between how much work you actually want to do in the XML vs in the view model, your case is a perfect example of too much work being done in the view model. In your code, it is not the observable fields that depend on other fields but a view's data that depends on other views' data. The observable field is just a representation of that data and when possible you should look to create dependencies in the view layer, rather than in the data layer.
The approach I would suggest is to start with the view layer (the XML) and assume you have no holistic view model but only data attached to the views. For e.g. you could start with something like this:
<layout>
<LinearLayout>
<EditText android:text="@{username}"/>
<EditText text="@{password}" />
<EditText text="@{confirmPassword}" />
<Button android:text="Register" android:enabled="@{password.equals(confirmPassword) && ...password validation...}" />
</LinearLayout>
</layout>
After this first step, you will quickly realize that the password validation logic does not make sense here because it's not trivial so you would go:
<layout>
<import "com.example.ValidationUtils"/>
<LinearLayout>
<EditText android:text="@{username}"/>
<EditText text="@{password}" />
<EditText text="@{confirmPassword}" />
<Button android:text="Register" android:enabled="@{password.equals(confirmPassword) && ValidationUtils.validatePassword(password)}" />
</LinearLayout>
At this point, you just need a container for the username, password and confirmPassword fields so you can pass them on, so you just add the viewModel variable.
<layout>
<import "com.example.ValidationUtils"/>
<variable name="viewModel" type="com.example.Register"/>
<LinearLayout>
<EditText android:text="@{viewModel.username}"/>
<EditText text="@{viewModel.password}" />
<EditText text="@{viewModel.confirmPassword}" />
<Button android:text="Register" android:enabled="@{password.equals(confirmPassword) && ValidationUtils.validatePassword(password)}" />
</LinearLayout>
Notice how much nicer that is and you didn't even need to see the Java code at all.
P.S: you could also replace the enabled expression to something like ValidationUtils.validateEntries(username, password, confirmPassword)
if you wished. That is more a stylistic choice; it does not affect the expressivity of the view code much and anyone reading the XML can figure out what's the view is trying to achieve without looking in multiple places.
Upvotes: 5
Reputation: 142
Use @Bindable annotation and the Observable interface. It avoids the boilerplate code and u can use it for primitive data types.
/**
* Created by Amardeep on 11/2/16.
*/
public class Cart implements Observable {
private final PropertyChangeRegistry mPropertyChangeRegistry =
new PropertyChangeRegistry();
@Bindable
private int itemCount;
@Bindable
private String name;
public int getItemCount() {
return itemCount;
}
public void setItemCount(int itemCount) {
this.itemCount = itemCount;
mPropertyChangeRegistry.notifyChange(this, BR.itemCount);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
mPropertyChangeRegistry.notifyChange(this, BR.name);
}
@Override
public void addOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
mPropertyChangeRegistry.add(callback);
}
@Override
public void removeOnPropertyChangedCallback(OnPropertyChangedCallback callback) {
mPropertyChangeRegistry.remove(callback);
}
}
Upvotes: 1
Reputation: 20926
It is not possible to data bind two ObservableField
s using binding syntax in Android data binding. However, you can bind them with code:
class RegisterViewModel {
public final ObservableField<String> username = new ObservableField<>();
public final ObservableField<String> password = new ObservableField<>();
public final ObservableField<String> confirmPassword = new ObservableField<>();
public final ObservableBoolean isValid = new ObservableBoolean();
private boolean isUsernameValid;
private boolean isPasswordValid;
private boolean arePasswordsSame;
public RegisterViewModel() {
// You can use 3 different callbacks, but I'll use just one here
// with 'if' statements -- it will save allocating 2 Object.
OnPropertyChangedCallback callback = new OnPropertyChangedCallback() {
@Override
public void onPropertyChanged(Observable sender, int propertyId) {
if (sender == username) {
isUsernameValid = ValidationUtils.validateUsername(name);
} else if (sender == password) {
isPasswordValid = ValidationUtils.validatePassword(pwd);
} else if (sender == confirmPassword) {
arePasswordsSame = password.get()
.equalsIgnoreCase(confirmPassword.get());
} else {
// shouldn't get here...
}
isValid.set(isUsernameValid && isPasswordValid && arePasswordsSame);
}
};
username.addOnPropertyChangedCallback(callback);
password.addOnPropertyChangedCallback(callback);
confirmPassword.addOnPropertyChangedCallback(callback);
}
}
Here, I've assumed that empty username, password, and confirmPassword are invalid. Seemed a safe assumption.
I don't see a tremendous need for private ObservableField
s. ObservableField
was designed to be bound to by the UI and if you can't, you can use other data types. If you find them useful for internal binding using callbacks like the above, then go for it.
Upvotes: 21