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.
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)
}
}
}
}