aksh1618
aksh1618

Reputation: 2571

Unable to access property of custom LiveData using Data binding

I'm trying to use live data with data binding for TextInputLayout using a class like this:

class MutableLiveDataWithErrorText<T> : MutableLiveData<T>() {
    val errorText = MutableLiveData<String>().apply { value = "" }
}

Now, when trying to use it for error text in the xml,

<layout>
    <data>
        <!-- ... -->
        <variable
            name="target"
            type="com.my.app.MutableLiveDataWithErrorText&lt;String&gt;" />

    </data>

    <com.google.android.material.textfield.TextInputLayout
        app:errorEnabled="true"
        app:errorText="@{target.errorText}">

        <!-- ... -->

    </com.google.android.material.textfield.TextInputLayout>
</layout>

I get this error:

Cannot find getter 'getErrorText' for type String.

I tried creating a BindingAdapter to get around this:

@BindingAdapter("errorTextLive")
fun setErrorTextLive(
    view: TextInputLayout,
    liveDataWithErrorText: MutableLiveDataWithErrorText<String>
) {
    if (liveDataWithErrorText.errorText.value.isNullOrEmpty().not()) {
        view.error = liveDataWithErrorText.errorText.value
    }
}

with xml assignment changed to:

app:errorTextLive="@{target}"

which makes the compilation succeed, but changes to target.errorText are no longer observed, instead it observes changes in target, updating errorText only when target's value changes.

Is there a way to make the it observe target.errorText?

Upvotes: 0

Views: 944

Answers (2)

Sergei Bubenshchikov
Sergei Bubenshchikov

Reputation: 5371

It's bad pattern to pass view model as set of fields instead of one composite object. If you need to pass at least two variable to data-binding, you need to create view model with this fields - it help you to make changes more flexible.

For example, you can define view model like this one:

class SimpleViewModel : ViewModel() {

    /**
     * Expose MutableLiveData to enable two way data binding
     */
    val textData = MutableLiveData<String>().apply { value = "" }
    /**
     * Expose LiveData for read only fields
     */
    val errorText = Transformations.map(textData, ::validateInput)

    /**
     * Validate input on the fly
     */
    private fun validateInput(input: String): String? = when {
        input.isBlank() -> "Input is blank!"
        else -> null
    }
}

on layout side it very closer to your variant:

<layout>
    <data>
        <variable
            name="vm"
            type="com.example.SimpleViewModel" />

    </data>

    <android.support.design.widget.TextInputLayout
        app:errorEnabled="true"
        app:errorText="@{vm.errorText}">

        <android.support.design.widget.TextInputEditText
            android:text="@={vm.textData}"/>

    </android.support.design.widget.TextInputLayout>
</layout>

NOTE: it is not necessary SimpleViewModel to extend ViewModel, but it allow your data to survive configuration changes out of the box

Upvotes: 1

anon
anon

Reputation:

Is there a way to make the it observe target.errorText?

I don't think so. The problem is, that the databinding library is resolving target inside target.errorText first and sees that it is of type MutableLiveData<String>, it then automatically gets the value of target, which is of type String and then tries to call getErrorText() on that String object, which leads to the error you see.

I had a similar use-case and I resorted to creating the following class:

class <T> ValidatableValue {

    val liveData = MutableLiveData<T>() // The actual value.

    // Other helper livedatas and functions.
    val isValid = MutableLiveData<Boolean>()
    val errorMessage = MutableLiveData<String>()

    fun validate() { ... }
}

I can then use all these LiveData objects in the databinding layout.

Upvotes: 1

Related Questions