Хорошо, я нашел правильное решение. Вот код с некоторыми пояснениями:
layout.xml
<Spinner
android:id="@+id/spProjects"
android:layout_width="368dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spActivities"
app:projects="@{viewModel.projects}"
app:selectedProject="@={viewModel.entry.project}" />
app:projects
привязан к val projects: List<Project>
в моей ViewModel
app:selectedProject
связан с val entry: Entry
, который является классом с Project
в качестве свойства.
Так что это часть моего ViewModel :
class EditEntryViewModel() : ViewModel() {
var entry: MutableLiveData<Entry> = MutableLiveData()
var projects : List<Project> = repository.getProjects()
}
В настоящее время не хватает BindingAdapter и InverseBindingAdapter для достижения следующих целей:
- Spinner должен перечислить все проекты, поступающие из хранилища
- Spinner должен предварительно выбрать текущий выбранный проект из
entry
- Когда выбран новый проект, он должен быть установлен на
entry
автоматически
BindingAdapter
/**
* fill the Spinner with all available projects.
* Set the Spinner selection to selectedProject.
* If the selection changes, call the InverseBindingAdapter
*/
@BindingAdapter(value = ["projects", "selectedProject", "selectedProjectAttrChanged"], requireAll = false)
fun setProjects(spinner: Spinner, projects: List?, selectedProject: Project, listener: InverseBindingListener) {
if (projects == null) return
spinner.adapter = NameAdapter(spinner.context, android.R.layout.simple_spinner_dropdown_item, projects)
setCurrentSelection(spinner, selectedProject)
setSpinnerListener(spinner, listener)
}
Вы можете поместить BindingAdapter в пустой файл. Это не должно быть частью какого-либо класса.
Важными являются его параметры. Они вычитаются из BindingAdapters value
s. В этом случае значения projects
, selectedProject
и selectedProjectAttrChanged
. Первые два параметра соответствуют двум атрибутам layout-xml, которые мы определили сами. Последний / третий параметр является частью процесса DataBinding: для каждого атрибута layout-xml с двусторонней привязкой данных (т. Е. @ = {) генерируется значение с именем <attribute-name>AttrChanged
Другой важной частью этого особого случая является NameAdapter
, который является моим собственным SpinnerAdapter, который может содержать мои Проекты как элементы и отображать их свойство name
только в пользовательском интерфейсе. Таким образом, мы всегда имеем доступ ко всем экземплярам Project, а не только к String (что обычно имеет место для SpinnerAdapter по умолчанию).
Вот код моего адаптера Spinner:
NameAdapter
class NameAdapter(context: Context, textViewResourceId: Int, private val values: List<Project>) : ArrayAdapter<Project>(context, textViewResourceId, values) {
override fun getCount() = values.size
override fun getItem(position: Int) = values[position]
override fun getItemId(position: Int) = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val label = super.getView(position, convertView, parent) as TextView
label.text = values[position].name
return label
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
val label = super.getDropDownView(position, convertView, parent) as TextView
label.text = values[position].name
return label
}
}
Теперь, когда у нас есть Spinner, который хранит всю информацию о нашем проекте, InverseBindingAdapter стал простым. Он используется, чтобы сообщить библиотеке DataBinding, какое значение следует установить из пользовательского интерфейса для фактического свойства класса viewModel.entry.project
:
InverseBindingAdapter
@InverseBindingAdapter(attribute = "selectedProject")
fun getSelectedProject(spinner: Spinner): Project {
return spinner.selectedItem as Project
}
Вот и все. Все работают гладко вместе. Стоит упомянуть, что этот подход не рекомендуется, если ваш список будет содержать много данных, так как все эти данные хранятся в адаптере. В моем случае это только немного строковых полей, так что все должно быть в порядке.
Для завершения я хочу добавить два метода из BindingAdapter:
private fun setSpinnerListener(spinner: Spinner, listener: InverseBindingListener) {
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) = listener.onChange()
override fun onNothingSelected(adapterView: AdapterView<*>) = listener.onChange()
}
}
private fun setCurrentSelection(spinner: Spinner, selectedItem: HasNameField): Boolean {
for (index in 0 until spinner.adapter.count) {
if (spinner.getItemAtPosition(index) == selectedItem.name) {
spinner.setSelection(index)
return true
}
}
return false
}