ChipGroup 2-way binding adapter

Viewed 4753

I would like to create ChipGroup 2-way binding adapter. I have copied default RadioGroup binding adapter with some changes but it doesn't work for both ways. In case of setting data to observable programmatically, ChipGroup retrieve changes from it. But manually Chip selection doesn't set changes to observable.

Here is my adapter

@InverseBindingMethods(InverseBindingMethod(type = ChipGroup::class, attribute = "android:checkedButton", method = "getCheckedRadioButtonId"))
class ChipGroupBindingAdapter {
companion object {
    @JvmStatic
    @BindingAdapter("android:checkedButton")
    fun setCheckedChip(view: ChipGroup?, id: Int) {
        if (id != view?.checkedChipId) {
            view?.check(id)
        }
    }

    @JvmStatic
    @BindingAdapter(value = ["android:onCheckedChanged", "android:checkedButtonAttrChanged"], requireAll = false)
    fun setChipsListeners(view: ChipGroup?, listener: ChipGroup.OnCheckedChangeListener?,
                          attrChange: InverseBindingListener?) {
        if (attrChange == null) {
            view?.setOnCheckedChangeListener(listener)
        } else {
            view?.setOnCheckedChangeListener { group, checkedId ->
                listener?.onCheckedChanged(group, checkedId)
                attrChange.onChange()
                }
            }
        }
    }
}

Layout file:

<android.support.design.chip.ChipGroup
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checkedButton="@{viewModel.checkedBtnObs}"
        app:singleSelection="true">

        <android.support.design.chip.Chip
            android:id="@+id/first_chip"
            style="@style/Widget.MaterialComponents.Chip.Choice"
            android:layout_width="110dp"
            android:layout_height="wrap_content"
            android:checkable="true"
            android:text="@string/month_12"
            app:chipBackgroundColor="@drawable/chip_background_selector" />

        <android.support.design.chip.Chip
            android:id="@+id/second_chip"
            style="@style/Widget.MaterialComponents.Chip.Choice"
            android:layout_width="110dp"
            android:layout_height="wrap_content"
            android:checkable="true"
            android:text="@string/month_6"
            android:textAlignment="center"
            app:chipBackgroundColor="@drawable/chip_background_selector" />

        <android.support.design.chip.Chip
            android:id="@+id/third_chip"
            style="@style/Widget.MaterialComponents.Chip.Choice"
            android:layout_width="110dp"
            android:layout_height="wrap_content"
            android:checkable="true"
            android:text="@string/month_1"
            app:chipBackgroundColor="@drawable/chip_background_selector" />

    </android.support.design.chip.ChipGroup>

And observable:

val checkedBtnObs = ObservableInt(R.id.second_chip)
3 Answers

Finally, I found solution. InverseBindingMethod method should be getCheckedChipId instead of getCheckedRadioButtonId

Also, @= should be added to xml android:checkedButton="@{viewModel.checkedBtnObs}" like this android:checkedButton="@={viewModel.checkedBtnObs}"

Now this adapter can be used for ChipGroup 2-way binding

@InverseBindingMethods(InverseBindingMethod(type = ChipGroup::class, attribute = "android:checkedButton", method = "getCheckedChipId"))
class ChipGroupBindingAdapter {
companion object {
    @JvmStatic
    @BindingAdapter("android:checkedButton")
    fun setCheckedChip(view: ChipGroup?, id: Int) {
        if (id != view?.checkedChipId) {
            view?.check(id)
        }
    }

    @JvmStatic
    @BindingAdapter(value = ["android:onCheckedChanged", "android:checkedButtonAttrChanged"], requireAll = false)
    fun setChipsListeners(view: ChipGroup?, listener: ChipGroup.OnCheckedChangeListener?,
                          attrChange: InverseBindingListener?) {
        if (attrChange == null) {
            view?.setOnCheckedChangeListener(listener)
        } else {
            view?.setOnCheckedChangeListener { group, checkedId ->
                listener?.onCheckedChanged(group, checkedId)
                attrChange.onChange()
                }
            }
        }
    }
}

I got ChipGroup with DataBinding working as expected and using the recommended way of InverseDataBinding, here's my implementation if anyone wish to know:

SomeBindingAdapters.kt:

...
object PropertyTypeFilterBindingAdapters {
    @BindingAdapter("propertyTypeFilter")
    @JvmStatic
    fun ChipGroup.bindPropertyTypeFilter(marsApiFilter: MarsApiFilter?) =
        marsApiFilter?.let { filter ->
            when (filter) {
                MarsApiFilter.ALL -> check(R.id.filter_all_properties_chip)
                MarsApiFilter.RENT -> check(R.id.filter_properties_for_rent_chip)
                MarsApiFilter.BUY -> check(R.id.filter_properties_for_buy_chip)
            }
        }

    @InverseBindingAdapter(attribute = "propertyTypeFilter")
    @JvmStatic
    fun ChipGroup.convertToMarsApiFilter(): MarsApiFilter = when (checkedChipId) {
        R.id.filter_properties_for_rent_chip -> MarsApiFilter.RENT
        R.id.filter_properties_for_buy_chip -> MarsApiFilter.BUY
        else -> MarsApiFilter.ALL
    }

    @BindingAdapter("propertyTypeFilterAttrChanged")
    @JvmStatic
    fun ChipGroup.setListeners(attrChange: InverseBindingListener?) =
        setOnCheckedChangeListener { _, _ -> attrChange?.onChange() }
}
...
  • MarsApiFilter is a enum class for my specific case.

some_layout.xml:

...
<com.google.android.material.chip.ChipGroup
    android:id="@+id/filter_property_type_chip_group"
    ...
    bind:propertyTypeFilter="@={viewModel.propertyTypeFilter}"
    >

    <com.google.android.material.chip.Chip
        android:id="@+id/filter_all_properties_chip"
        ...
        />

    <com.google.android.material.chip.Chip
        android:id="@+id/filter_properties_for_rent_chip"
        ...
        />

    <com.google.android.material.chip.Chip
        android:id="@+id/filter_properties_for_buy_chip"
        ...
        />
</com.google.android.material.chip.ChipGroup>
...
  • bind is just a fancy namespace declared as a regular one: xmlns:bind="http://schemas.android.com/apk/res-auto"

SomeViewModel.kt:

...
val propertyTypeFilter = MutableLiveData<MarsApiFilter>().apply { value = MarsApiFilter.ALL }
...
if (attrChange == null) {
            view?.setOnCheckedChangeListener(listener)
        } else {
            view?.setOnCheckedChangeListener { group, checkedId ->
                listener?.onCheckedChanged(group, checkedId)
                attrChange.onChange()
                }
            }
        }

maybe should be

if (listener != null) {
            view?.setOnCheckedChangeListener(listener)
        } else {
            view?.setOnCheckedChangeListener { group, checkedId ->
                listener?.onCheckedChanged(group, checkedId)
                attrChange.onChange()
                }
            }
        }
Related