I am using Android's two-way databinding for a form fragment with a the corresponding ViewModel. I am using an Observer object in the ViewModel that extends BaseObservable in order to use the @Bindable annotation and so far it has been working perfectly for TextInputEditTexts and MaterialSwitches. The problem I have now is that when using AutoCompleteTextView (within the TextInputLayout) for a spinner behaviour I can not make it work.
Layout:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_mesh_channel"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:hint="@string/device.parameter.mesh.channel"
android:enabled="@{!device_vm.observer.meshChannel.readOnly}"
android:visibility="@{device_vm.observer.meshChannel.basic ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toStartOf="@id/guideline_vertical_center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_mesh_password">
<AutoCompleteTextView
android:id="@+id/tv_mesh_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:simpleItems="@array/device.parameter.mesh.channel.values"
android:text="@={device_vm.observer.meshChannel.value}"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
ViewModel:
@Bindable
public StringValue getMeshChannel() {
IntegerValue meshChannel = currentDevice.getMeshChannel();
return new StringValue(String.valueOf(meshChannel.getValue()), meshChannel.isReadOnly(), meshChannel.isBasic());
}
public void setMeshChannel(String value) {
if(!String.valueOf(currentDevice.getMeshChannel().getValue()).equals(value)){
currentDevice.setMeshChannel(Integer.parseInt(value));
notifyPropertyChanged(BR.meshChannel);
}
}
I have tried app:simpleItems='@array/device.parameter.mesh.channel.values'
, and the values are set properly, but I need to show the initial value. For that I have used android:text="@={device_vm.observer.meshChannel.value}"
and it sets the value properly, but this removes the other values from the array set in simpleItems.
I have also tried setting the value via code in the Fragment like this deviceBinding.tvMeshChannel.setText(String.valueOf(deviceViewModel.getCurrentDevice().getMeshChannel().getValue()), false);
in order to not remove the other values, but whenever I change the value, the object in the view model does not get updated, so I think I am missing the two-way binding in doing this.
Answering my own question: yes, it is possible to do it, but there are a couple of changes to be done.
First, when you use app:simpleItems
in the layout along with android:text
, the text is set with filter, which removes all other options from the simpleItems. In my case, the initial value is only set once because the model won't change while editing (which I know is not the use case for two-way binding, but anyways), so the solution to this is to set the value of the view's text in the fragment after setting the viewmodel data, like this:
//Retrieved config via BLE
deviceViewModel.setCurrentDevice(connectedDevice.getDevice());
//Set the text with the second parameter (filter) of the method setText as false
deviceBinding.tvMeshChannel.setText(String.valueOf(deviceViewModel.getCurrentDevice().getMeshChannel().getValue()), false);
Second thing was what was preventing the value from changing. I was returning a new StringValue object in the getMeshChannel() method of the ViewModel, so as to show a String type in the view, which does not accept int types. The proper way to do it is return the IntegerValue in the getMeshChannel() method and use a Converter, as the documentation states (https://developer.android.com/topic/libraries/data-binding/two-way#converters). So the code ended up like this:
ViewModel:
@Bindable
public IntegerValue getMeshChannel() {
return currentDevice.getMeshChannel();
}
public void setMeshChannel(String value) {
if(!String.valueOf(currentDevice.getMeshChannel().getValue()).equals(value)){
currentDevice.setMeshChannel(Integer.parseInt(value));
notifyPropertyChanged(BR.meshChannel);
}
}
...
public static class ConfigValueConverter {
@InverseMethod("integerValueToString")
public static int stringToIntegerValue(String stringValue) {
return Integer.parseInt(stringValue);
}
public static String integerValueToString(int intValue) {
return String.valueOf(intValue);
}
}
Layout:
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_mesh_channel"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:hint="@string/device.parameter.mesh.channel"
android:enabled="@{!device_vm.currentDevice.meshChannel.readOnly}"
android:visibility="@{device_vm.currentDevice.meshChannel.basic ? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toStartOf="@id/guideline_vertical_center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_mesh_password">
<AutoCompleteTextView
android:id="@+id/tv_mesh_channel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:simpleItems="@array/device.parameter.mesh.channel.values"
android:text="@={ConfigValueConverter.integerValueToString(device_vm.observer.meshChannel.value)}"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>