Невозможно привязать LiveData List к записям Spinner - PullRequest
0 голосов
/ 31 декабря 2018

У меня есть фрагмент MyFragment, в котором есть Spinner my_spinner.Для тестирования моего приложения я изначально заполнил содержимое my_spinner вручную, наблюдая за свойством myLiveDataList в AndroidViewModel MyViewModel, как показано ниже:

my_fragment.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".ui.fragments.MyFragment">

    <Spinner
            android:id="@+id/my_spinner"
            android:layout_width="match_parent"
            android:layout_height="100dp" />
</FrameLayout>

MyFragment.kt

import androidx.lifecycle.ViewModelProviders
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.Observer

import com.example.app.R
import com.example.app.data.room.entities.MyEntity
import com.example.app.ui.viewmodels.MyViewModel
import kotlinx.android.synthetic.main.my_fragment.*

class MyFragment : Fragment() {

    companion object {
        fun newInstance() = MyFragment()
    }

    private lateinit var viewModel: MyViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.my_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)

        val myAdapter = ArrayAdapter<MyEntity>(this.context!!, android.R.layout.simple_spinner_item)

        // This is where I populate my_spinner
        viewModel.myLiveDataList.observe(this, Observer<List<MyEntity>> { data ->
            data?.forEach {
                myAdapter.add(it)
            }
        })

        my_spinner.adapter = myAdapter
    }
}

MyViewModel.kt

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.toLiveData
import com.example.app.data.repositories.MyRepository
import com.example.app.data.room.entities.MyEntity

class MyViewModel(application: Application) : AndroidViewModel(application) {
    private val myRepository = MyRepository(application)

    val myLiveDataList: LiveData<List<MyEntity>>
        get() = myRepository.getAllData().toLiveData()
}

Это успешно заполняет my_spinner при навигациина MyFragment:

App before making data-binding changes

Поскольку он заполняется, как и ожидалось, я сделал следующие изменения в my_fragment.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.example.app.ui.viewmodels.MyViewModel" />
    </data>

    <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".ui.fragments.MyFragment">

        <Spinner
                android:id="@+id/my_spinner"
                android:layout_width="match_parent"
                android:layout_height="100dp"
                app:entries="@{viewmodel.myLiveDataList}"/>
    </FrameLayout>
</layout>

Я добавил в файл Binding Adapter BindingAdapterUtil (следующий код был скопирован из этой статьи ):

import android.R
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Spinner
import androidx.databinding.BindingAdapter
import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.getSpinnerValue
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerEntries
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerInverseBindingListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerItemSelectedListener
import com.example.app.ui.adapter.SpinnerExtensions.setSpinnerValue

@BindingAdapter("entries")
fun Spinner.setEntries(entries: List<Any>?) {
    setSpinnerEntries(entries)
}

@BindingAdapter("onItemSelected")
fun Spinner.setItemSelectedListener(itemSelectedListener: SpinnerExtensions.ItemSelectedListener?) {
    setSpinnerItemSelectedListener(itemSelectedListener)
}

@BindingAdapter("newValue")
fun Spinner.setNewValue(newValue: Any?) {
    setSpinnerValue(newValue)
}

@BindingAdapter("selectedValue")
fun Spinner.setSelectedValue(selectedValue: Any?) {
    setSpinnerValue(selectedValue)
}

@BindingAdapter("selectedValueAttrChanged")
fun Spinner.setInverseBindingListener(inverseBindingListener: InverseBindingListener?) {
    setSpinnerInverseBindingListener(inverseBindingListener)
}

@InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged")
fun Spinner.getSelectedValue(): Any? {
    return getSpinnerValue()
}

object SpinnerExtensions {

    fun Spinner.setSpinnerEntries(entries: List<Any>?) {
        if (entries != null) {
            val arrayAdapter = ArrayAdapter(context, R.layout.simple_spinner_item, entries)
            arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
            adapter = arrayAdapter
        }
    }

    fun Spinner.setSpinnerItemSelectedListener(listener: ItemSelectedListener?) {
        if (listener == null) {
            onItemSelectedListener = null
        } else {
            onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
                override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                    if (tag != position) {
                        listener.onItemSelected(parent.getItemAtPosition(position))
                    }
                }

                override fun onNothingSelected(parent: AdapterView<*>) {}
            }
        }
    }

    fun Spinner.setSpinnerInverseBindingListener(listener: InverseBindingListener?) {
        if (listener == null) {
            onItemSelectedListener = null
        } else {
            onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
                override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
                    if (tag != position) {
                        listener.onChange()
                    }
                }

                override fun onNothingSelected(parent: AdapterView<*>) {}
            }
        }
    }

    fun Spinner.setSpinnerValue(value: Any?) {
        if (adapter != null ) {
            val position = (adapter as ArrayAdapter<Any>).getPosition(value)
            setSelection(position, false)
            tag = position
        }
    }

    fun Spinner.getSpinnerValue(): Any? {
        return selectedItem
    }

    interface ItemSelectedListener {
        fun onItemSelected(item: Any)
    }
}

И я изменил onActivityCreatedв MyFragment примерно так:

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)

    DataBindingUtil.setContentView<MyFragmentBinding>(
        this.activity!!, R.layout.my_fragment
    ).apply {
        this.setLifecycleOwner(this@MyFragment)
        this.viewmodel = viewModel
    }
}

В результате этого my_spinner больше не заполняется содержимым MyViewModel.myLiveDataList.Чтобы попытаться выяснить, было ли свойство ошибочным, я создал новое свойство в MyViewModel, например, так:

val myList: List<String>?
    get() = listOf("First", "Second", "Third")

И я связал это свойство с my_spinner, как и MyViewModel.myLiveDataList выше, с успехом.этот раз.

Функция в MyRepository.getAllData() (которую возвращает myLiveDataList) возвращает Flowable<List<MyEntity>> (RxJava), который вызывает Room DAO для получения данных.Здесь я предполагаю, что myLiveDataList не имеет ничего, чтобы служить, когда он пытается связать значения в первый раз, и никогда не пытается снова.

Я что-то упускаю при попытке связать источник данных LiveData сспиннер?

1 Ответ

0 голосов
/ 01 января 2019

После прочтения этого ответа я изменил my_fragment.xml на следующее:

...
<data>
    <import type="java.util.List" />
    <import type="com.example.app.data.room.entities.MyEntity" />
    <import type="androidx.lifecycle.LiveData" />
    <variable name="viewmodel"
              type="com.example.app.ui.viewmodels.MyViewModel" />

    <variable name="myTestList"
              type="LiveData&lt;List&lt;MyEntity&gt;&gt;" />
</data>
...
<Spinner
    android:id="@+id/my_spinner"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    app:entries="@{myTestList}"/>
...

Я также удалил содержимое MyFragment.onActivityCreated и изменил MyFragment.onCreateViewследующим образом:

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
    val binding = MyFragmentBinding.inflate(inflater, container, false)
    binding.setLifecycleOwner(this)
    binding.viewmodel = viewModel
    binding.myTestList = viewModel.myLiveDataList

    return binding.root
}

Не идеальное решение, и я до сих пор не знаю, почему мой первоначальный бой по этой проблеме не принес желаемых результатов, но это будет полезно.Если есть лучший способ привязки Spinner к LiveData таким способом, пожалуйста, дайте мне знать.

...