如何使用 androidx 数据绑定(bind)库将自定义对象列表填充到 Spinner (app:entries)?以及如何为 Spinner (app:onItemSelected) 创建一个合适的选择回调?
我的布局:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="viewModel"
type=".ui.editentry.EditEntryViewModel" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.editentry.EditEntryActivity">
<Spinner
android:id="@+id/spClubs"
android:layout_width="368dp"
android:layout_height="25dp"
app:entries="@{viewModel.projects}"
app:onItemSelected="@{viewModel.selectedProject}"
/>
</FrameLayout>
</layout>
EditEntryViewModel.kt
class EditEntryViewModel(repository: Repository) : ViewModel() {
/** BIND SPINNER DATA TO THESE PROJECTS **/
val projects : List<Project> = repository.getProjects()
/** BIND SELECTED PROJECT TO THIS VARIABLE **/
val selectedProject: Project;
}
项目.kt
data class Project(
var id: Int? = null,
var name: String = "",
var createdAt: String = "",
var updatedAt: String = ""
)
Spinner 应该显示每个项目的名称,当我选择一个项目时,它应该保存在 viewModel.selectedProject 中。 LiveData 的使用是可选的。
我想我必须为 app:entries 编写一个@BindingAdapter,为 app:onItemSelected 编写一个@InverseBindingAdapter。但是如果不为 Spinneradapter 编写通常的样板代码,我无法弄清楚如何实现它们...
最佳答案
好的,我想出了一个合适的解决方案。这是带有一些解释的代码:
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
绑定(bind)到 val projects: List<Project>
在我的 ViewModel 中
app:selectedProject
绑定(bind)到 val entry: Entry
这是一个具有 Project
的类作为属性(property)。
所以这是我的 ViewModel 的一部分:
class EditEntryViewModel() : ViewModel() {
var entry: MutableLiveData<Entry> = MutableLiveData()
var projects : List<Project> = repository.getProjects()
}
现在缺少的是 BindingAdapter 和 InverseBindingAdapter 来实现以下事情:
- Spinner 应该列出来自存储库的所有项目
- Spinner 应该预先选择当前选择的项目
entry
- 选择新项目时,应将其设置为
entry
自动
绑定(bind)适配器
/** * 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) }
You can place the BindingAdapter in an empty file. It has not to be part of any class.
The important thing are its parameters. They are deducted by the BindingAdapters value
s. In this case the values are projects
, selectedProject
and selectedProjectAttrChanged
. The first two parameters correspond to the two layout-xml attributes that we defined ourselves. The last/third parameter is part of the DataBinding process: For each layout-xml attribute with two-way databining (i.e. @={) a value get generated with the name <attribute-name>AttrChanged
Another important part for this special case is the NameAdapter
which is my own SpinnerAdapter that is able to hold my Projects as items and only display their name
property in the UI. That way we always have access to the whole Project instances instead of only a String (which is usually the case for the default SpinnerAdapter).
Here's the code for my custom Spinner Adapter:
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 库它应该从 UI 设置什么值到实际的类属性 viewModel.entry.project
:
反向绑定(bind)适配器
@InverseBindingAdapter(attribute = "selectedProject") fun getSelectedProject(spinner: Spinner): Project { return spinner.selectedItem as Project }
That's it. All working smoothly together. One thing to mention is that this approach is not recommended if your List would contain a lot of data, since all this data is stored in the adapter. In my case it's only a bit of String fields, so it should be fine.
For completion, I wanna add the two methods from the 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
}
关于androidx 与 Spinner 和自定义对象的数据绑定(bind),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53355125/