Text Watcher Binding Adapter with ViewModel - android

I'm still new to all this MVVM and Android Architecture Components. I have a few screens (Login and Register) that have inputs for email, password, name etc and a 'continue' button that's only enabled when the required fields are filled out correctly. There's also text views for messages such as "password must be..." and "not a valid email...". I'm trying to use a binding adapter and viewmodel to pull some of this validation logic out of my MainActivity and away from my business logic, but I'm struggling. I'm not even sure if this is the best way to do it. My thought with the ViewModel is that it would hold it's state on rotation/activity changes.
RegisterActivity is an Activity that simply holds a ViewPager with 3 fragments (email/password, first/last name, validation code).
Binding Adapter
#BindingAdapter("app:onTextChanged")
public static void onTextChanged(TextInputEditText view, TextViewBindingAdapter.OnTextChanged listener) {
}
Layout
<TextView
android:id="#+id/tv_error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="#string/register_email_requirement"
android:textColor="#color/moenPrimaryError"
android:textSize="18sp"
android:visibility="#{viewModel.emailValidationVisible ? View.VISIBLE: View.GONE}"
app:layout_constraintBottom_toTopOf="#id/input_layout_password"
app:layout_constraintEnd_toEndOf="#+id/input_layout_email_address"
app:layout_constraintStart_toStartOf="#+id/input_layout_email_address"
app:layout_constraintTop_toBottomOf="#+id/input_layout_email_address"
app:layout_goneMarginTop="16dp" />
<com.google.android.material.textfield.TextInputEditText
android:id="#+id/input_email_address"
style="#style/MoenTextInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress"
app:onTextChanged="#{viewModel.onTextChanged}"
app:onFocusChange="#{inputLayoutEmailAddress}">
</com.google.android.material.textfield.TextInputEditText>
ViewModel
public class RegisterFragmentViewModel extends BaseObservable {
private boolean emailValidationVisible = false;
#Bindable
public boolean getEmailValidationVisible() {
return this.emailValidationVisible;
}
public void toggle() {
this.emailValidationVisible = !this.emailValidationVisible;
notifyPropertyChanged(BR.viewModel);
}
public void onTextChanged(CharSequence s, int start, int before, int count) {
Log.w("tag", "onTextChanged " + s);
this.toggle();
}
}
This is just a test I have. My thought was that I could bind the TextView visibility to a boolean that I can toggle/manipulate with a onTextChanged listener, but I don't know how to wire up the Binding Adapter. Am I on the right path? Is there a better/easier way to do this?

Am I on the right path? Is there a better/easier way to do this?
it is a way of doing it. I would remove the ternary operator from this line
android:visibility="#{viewModel.emailValidationVisible ? View.VISIBLE: View.GONE}"
and create a simple function in your VM that returns it. EG:
#Bindable
public int getVisibility() {
return emailValidationVisible ? View.VISIBLE: View.GONE
}
and in toggle(), you will have something like
public void toggle() {
this.emailValidationVisible = !this.emailValidationVisible;
notifyPropertyChanged(BR.visibility);
}
which will call the getter for you. Your xml will have to be changed like follow
android:visibility="#{viewModel.getVisibility()}"
Alternately you could create a BindingAdapter that takes a Boolean and change the visibility accordingly to it

Related

Android - Update view outside a custom control (from inside custom control)

