带有Spinner和自定义对象的androidx数据绑定

时间:2018-11-17 20:10:50

标签: android android-spinner android-databinding android-viewmodel

您如何使用androidx数据绑定库在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;
}

Project.kt

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

微调框应显示每个项目的名称,当我选择一个项目时,应将其保存在viewModel.selectedProject中。 LiveData的使用是可选的。

我想我必须为app:entries写一个@BindingAdapter和为app:onItemSelected写一个@InverseBindingAdapter。但是如果不编写Spinneradapter的常用样板代码,我无法弄清楚如何实现它们。

3 个答案:

答案 0 :(得分:2)

这个问题和回复非常有帮助,因为我在过去几天里努力解决了类似的问题。本着分享的精神,这里是我所有的文件:

MainActivity.kt

package com.mandal.mvvmspinnerviewbindingexample

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import com.mandal.mvvmspinnerviewbindingexample.databinding.ActivityMainBinding
import com.mandal.mvvmspinnerviewbindingexample.viewmodel.UserViewModel


class MainActivity : AppCompatActivity()  {
    /**
     * Lazily initialize our [UserViewModel].
     */
   private val viewModel: UserViewModel by lazy {
       ViewModelProvider(this).get(UserViewModel::class.java)
   }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    enter code here
     val binding: ActivityMainBinding =
            DataBindingUtil.setContentView(this, R.layout.activity_main)

        // Allows Data Binding to Observe LiveData with the lifecycle of this Activity
        binding.lifecycleOwner = this

        // Giving the binding access to the UserViewModel
       binding.viewModel = viewModel

    }
}

User.kt

package com.mandal.mvvmspinnerviewbindingexample.model

data class User(
    val id: Int,
    val name: String,
) {
    override fun toString(): String = name
}

Entry.kt

package com.mandal.mvvmspinnerviewbindingexample.model

data class Entry (var user: User)

NameAdapter.kt

package com.mandal.mvvmspinnerviewbindingexample.adapter

import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.mandal.mvvmspinnerviewbindingexample.model.User

class NameAdapter(context: Context, textViewResourceId: Int, private val values: List<User>) : ArrayAdapter<User>(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
    }
}

UserViewModel.kt

package com.mandal.mvvmspinnerviewbindingexample.viewmodel

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.mandal.mvvmspinnerviewbindingexample.model.Entry
import com.mandal.mvvmspinnerviewbindingexample.model.User

class UserViewModel: ViewModel (){

    var users : List<User> = getUserList()
    var entry: MutableLiveData<Entry> = MutableLiveData()

    /**
     * Sets the value of the status LiveData to the Mars API status.
     */
    private fun getUserList() : List<User>{
        //Setup Users
        val user1 = User(1, "John")
        val user2 = User(2, "Mary")
        val user3 = User(2, "Patrick")
        val user4 = User(2, "Amanda")
        //Setup User List
        val list = arrayListOf<User>(user1, user2, user3, user4)
        return list
    }

}

BindingAdapters.kt

package com.mandal.mvvmspinnerviewbindingexample

import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.mandal.mvvmspinnerviewbindingexample.adapter.NameAdapter
import com.mandal.mvvmspinnerviewbindingexample.model.User

/**
 * fill the Spinner with all available projects.
 * Set the Spinner selection to selectedProject.
 * If the selection changes, call the InverseBindingAdapter
 */
@BindingAdapter(value = ["users", "selectedUser", "selectedUserAttrChanged"], requireAll = false)
fun setUsers(spinner: Spinner, users: List<User>?, selectedUser: User?, listener: InverseBindingListener) {
    if (users == null) return
    spinner.adapter = NameAdapter(spinner.context, R.layout.simple_spinner_dropdown_item, users)
    setCurrentSelection(spinner, selectedUser)
    setSpinnerListener(spinner, listener)
}


@InverseBindingAdapter(attribute = "selectedUser")
fun getSelectedUser(spinner: Spinner): User {
    Toast.makeText(
        spinner.context,
        (spinner.selectedItem as User).name,
        Toast.LENGTH_LONG
    )
        .show()
    return spinner.selectedItem as User
}

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: User?): Boolean {
    for (index in 0 until spinner.adapter.count) {
        if (spinner.getItemAtPosition(index) == selectedItem?.name) {
            spinner.setSelection(index)
            return true
        }
    }
    return false
}

activity_main.xml

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

    <variable name="viewModel"
        type="com.mandal.mvvmspinnerviewbindingexample.viewmodel.UserViewModel" />
</data>
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <!--Spinner widget-->
    <Spinner
        android:id="@+id/userSpinner"
        android:layout_width="160dp"
        android:layout_height="50dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.17"
        app:users="@{viewModel.users}"
        app:selectedUser="@={viewModel.entry.user}"
     />

</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mandal.mvvmspinnerviewbindingexample" >

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name="com.mandal.mvvmspinnerviewbindingexample.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>
        </activity>

    </application>

</manifest>

答案 1 :(得分:0)

您可以将其设置在片段内

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

请注意,项目应为

 MutableLiveData<List<Projects>>()

答案 2 :(得分:0)

好的,我想出了一个适当的解决方案。这是带有一些解释的代码:

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已绑定到我的ViewModel中的val projects: List<Project>

app:selectedProject绑定到val entry: Entry,后者是一个具有Project作为属性的类。

所以这是我的 ViewModel 的一部分:

class EditEntryViewModel() : ViewModel() {
    var entry: MutableLiveData<Entry> = MutableLiveData()
    var projects : List<Project> = repository.getProjects()
}

现在缺少实现以下目标的BindingAdapter和InverseBindingAdapter:

  1. 微调框应列出所有来自检举的项目
  2. 微调框应预先选择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扣除。在这种情况下,值为projectsselectedProjectselectedProjectAttrChanged。前两个参数对应于我们定义的两个layout-xml属性。最后一个/第三个参数是数据绑定过程的一部分:对于具有双向数据绑定(即@ = {)的每个layout-xml属性,将生成一个名称为<attribute-name>AttrChanged

此特殊情况的另一个重要部分是NameAdapter,它是我自己的SpinnerAdapter,能够将我的项目作为项目保存,并且仅在UI中显示其name属性。这样,我们始终可以访问整个Project实例,而不仅可以访问String(默认的SpinnerAdapter通常是这种情况)。

这是我的自定义微调框适配器的代码:

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设置什么值:

InverseBindingAdapter

    @InverseBindingAdapter(attribute = "selectedProject")
    fun getSelectedProject(spinner: Spinner): Project {
        return spinner.selectedItem as Project
    }

就是这样。所有人一起工作顺利。值得一提的是,如果您的列表包含很多数据,则不建议使用此方法,因为所有这些数据都存储在适配器中。就我而言,它只是一些String字段,所以应该没问题。


为完成操作,我想从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
}