привязка данных androidx с помощью Spinner и пользовательских объектов - PullRequest
0 голосов
/ 17 ноября 2018

Как использовать библиотеку привязки данных androidx для заполнения Spinner списком пользовательских объектов (приложение: записи) ? И как создать правильный обратный вызов выбора для 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;
}

Project.kt

data class Project(
    var id: Int? = null,
    var name: String = "",
    var createdAt: String = "",
    var updatedAt: String = ""
)

Spinner должен отображать имена каждого проекта, и когда я выбираю проект, он должен быть сохранен в viewModel.selectedProject. Использование LiveData не является обязательным.

Полагаю, мне нужно написать @BindingAdapter для app: records и @InverseBindingAdapter для app: onItemSelected. Но я не могу понять, как их реализовать, не написав обычный шаблонный код для Spinneradapter ...

Ответы [ 2 ]

0 голосов
/ 02 декабря 2018

Хорошо, я нашел правильное решение. Вот код с некоторыми пояснениями:

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 для достижения следующих целей:

  1. Spinner должен перечислить все проекты, поступающие из хранилища
  2. Spinner должен предварительно выбрать текущий выбранный проект из entry
  3. Когда выбран новый проект, он должен быть установлен на 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
}
0 голосов
/ 23 ноября 2018

Вы можете установить его внутри фрагмента

            binding.spinnerState.adapter = ArrayAdapter(
                context!!,
                R.layout.simple_spinner_item_1line,
                viewModel.projects?.map { it.name }!!
            )

Обратите внимание, что проект должен быть

 MutableLiveData<List<Projects>>()
...