Search code examples
javaandroidkotlinandroid-databindingandroid-binding-adapter

Databinding, MaterialCardView should act like Radiogroup


I am trying to implement two MaterialCardViews that should act like a Radiogroup. So if I click one, the other should be unchecked. I am using viewModel, liveData and custom two-way data binding to save these values for later purpose (sending per email).

I had success writing the .xml and implementing the check logic, but I struggle implementing uncheck logic.

XML, short version for better visibility

<layout
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="vm"
            type="com.example.app.data.viewmodel.EmailViewModel" />
    </data>

        <com.google.android.material.card.MaterialCardView
            android:id="@+id/cardViewOne"
            android:checkable="true"
            android:clickable="true"
            android:focusable="true"
            <!-- Custom Two way databinding -->
            app:state_checked="@={vm.cardOptionOneChecked}"
        </com.google.android.material.card.MaterialCardView>


        <com.google.android.material.card.MaterialCardView
            android:id="@+id/cardViewTwo"
            android:checkable="true"
            android:clickable="true"
            android:focusable="true"
            <!-- Custom Two way databinding -->
            app:state_checked="@={vm.cardOptionTwoChecked}">

        </com.google.android.material.card.MaterialCardView>
</layout>

ViewModel

class EmailViewModel @ViewModelInject constructor(
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // Variable for Id = cardViewOne
    val cardOptionOneChecked = MutableLiveData<Boolean>()
    
    // Variable for Id = cardViewTwo
    val cardOptionTwoChecked = MutableLiveData<Boolean>()
}

CardViewAdapter.kt

@BindingAdapter("state_checked")
fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
    if (view.isChecked != liveData.value) {
        liveData.value = view.isChecked
    }
}

@InverseBindingAdapter(attribute = "state_checked")
fun getStateChecked(view: MaterialCardView,): Boolean {
    return view.isChecked
}

// I don't know what logic belongs here to make it work! 
// Current approach just checks the current view and does nothing more. How can I save the last
// checked value?
@BindingAdapter("state_checkedAttrChanged")
fun setCheckedAttrListener(
    view: MaterialCardView,
    attrChange: InverseBindingListener,
) {
    view.apply {
        setOnClickListener { view.isChecked = true }
        setOnCheckedChangeListener { card, isChecked ->
            if (card.isChecked && card != view) {
                card.isChecked = false
            }
        }
        attrChange.onChange()
    }
}

I appreciate every help, thank you very much!

P.S: If there is a better and easier way to achieve this e.g. telling the viewModel from the view to save isChecked, please inform me. MaterialCardView has implemented "isChecked" by default but no logic.


Solution

  • Okay, I've solved the Problem:

    First, Change Binding Adapter

    I actually don't saw any way to use two-way data binding to achieve the above written case. Here is the new Binding Adapter

    // View = Clicked MaterialCard, liveData = value in viewModel
    @BindingAdapter("state_checked")
    fun setStateChecked(view: MaterialCardView, liveData: MutableLiveData<Boolean>) {
        if (view.isChecked != liveData.value) {
            if (liveData.value != null) {
                view.isChecked = liveData.value!!
            }
        }
    }
    

    Second, Change XML Layout, because we don't use two-way data binding anymore

    <layout
            xmlns:app="http://schemas.android.com/apk/res-auto">
        
            <data>
                <variable
                    name="vm"
                    type="com.example.app.data.viewmodel.EmailViewModel" />
            </data>
        
                <com.google.android.material.card.MaterialCardView
                    android:id="@+id/cardViewOne"
                    android:checkable="true"
                    android:clickable="true"
                    android:focusable="true"
                    <!-- Deleted "=" -->
                    app:state_checked="@{vm.cardOptionOneChecked}"
                </com.google.android.material.card.MaterialCardView>
        
    
                <com.google.android.material.card.MaterialCardView
                    android:id="@+id/cardViewTwo"
                    android:checkable="true"
                    android:clickable="true"
                    android:focusable="true"
                    <!-- Deleted "=" -->
                    app:state_checked="@{vm.cardOptionTwoChecked}">
        
                </com.google.android.material.card.MaterialCardView>
        </layout>
    

    Third, Change viewmodel

    class EmailViewModel @ViewModelInject constructor(
        @ApplicationContext context: Context,
        @Assisted private val savedStateHandle: SavedStateHandle
    ) : ViewModel() {
    
        val cardOptionOneChecked = MutableLiveData<Boolean>()
        
        val cardOptionTwoChecked = MutableLiveData<Boolean>()
    
        // Added
         fun firstCardClicked() {
            cardOneChecked.value = true
            cardTwoChecked.value = false
        }
    
        fun secondCardClicked() {
            cardOneChecked.value = false
            cardTwoChecked.value = true
        }
    }
    

    Fourth, add clickListener to XML or Fragment (here Fragment)

    cardViewOne.setOnClickListener {
        viewModel.firstCardClicked()
    }
    cardViewTwo.setOnClickListener {
        viewModel.secondCardClicked()
    }
    

    If someone has any questions, just write it in the comments, I will help.