I have a custom control with buttons (image) that should update an activity (fragment) control (big 0 string).
May be it is too simple but I don't know how to do it because onClickListener buttons are inside the custom control class. Which is the best approach?
Listener inside custom control class is:
binding.counterSelectorViewPrevious.setOnClickListener(OnClickListener {
decreaseValue()}
fun decreaseValue() {
if (mSelectedIndex > 0) {
val newSelectedIndex = mSelectedIndex - 1
setSelectedIndex(newSelectedIndex)
}
}
Activity control is just a TextView outside of custom control class.
<TextView
android:id="#+id/workout_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="0"
android:textAlignment="textEnd"
android:textAppearance="#style/TextAppearance.AppCompat.Display2"
tools:text="99:99:99" />
Thanks in advance.
You need to accept a listener in your custom view what get's some callback from your activity/fragment
something like this
private OnValueChangeListener onValueChangeListener;
public void addOnValueChangeListener(OnValueChangeListener onValueChangeListener)
{
this.onValueChangeListener = onValueChangeListener;
}
public void decreaseValue() {
onValueChangeListener.onValueChange(newValue);
}
public interface OnValueChangeListener {
void onValueChange(int newValue);
}
then you need to add invoke this somewhere in your activity/fragment (like in onCreate/onCreateView)
void setupListener() {
addOnValueChangeListener(newValue -> {
findViewById(R.id.workout_length).setText("" + newValue);
});
}

Can I have BindingAdapters with overlapping value sets?

Let's say I have this view model with methods:
public int getValueA() {
return a;
}
public int getValueB() {
return b;
}
#BindingAdapter("valueA")
public void setupSomething(View view, int valueA) {
// do something with a
}
#BindingAdapter({"valueA", "valueB"})
public void setupSomethingElse(View view, int valueA, int valueB) {
// do something with a and b
}
and I bind this to a view:
<View
android:layout_width="match_parent"
android:layout_height="wrap_content"
bind:valueA="#{viewmodel.valueA}"
bind:valueB="#{viewmodel.valueB}"/>
How can I call both BindingAdapter methods? Right now data binding is just calling the later. I guess I could just call setSomething from within setSomethingElse but this smells a little fishy to me (and partially defeats the purpose of databinding).
It's like you're suggesting yourself: you need to call setupSomething() from setupSomethingElse. It's just fine doing so and how databindings work. Only the best fitting #BindingAdapter will be used for your attributes.
Alternatively you can use the requireAll() field of #BindingAdapter. But this is only feasible if you can handle the Java default value (in your case 0) for your values.
Whether every attribute must be assigned a binding expression or if some can be absent. When this is false, the BindingAdapter will be called when at least one associated attribute has a binding expression. The attributes for which there was no binding expression (even a normal XML value) will cause the associated parameter receive the Java default value. Care must be taken to ensure that a default value is not confused with a valid XML value.
#BindingAdapter({"valueA", "valueB"}, requireAll = false)
public void setupSomethingElse(View view, int valueA, int valueB) {
if (valueA != 0) {
// do something with a
if (valueB != 0) {
// do something with a and b
}
}
}
So you don't need setupSomething() anymore. But personally I like the first approach better.

android databinding: how to avoid onCheckedChanged triggered by programmatically

I am now trying to use android data-binding in my project, and encounter this kind of issue, for example: I have 3 checkbox as a checkbox group, if first checkbox is checked, then a variable type is 1. the second makes type to 2, the 3rd makes type to 3. so I implement the code in this way.
// layout.xml
<android.support.v7.widget.AppCompatCheckBox
android:layout_width="50dp"
android:layout_height="50dp"
android:checked="#{userInfoViewModel.type == 1}"
android:onCheckedChanged="#{(compoundButton, checked) -> userInfoViewModel.onTypeChecked(checked, 1)}"
/>
<android.support.v7.widget.AppCompatCheckBox
android:layout_width="50dp"
android:layout_height="55dp"
android:checked="#{userInfoViewModel.type == 2}"
android:onCheckedChanged="#{(compoundButton, checked) -> userInfoViewModel.onTypeChecked(checked, 2)}"
/>
<android.support.v7.widget.AppCompatCheckBox
android:layout_width="50dp"
android:layout_height="55dp"
android:checked="#{userInfoViewModel.type == 3}"
android:onCheckedChanged="#{(compoundButton, checked) -> userInfoViewModel.onTypeChecked(checked, 3)}"
/>
// viewModel
public void onTypeChecked(boolean checked, int i) {
if (checked) {
// if it is a check. set the type
type.set(i);
} else {
// if it is a uncheck. set type to unknown
type.set(0);
}
}
Now the problem is that, if I have checked 1st checkbox, then I check the 2nd. type should be set to 2, and the UI should update correctly. But the reality is that uncheck event also occur on the 1st checkbox, after type is set to 2, then type.set(0) is triggered, so no checkbox is checked.
In fact, this issue is same to onCheckedChanged called automatically. What I need is a solution for data-binding.
In non-data-binding project, I think the best solution is using setCheckedSilent(answer by #Emanuel Andrada).
public void setCheckedSilent(boolean checked) {
super.setOnCheckedChangeListener(null);
super.setChecked(checked);
super.setOnCheckedChangeListener(listener);
}
But in data-binding, I can not do this. So is there any expert can help me out?
According to #Arpan Sharma's answer, listen to onClick instead of onCheckedChanged. This solution works currently, But I am worried about the value of checked, is it always right?
public void onTypeChecked(View view, int i) {
Boolean checked = ((CheckBox) view).isChecked();
if (checked) {
type.set(i);
} else {
type.set(0);
}
}
Expose an ObservableBoolean from the ViewModel, then use two-way databinding over that boolean.
Then you can use the ObservableBoolean's values to decide what you want to do, rather than encode it in the XML.
android:checked="#={vm.checkedValue}"
This is very simple with data binding
In xml checkbox component
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onCheckedChanged="#{(compoundButton, checked) ->
changeKeyboardVM.onCheckedChange(compoundButton, checked)}" />
In ViewModel or Activity
public void onCheckedChange(CompoundButton button, boolean check) {
Log.d("Z1D1", "onCheckedChange: " + check);
}
now Boolean check true on checked
and false on unchecked
I faced the same problem and i used onCLick listener instead onCHeck
listener .That way the listener wont change the check state when it is set programatically.
In your problem you should try setting different check change listeners to your check boxes.
I came across this question for first time and I think it's better to be implemented using binding adapter.
Here is the code for binding adapter
interface OnUserCheckedChangeListener {
fun onUserCheckChange(view:CompoundButton, isChecked:Boolean)
}
#BindingAdapter("onUserCheckedChange")
fun setUserCheckedChangeListener(view:CompoundButton, listener: OnUserCheckedChangeListener?){
if(listener == null){
view.setOnClickListener(null)
}else{
view.setOnClickListener {
listener.onUserCheckChange(view, view.isChecked)
}
}
}
And we can use it on any compound button
<CheckBox
android:id="#+id/finish_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
android:checked="#{your_condition}"
onUserCheckedChange="#{(view, checked) -> vm.onItemChecked(todo, checked)}"
/>
Using onClick instead of onCheckedChanged to prevent 2-ways binding.
From item_order_view.xml:
<data>
<variable
name="viewModel"
type="com.package.name.OrderItemViewModel" />
</data>
<CheckBox
android:id="#+id/cb_selected"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="8dp"
android:buttonTint="#color/white"
android:checked="#{viewModel.isSelect}"
android:onClick="#{() -> viewModel.onClickedCheckBox()}"
android:textColor="#color/white" />
From OrderItemViewModel.java
public class OrderItemViewModel {
public final ObservableField<Boolean> isSelect;
public final OrderItemViewModelListener mListener;
private final Order mOrder;
public OrderItemViewModel(Order order, OrderItemViewModelListener listener) {
this.mListener = listener;
this.mOrder = order;
isSelect = new ObservableField<>(mOrder != null ? mOrder.isSelect() : false);
}
/**
* Using onClick instead of onCheckedChanged
* to prevent 2-ways binding issue.
*/
public void onClickedCheckBox() {
mListener.onCheckChanged();
}
public interface OrderItemViewModelListener {
void onCheckChanged();
}
}
See https://stackoverflow.com/a/52606437/2914140:
Write inside OnCheckedChangeListener:
if (button.isPressed()) {
// A user pressed Switch.
}
Maybe some answers from How to use data binding for Switch onCheckedChageListener event? may help, for instance, defining android:onCheckedChanged listener, but I didnt test.

IntegerView instead of TextView

I have an Integer that I want to display in a TextView. Instead of having Textview foo and int foo with the same number all the time, I was hoping there was a way to do something like IntegerView. (A TextView that only takes integers) or that serves the same purpose. Please ask if you need clarification. My objective is to keep my code clean instead of either parsing 100 variables or having 200 variables instead of 100.
Simplification:
In class A, I have int a. I want to display int a in a TextView and then send it to class B. I want to keep my code as clean and elegant as possible. Suggestions?
Coding:
private TextView tvFoo;
private int foo;
public void thing(){
tvFoo.setText("" + foo);
DifferentClass.someMethod(foo);
}
<TextView
android:id="#+id/tvFoo" />
**I have a lot of variables to mess with, so I am trying to eliminate one of the 2.
Thanks!
Just subclass TextView.
public class IntegerView extends TextView {
...
public void setInt(int value) {
setText(String.valueOf(value));
}
public int getInt() {
return Integer.valueOf(getText().toString());
}
}
Put it in your xml layouts this way:
<com.package.to.IntegerView android:id="#+id/intView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
I would recommend you use the NumberPicker from this library.
https://github.com/derekbrameyer/android-betterpickers

Seekbars and EditTexts

I am working on an application in which I need the user to set an array of 30 values and to make it user friendly and easy to use I have decided to use a series of SeekBars and EditText widgets. I am using eclipse and have used it to set up the layout.
I am looking for an efficient way to set up the application to automatically update the value of the SeekBar when the value of the EditText has been changed and then use that same value in a application integer array.
<SeekBar android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:max="255"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:padding="10dp"
android:id="#+id/seekX"></SeekBar>
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight=".01"
android:minWidth="50sp"
android:maxWidth="50sp"
android:gravity="center_horizontal"
android:padding="10dp"
android:layout_marginRight="10dp"
android:inputType="number"
android:id="#+id/editX"></EditText>
The seek bars and EditText fields are named in a corresponding way editX should match seekX as editY should match seekY and so on. I am also storing the values in the int[] upgradeValues.
I have also set the SeekBar to have a max of 255 but i need a way to automatically change any value set in the EditText field above 255 down to 255 and similarly any value below 0 to 0.
Basically I need an efficient way of making "(ValueOF)seekX = (ValueOf)editX = (ValueOf)upgradeValues[x] >= 0 <= 255" and be updated if seekX or editX is changed.
I am still pretty new to Android development, so this answer isn't very specific, but hopefully it gets you on your way.
There are a few approaches. You can set change listeners on both of your seekbar and the edittext that update the values of the other (suppose you have your EditText t, SeekBar b and the value in model:
b.setOnSeekBarChangeListener(new OnSeekBarChangeListener()
{
#Override
public void onProgressChanged(final SeekBar seekBar,
final int progress,
final boolean fromUser)
{
if (fromUser)
{
model = progress;
updateUI();
}
}
#Override
public void onStartTrackingTouch(final SeekBar seekBar)
{
}
#Override
public void onStopTrackingTouch(final SeekBar seekBar)
{
}
});
t.setOnKeyListener(new View.OnKeyListener()
{
#Override
public boolean onKey(final View v,
final int keyCode,
final KeyEvent event)
{
model = Integer.parseInt(t.getText().toString());
updateUI();
return true;
}
});
Then updateUI() would be something like:
private void updateUI()
{
t.setText(model);
b.setProgress(model);
}
Those are some crazy names and I clearly did no error checking. But you get the idea. This is probably NOT the way you want to go, because you have to make sure you catch all the different ways the user interacts with each component (onKey probably isn't enough to detect whenever the EditText value changes).
The proper (MVC) way is to have your model (your int[]) contain the value and have your SeekBar and EditText use your model as their models. I don't know how to do that in Android's UI model.

Categories

Resources