Search code examples
androidkotlindatepickerandroid-textinputlayout

Kotlin - Android: Two-way databinding custom property on custom view


I've created a custom view for selecting days of the week which results is a string. I'd like to use it with two-way data binding.

<?xml version="1.0" encoding="utf-8"?><com.google.android.material.textfield.TextInputLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
    android:id="@+id/daypicker"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">

    <ToggleButton
        android:id="@+id/tMon"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Mon"
        android:textOn="@string/Mon" />

    <ToggleButton
        android:id="@+id/tTue"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Tue"
        android:textOn="@string/Tue" />

    <ToggleButton
        android:id="@+id/tWed"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Wed"
        android:textOn="@string/Wed" />

    <ToggleButton
        android:id="@+id/tThu"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Thu"
        android:textOn="@string/Thu" />

    <ToggleButton
        android:id="@+id/tFri"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Fri"
        android:textOn="@string/Fri" />

    <ToggleButton
        android:id="@+id/tSat"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Sat"
        android:textOn="@string/Sat" />

    <ToggleButton
        android:id="@+id/tSun"
        style="@style/toggleButton"
        android:layout_width="35dp"
        android:layout_height="35dp"
        android:layout_marginLeft="3dp"
        android:layout_marginRight="3dp"
        android:background="@drawable/toggle_bg"
        android:textOff="@string/Sun"
        android:textOn="@string/Sun" />
</LinearLayout></com.google.android.material.textfield.TextInputLayout>

And class to service:

class DayPicker : TextInputLayout {

var days: MutableSet<DayOfWeek> = HashSet()

private lateinit var tMon: ToggleButton
private lateinit var tTue: ToggleButton
private lateinit var tWed: ToggleButton
private lateinit var tThu: ToggleButton
private lateinit var tFri: ToggleButton
private lateinit var tSat: ToggleButton
private lateinit var tSun: ToggleButton

var mContext: Context? = null


constructor(context: Context) : super(context) {
    mContext = context
}

constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    mContext = context
    initControl(context)
    initDays()
    initListeners()
}

private fun initListeners() {
    initListener(tMon, DayOfWeek.MONDAY)
    initListener(tTue, DayOfWeek.TUESDAY)
    initListener(tWed, DayOfWeek.WEDNESDAY)
    initListener(tThu, DayOfWeek.THURSDAY)
    initListener(tFri, DayOfWeek.FRIDAY)
    initListener(tSat, DayOfWeek.SATURDAY)
    initListener(tSun, DayOfWeek.SUNDAY)
}

constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
    context,
    attrs,
    defStyle
) {
    mContext = context
}

@BindingAdapter("selectedDays")
fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
    days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
            ?: HashSet()) as MutableSet<DayOfWeek>
    
}

@InverseBindingAdapter(attribute = "selectedDays")
fun getSelectedDays(dayPicker: DayPicker): String {
    if (days.isEmpty()) {
        this.error = "emptyy"
    }
    return days.map { x -> x.value }.joinToString(",")
}

@BindingAdapter("selectedDaysAttrChanged")
fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener: InverseBindingListener) {
    listener.onChange()
}

/**
 * Load component XML layout
 */
private fun initControl(context: Context) {
    val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
    inflater.inflate(R.layout.daypicker, this, true)

    // layout is inflated, assign local variables to components
    tMon = findViewById(R.id.tMon)!!
    tTue = findViewById(R.id.tTue)!!
    tWed = findViewById(R.id.tWed)!!
    tThu = findViewById(R.id.tThu)!!
    tFri = findViewById(R.id.tFri)!!
    tSat = findViewById(R.id.tSat)!!
    tSun = findViewById(R.id.tSun)!!
}


fun initDays() {
    this.days.forEach { day ->
        if (day == DayOfWeek.MONDAY) {
            tMon.isChecked = true
        } else if (day == DayOfWeek.TUESDAY) {
            tTue.isChecked = true
        } else if (day == DayOfWeek.WEDNESDAY) {
            tWed.isChecked = true
        } else if (day == DayOfWeek.THURSDAY) {
            tThu.isChecked = true
        } else if (day == DayOfWeek.FRIDAY) {
            tFri.isChecked = true
        } else if (day == DayOfWeek.SATURDAY) {
            tSat.isChecked = true
        } else if (day == DayOfWeek.SUNDAY) {
            tSun.isChecked = true
        }
    }
}

fun initListener(button: ToggleButton, day: DayOfWeek) {
    button.setOnClickListener {
        if (button.isChecked) {
            days.add(day)
        } else {
            days.remove(day)
        }
    }
}

}

When I use it in activity/fragment:

 <.....textview.DayPicker
            android:id="@+id/daypicker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:visibleOrGone="@{viewModel.isSelectedDays().ld}"
            app:selectedDays="@={viewModel.treatment.selected_days}"/>

I receive an error:

.../androidApp/build/generated/source/kapt/debug/com/package/android/databinding/FragmentAddStep2BindingImpl.java:41: error: expected java.lang.String callbackArg_0 = mBindingComponent.null.getSelectedDays(daypicker);

