Two-way databinding custom property on custom view

Viewed 2164

I'm having a hard time achieving two-way binding from a fragment's viewmodel to a property on a custom view. There are no issues two-way binding a String to an EditText, and one-way binding works fine with my custom SearchView. But when I change "@{vm.entry.item}" to "@={vm.entry.item}" on the custom view then the project won't compile, and throws:

error: cannot find symbol import com.test.test.databinding.FragmentTestBindingImpl;

I've been reading tutorials like this one for some time now, and tried fiddling with BindingAdapters, but having no luck. Feel like I'm just missing something obvious.

Kotlin

data class TestModel(
  var item: String? = null,
  var quantity: String? = null
)

class TestViewModel : ViewModel() {
  val entry = TestModel()
}

class TestFragment : Fragment() {

  private val vm: TestViewModel by lazy {
    ViewModelProvider(this).get(TestViewModel::class.java)
  }

  override fun onCreateView(
    inflater: LayoutInflater,
    parent: ViewGroup?,
    savedInstanceState: Bundle?
  ): View {
    val binding: FragmentTestBinding =
      DataBindingUtil.inflate(inflater, R.layout.fragment_test, parent, false)
    binding.setVariable(BR.vm, vm)
    binding.lifecycleOwner = viewLifecycleOwner
    return binding.root
  }
}

class SearchView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs, 0) {

  var selected: String? = null

  init {
    inflate(context, R.layout.view_search, this)

    val input: AutoCompleteTextView = findViewById(R.id.search_input)
    input.threshold = 1
    input.setAdapter(SearchAdapter(context))

    input.setOnItemClickListener { adapterView, _, position, _ ->
      val item: SearchModel = adapterView.getItemAtPosition(position) as SearchModel
      input.setText(item.name)
      selected = item.code
    }
  }
}

Fragment layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
            name="vm"
            type="com.test.test.ui.TestViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.test.test.ui.views.SearchView
            android:id="@+id/test_search"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:selected="@={vm.entry.item}" />

        <EditText
            android:id="@+id/test_edit_text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={vm.entry.quantity}" />

    </LinearLayout>

</layout>

SearchView layout

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="@dimen/paddingDouble"
    android:orientation="horizontal">

    <AutoCompleteTextView
        android:id="@+id/search_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</merge>
1 Answers

Turns out the answer was in the docs the whole time (crazy, right?).

The key takeaway seems to be that it's not enough to wrap the [Inverse]BindingAdapter functions in a companion object. They also have to be annotated with @JvmStatic.

My full custom view now looks like:

class SearchView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs, 0) {

  var selected: String? = null

  init {
    inflate(context, R.layout.view_search, this)
  }

  companion object {
    @BindingAdapter("selectedAttrChanged")
    @JvmStatic
    fun setListener(view: SearchView, listener: InverseBindingListener) {
      val input: AutoCompleteTextView = view.findViewById(R.id.search_input)
      input.threshold = 1
      input.setAdapter(SearchAdapter(view.context))
      input.setOnItemClickListener { adapterView, _, position, _ ->
        val item: SearchModel = adapterView.getItemAtPosition(position) as SearchModel
        input.setText(item.name)
        selected = item.code
        listener.onChange()
      }
    }

    @BindingAdapter("selected")
    @JvmStatic
    fun setTextValue(view: SearchView, value: String?) {
      if (value != view.selected) view.selected = value
    }

    @InverseBindingAdapter(attribute = "selected")
    @JvmStatic
    fun getTextValue(view: SearchView): String? = view.selected
  }
}
Related