This happens when I use two-way data binding: 'app:selectedDays="@={viewModel.treatment.selected_days}"'

I think that is something wrong with @InverseBindingAdapter but I don't know where.

I tried to look for a solution, but unfortunately I couldn't find it. I don't know what should I do to not have null in mBindingComponent object.


Solution

  • I was just doing a workaround and I found solution. I changed two way data binding to binding and I want in action manually get data from picker and replace in ViewModels data object.

    When I do that and I run app in this fragment I got error:

    E/AndroidRuntime: FATAL EXCEPTION: main Process: com.package.android, PID: 12221 java.lang.IllegalStateException: Required DataBindingComponent is null in class FragmentAddStep2BindingImpl. A BindingAdapter in com.package.utils.textview.DayPicker is not static and requires an object to use, retrieved from the DataBindingComponent. If you don't use an inflation method taking a DataBindingComponent, use DataBindingUtil.setDefaultComponent or make all BindingAdapter methods static. at androidx.databinding.ViewDataBinding.ensureBindingComponentIsNotNull(ViewDataBinding.java:709) at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:51) at com.package.android.databinding.FragmentAddStep2BindingImpl.(FragmentAddStep2BindingImpl.java:38) at com.package.android.DataBinderMapperImpl.getDataBinder(DataBinderMapperImpl.java:222)

    After that I modify my DayPicker class and I moved adapters of class outside. And finally compilation not show error! Functions in class are not properly implement so don't suggested in.

    var days: MutableSet<DayOfWeek> = HashSet()
    
    @BindingAdapter("selectedDays")
    fun setSelectedDays(dayPicker: DayPicker, selectedDays: String?) {
       if(selectedDays.isNullOrEmpty()){
           return
       }
       days = (selectedDays?.split(",")?.map { id -> DayOfWeek.of(Integer.parseInt(id)) }?.toSet()
        ?: HashSet()) as MutableSet<DayOfWeek>
    
     }
    
    @InverseBindingAdapter(attribute = "selectedDays")
    fun getSelectedDays(dayPicker: DayPicker): String {
        if (days.isEmpty()) {
            dayPicker.error = "emptyy"
        }
        return days.map { x -> x.value }.joinToString(",")
    }
    
    @BindingAdapter("selectedDaysAttrChanged")
    fun setSelectedDaysChangedListener(dayPicker: DayPicker, listener:    InverseBindingListener) {
      
       dayPicker
       listener.onChange()
    }
    
    class DayPicker : TextInputLayout {
    
    private lateinit var tMon: ToggleButton
    private lateinit var tTue: ToggleButton
    private lateinit var tWed: ToggleButton
    private lateinit var tThu: ToggleButton
    private lateinit var tFri: ToggleButton
    private lateinit var tSat: ToggleButton
    private lateinit var tSun: ToggleButton
    
    var mContext: Context? = null
    
    
    constructor(context: Context) : super(context) {
        mContext = context
    }
    
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        mContext = context
        initControl(context)
        initDays()
        initListeners()
    }
    
    private fun initListeners() {
        initListener(tMon, DayOfWeek.MONDAY)
        initListener(tTue, DayOfWeek.TUESDAY)
        initListener(tWed, DayOfWeek.WEDNESDAY)
        initListener(tThu, DayOfWeek.THURSDAY)
        initListener(tFri, DayOfWeek.FRIDAY)
        initListener(tSat, DayOfWeek.SATURDAY)
        initListener(tSun, DayOfWeek.SUNDAY)
    }
    
    constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(
        context,
        attrs,
        defStyle
    ) {
        mContext = context
    }
    
    
    
    /**
     * Load component XML layout
     */
    private fun initControl(context: Context) {
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        inflater.inflate(R.layout.daypicker, this, true)
    
        // layout is inflated, assign local variables to components
        tMon = findViewById(R.id.tMon)!!
        tTue = findViewById(R.id.tTue)!!
        tWed = findViewById(R.id.tWed)!!
        tThu = findViewById(R.id.tThu)!!
        tFri = findViewById(R.id.tFri)!!
        tSat = findViewById(R.id.tSat)!!
        tSun = findViewById(R.id.tSun)!!
    }
    
    
    fun initDays() {
        days.forEach { day ->
            if (day == DayOfWeek.MONDAY) {
                tMon.isChecked = true
            } else if (day == DayOfWeek.TUESDAY) {
                tTue.isChecked = true
            } else if (day == DayOfWeek.WEDNESDAY) {
                tWed.isChecked = true
            } else if (day == DayOfWeek.THURSDAY) {
                tThu.isChecked = true
            } else if (day == DayOfWeek.FRIDAY) {
                tFri.isChecked = true
            } else if (day == DayOfWeek.SATURDAY) {
                tSat.isChecked = true
            } else if (day == DayOfWeek.SUNDAY) {
                tSun.isChecked = true
            }
        }
    }
    
    fun initListener(button: ToggleButton, day: DayOfWeek) {
        button.setOnClickListener {
            if (button.isChecked) {
                days.add(day)
            } else {
                days.remove(day)
            }
        }
    }
    }